-
Flutter 의 App State Management Provider 에 대하여 알아봅시다.Flutter 2019. 8. 27. 02:18
Google I/0 2019 에서 발표 된 Flutter 의 State Management 방법인
Provider 패키지를 이용하는 방법을 간단하게 핵심만 알아 봅시다.
Provider 는 Flutter 커뮤니티에서 시작되었으며, 이후 Google 에서
리 패키징하여 현재 공식적으로 업데이트를 하고 있습니다.
Bloc, Inheritedwidget, ScopeModel, Redux 등 Flutter 에서 State Management관련 기법이
이미 다양하게 있지만 Provider 는 좀 더 깔끔하고 간편하게 사용할 수 있습니다.
ScopeModel 의 상위 버전 같은 느낌입니다.
간단하게 기본 제공되는 Counter App 에서 사용 해 봅시다.
새로운 프로젝트를 만들고,
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( brightness: Brightness.dark, ), home: MyHomePage(title: 'Provider Example'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.display1, ), ], ), ), floatingActionButton: FloatingActionButton( backgroundColor: Colors.amber, onPressed: _incrementCounter, tooltip: 'Increment', child: Icon(Icons.add), ), ); } }
여기서 Counter 의 Increment 와 Decrement, 그리고 Reset 기능을 Provider를 이용하여 추가 해봅시다.
먼저 Provider 패키지를 설치 합시다.
Provider : https://pub.dev/packages/provider
이제 Increment 와 Decrement 버튼을 별도로 만들고, Floating Action Button은
Reset 을 실행하도록 할 것 입니다.
일단 기본 state 를 이용하여 counter 를 변경 하도록 만듭시다.
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { ... } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; // Counter 관련 함수 추가 void _incrementCounter() { setState(() { _counter++; }); } void _decrementCounter() { setState(() { if (_counter > 0) _counter--; }); } void _reset() { setState(() { _counter = 0; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Container( margin: EdgeInsets.symmetric( vertical: 10.0, horizontal: 10.0, ), child: Text( '$_counter', style: Theme.of(context).textTheme.display1, ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: <Widget>[ RaisedButton( elevation: 4.0, // 증가 onPressed: _incrementCounter, child: Text('INCREMENT'), ), RaisedButton( elevation: 4.0, // 감소 onPressed: _decrementCounter, child: Text('DECREMENT'), ), ], ) ], ), ), floatingActionButton: FloatingActionButton( backgroundColor: Colors.amber, // 리셋 onPressed: _reset, tooltip: 'Increment', child: Icon(Icons.refresh), ), ); } }
정상적으로 작동하는 것을 확인 할 수 있습니다.
이제 현재 화면의 state 인 counter 를 Provider 를 사용하여 관리 해 봅시다.
먼저 state 를 저장하고 변경 할 모델 클래스를 만들어야 합니다.
// provider/counter_provider.dart import 'package:flutter/widgets.dart'; class CounterProvider with ChangeNotifier { int _counter; get counter => _counter; CounterProvider(this._counter); void incrementCounter() { _counter++; notifyListeners(); } void decrementCounter() { if (_counter > 0) _counter--; notifyListeners(); } void reset() { _counter = 0; notifyListeners(); } }
state 로 _counter 를 가지고 있고, counter state 를 변경 할 increment, decrement,
그리고 reset 메소드를 가지고 있습니다.
그리고 해당 클래스에 ChangeNotifier 를 mixin 방식으로 가져오고 있습니다.
이 mixin (사용시에는 with ) 은 일종의 추상클래스 라고 할 수 있습니다
다만 기존 extends 로 가져오는 추상 클래스와 다르게 확장 할 수 없고,
with 와 함께 사용 시 mixin 으로 사용되는 클래스의 기능을 빌려와서 해당 클래스에서 사용할 수 있도록 해줍니다.
다중 상속이 되지 않는 Dart 에서 여러 클래스의 기능을 구현하기 위해 사용되기도 합니다.
주의 할 점은 mixin 은 계층이 스택형식으로 클래스가 쌓입니다.
무슨 말인고 하니, 만약 class A with D, C, B 로 선언하면 스택이 B, C, D 순으로 쌓이게 됩니다.
D,C,B 가 공통적인 메소드가 있다면 스택 중 가장 밑에 있는 B 의 메소드가 실행되는 것 입니다.
CounterProvider 는 ChangeNotifier 클래스를 확장 할 이유가 없습니다.
해당 클래스의 기능만이 필요한 것 입니다.
따라서 extends 가 아닌 with 로 해당 클래스를 불러오도록 한 것 입니다.
물론 extends 로 선언해도 현재로선 사용하는데 문제가 없습니다.
Dart 의 mixin 에 대해 좀 더 자세히 알고 싶다면 아래 링크의 글을 봅시다.
https://medium.com/flutter-community/dart-what-are-mixins-3a72344011f3
counter state 의 변경 후 실행 되는 notifiyListeners() 는 객체가 변경 될때 마다 리스너 들에게 변경 되었다고 알려주게 됩니다.
변경 소식을 받은 listener 들은 변경된 상태 값을 가지고 widget 을 rebuild 하고 다시 화면에 나타나게 됩니다.
해당 listener 를 지정하기 위해서 provider.of() 또는 consumer() 를 사용하여 지정할 수 있습니다.
react 의 context API 와 상당히 유사 합니다.
이제 main 화면에 provider 를 주입시켜 봅시다.
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( brightness: Brightness.dark, primaryColor: Colors.amber, textTheme: TextTheme( display1: TextStyle(color: Colors.white), ), buttonTheme: ButtonThemeData( buttonColor: Colors.amber, textTheme: ButtonTextTheme.primary, ), ), home: MultiProvider( providers: [ ChangeNotifierProvider( builder: (_) => CounterProvider(0), ) ], child: MyHomePage(title: 'Provider Example'), ), ); } }
Provider 를 주입시킬 때 어디서 주입 시킬지 생각해야 합니다.
보통 해당 Provider 의 state 를 사용할 위젯의 한 스택 위에 Provider 를 지정 합니다.
현재 MyHomePage 위젯의 자식 위젯에서 사용할 테니 MyHomePage 위젯의 부모로 Provider 위젯을 사용합시다.
위 코드에서 MultiProvider 를 사용했는데, 여러개의 Provider 를 사용할 때 사용합니다.
현재 Provider 는 하나지만 예제를 위해서 MultiProvider 를 사용 하였습니다.
main.dart 에 적용시켜봅시다.
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:provider_example/provider/counter_provider.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( brightness: Brightness.dark, primaryColor: Colors.amber, textTheme: TextTheme( display1: TextStyle(color: Colors.white), ), buttonTheme: ButtonThemeData( buttonColor: Colors.amber, textTheme: ButtonTextTheme.primary, ), ), // MultiProvider 가 적용됩니다. home: MultiProvider( providers: [ ChangeNotifierProvider( builder: (_) => CounterProvider(0), ) ], child: MyHomePage(title: 'Provider Example'), ), ); } } class MyHomePage extends StatefulWidget { ... } class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { // Provider.of 를 사용하여 Provider 를 불러옵니다. final CounterProvider counter = Provider.of<CounterProvider>(context); return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Container( margin: EdgeInsets.symmetric( vertical: 10.0, horizontal: 10.0, ), child: Text( // Provider 로 값을 가져옵니다. '${counter.counter}', style: Theme.of(context).textTheme.display1, ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: <Widget>[ RaisedButton( elevation: 4.0, // Provider 를 이용한 메소드 실행 onPressed: counter.incrementCounter, child: Text('INCREMENT'), ), RaisedButton( elevation: 4.0, // Provider 를 이용한 메소드 실행 onPressed: counter.decrementCounter, child: Text('DECREMENT'), ), ], ) ], ), ), floatingActionButton: FloatingActionButton( backgroundColor: Colors.amber, // Provider 를 이용한 메소드 실행 onPressed: counter.reset, tooltip: 'Increment', child: Icon(Icons.refresh), ), ); } }
기존에 있던 _counter 변수와 increment, decrement, reset 함수를 지우고
provider.of 를 이용하여 CounterProvider 를 해당 위젯에 전역적으로 사용합니다.
또 하나의 방법인 Consumer 를 이용한 예제도 살펴봅시다.
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:provider_example/provider/counter_provider.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { ... } class MyHomePage extends StatefulWidget { ... } class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { // Consumer 를 사용하여 위젯을 리턴한다. return Consumer<CounterProvider>( builder: (_, counter, child) => Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Container( margin: EdgeInsets.symmetric( vertical: 10.0, horizontal: 10.0, ), child: Text( '${counter.counter}', style: Theme.of(context).textTheme.display1, ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: <Widget>[ RaisedButton( elevation: 4.0, onPressed: counter.incrementCounter, child: Text('INCREMENT'), ), RaisedButton( elevation: 4.0, onPressed: counter.decrementCounter, child: Text('DECREMENT'), ), ], ) ], ), ), floatingActionButton: FloatingActionButton( backgroundColor: Colors.amber, onPressed: counter.reset, tooltip: 'Increment', child: Icon(Icons.refresh), ), ), ); } }
Consumer 사용과 Provider.of 의 현재 동작은 같습니다.
그럼 Provider.of 와 Consumer 의 차이점은 무엇일까요?
Consumer 는 상태 변경이 일어날때 builder 에 지정된 위젯을 rebuild 하게 됩니다.
state 의 변경에 의해서 위젯의 rebuild 가 필요할 때 consumer 를 지정하여 사용해야 합니다.
state 와 관련없는 위젯까지 rebuild 할 필요가 없겠죠.
현재 코드에서 counter 가 변경될때 변경되어야 하는 위젯은 중앙에 위치한 Text 위젯 입니다.
따라서 Consumer 는 변경될 위젯 가장 깊숙한 곳 에서 사용하는 것이 좋습니다.
Provider.of 는 consumer 와 같이 해당 Provider 클래스에 접근 할 수 있지만 조금 다른 용도로 주로 사용 됩니다.
CounterProvider 클래스에는 state 를 변경하는 increment, decrement, reset 메소드가 있습니다.
해당 메소드를 실행 할 때 특정 위젯이나 상태가 필요하지 않습니다.
해당 위젯의 상태 변경도 없고, 단지 메소드만 실행하는 위젯 들 입니다.
이런 경우에는 해당 메소드를 실행시키는 위젯은 notify 를 받을 필요가 없습니다.
이런 경우 provider.of 의 옵션으로 listen 값을 false 로 주고 사용할 수 있습니다.
이 경우 해당 위젯들은 notify 를 받지 않습니다.
해당 사항을 준수한 최종 코드는 아래와 같습니다.
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:provider_example/provider/counter_provider.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { ... } class MyHomePage extends StatefulWidget { ... } class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { // 순수하게 메소드 실행만을 위해 Notify 를 받지 않는 Provider.of 를 지정 final counter = Provider.of<CounterProvider>(context, listen: false); return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Container( margin: EdgeInsets.symmetric( vertical: 10.0, horizontal: 10.0, ), // state 에 의해 변경되는 부분에 Consumner 를 적용 child: Consumer<CounterProvider>( builder: (context, counter, child) => Text( '${counter.counter}', style: Theme.of(context).textTheme.display1, ), ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: <Widget>[ RaisedButton( elevation: 4.0, onPressed: counter.incrementCounter, child: Text('INCREMENT'), ), RaisedButton( elevation: 4.0, onPressed: counter.decrementCounter, child: Text('DECREMENT'), ), ], ) ], ), ), floatingActionButton: FloatingActionButton( backgroundColor: Colors.amber, onPressed: counter.reset, tooltip: 'Increment', child: Icon(Icons.refresh), ), ); } }
state 변경 메소드 실행을 위한 Provider.of 는 listen 값을 false 로 사용하고,
변경이 필요한 위젯은 Consumer 로 분리하였습니다.
본 코드에서는 Stateful 위젯을 사용했지만, 지정된 state 가 변경될 때 위젯을 rebuild 하기 때문에
특정 상황이 아니면 Stateful 위젯을 사용할 필요 없이 대부분 Stateless 위젯을 사용할 수 있습니다.
이로서 간단한게 Flutter 의 Provider 에 대하여 알아보았습니다.
'Flutter' 카테고리의 다른 글
Flutter Local Storage (SQLite) 사용하기 with BLoC Pattern (4) 2019.07.31 Flutter 란 무엇인가? RN 과 뭐가 다르지? (1) 2019.04.24