ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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

     

    Dart: What are mixins?

    It’s a kind of magic ✨

    medium.com

     

    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 에 대하여 알아보았습니다.

    댓글