Flutter Provider 패턴

|

Provider Pattern

여기서 Provider는 생산자/소비자 패턴에서의 생산자를 의미합니다. ReactRedux와 비슷한 개념이며 Flutter 기본 샘플 프로그램인 카운터 프로그램에 Provider 패턴을 적용해보도록 하겠습니다.


원래 코드

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Provider Pattern Example',
      theme: ThemeData(
        primarySwatch: Colors.indigo,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Provider Pattern 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.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}


Provider 패턴 적용 맛보기

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  provider: ^4.1.3


counter.dart 파일을 다음과 같이 작성합니다.

counter.dart

import 'package:flutter/material.dart';

class Counter with ChangeNotifier {
  int _count = 0;

  int get getCount => _count;

  void incrementCounter() {
    _count = _count + 1;
    notifyListeners();
  }
}


main.dart

여기서 눈여겨 볼 부분은 MyApp 위젯에서 ChangeNotifierProivder를 사용한 것과 _MyHomePageState 클래스에서 Provider를 연동한 부분입니다.

그리고 MyHomePage 위젯이 StatefulWidget에서 StatelessWidget가 되었습니다. 더 이상 _count라는 변수를 가질 필요가 없기 때문에 State를 가질 필요가 없어졌습니다. 기존에는 화면을 갱신하기 위해서 setState() 메소드를 호출했었지만, Provider를 사용하면 notifyListeners() 메소드를 통해 해당 데이터를 사용하는 모든 위젯들을 갱신할 수 있습니다.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'counter.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Provider Pattern Example',
      theme: ThemeData(
        primarySwatch: Colors.indigo,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: ChangeNotifierProvider<Counter>(
        builder: (context) => Counter(),
        child: MyHomePage(title: 'Provider Pattern Example'),
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  final String title;

  MyHomePage({this.title});

  @override
  Widget build(BuildContext context) {
    final consumer = Provider.of<Counter>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${consumer.getCount.toString()}',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Provider.of<Counter>(context, listen: false).increment();
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}


Provider 패턴의 장점

  • Provider 패턴은 데이터 공유와 로직의 분리를 좀 더 쉽게 할 수 있게 해줍니다. 로직이 분리되면 코드가 좀 더 깔끔해줍니다.
  • Provider를 이용하면 각 위젯을 StatefulWidget 대신 StatelessWidget로 변경할 수 있어서 위젯의 부담이 줄어듭니다. 각 위젯이 상태값을 가지지 않는 다는 것은 그만큼 함수형 구현(동일한 입력값에 대해서는 항상 동일한 결과가 출력됨)에 더 근접하는 것이며, 보다 더 안정적인 코드를 작성할 수 있습니다.
  • 앱을 구성하는 여러 화면에서 참조해야 하는 공통의 데이터를 관리할 때 유용합니다.


Provider 패턴의 구조

위에서 작성했던 예제를 조금 더 구조화해서 설명하도록 하겠습니다.

Provider는 기본적으로 child로 위젯(Widget)을 가집니다. 위에서 작성했던 코드에는 다음과 같은 형태로 사용하고 있습니다.

ChangeNotifierProvider<Counter>(
  builder: (context) => Counter(),
  child: MyHomePage(title: 'Provider Pattern Example'),
)

그리고 공유하는 데이터는 ChangeNotifierwith로 구현해줍니다. notifyListeners() 메소드를 통해 해당 데이터를 사용하는 위젯들에게 변경 이벤트를 전송해줍니다.

import 'package:flutter/material.dart';

class Counter with ChangeNotifier {
  int _count = 0;

  int get getCount => _count;

  void incrementCounter() {
    _count = _count + 1;
    notifyListeners();
  }
}

위의 예제에서는 여러 개의 Provider를 사용했지만, 아래의 예제처럼 여러 개의 Proivder를 사용할 수도 있습니다. (위의 예제보다는 좀 더 정돈된 코드입니다.)

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(
          create: (context) => Counter(),
        ),
      ],
      child: MaterialApp(
        title: 'Provider Pattern Example',
        theme: ThemeData(
          primarySwatch: Colors.indigo,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: MyHomePage(title: 'Provider Pattern Example'),
      ),
    );
  }
}

Provider의 데이터는 아래와 같이 Provider.of를 이용해서 가져올 수 있습니다.

final consumer = Provider.of<Counter>(context);

다만, 위의 코드처럼 구현하면 위젯의 재빌드가 자주 발생하게 되는데 이 경우는 Provider.of 대신 ConsumerSelector를 사용하고 해당 데이터를 사용하는 부분으로 호출 코드를 이동시켜주면 됩니다.

Consumer<Counter>(
  builder: (context, counter, child) {
    return Text(
      '${counter.getCount.toString()}',
      style: Theme.of(context).textTheme.headline4,
    );
  },
),

전체 위젯 코드는 다음과 같습니다.

class MyHomePage extends StatelessWidget {
  final String title;

  MyHomePage({this.title});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'You have pushed the button this many times:',
            ),

            Consumer<Counter>(
              builder: (context, counter, child) {
                return Text(
                  '${counter.getCount.toString()}',
                  style: Theme.of(context).textTheme.headline4,
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Provider.of<Counter>(context, listen: false).increment();
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}