providerを解説

こんにちはnasustです。 pub.devで配布されているproviderを解説します。 providerはInheritedWidgetを使いやすくしたものです。

DIと状態管理を簡単に、そしてスケールしやすい実装ができます。 Google I/O 2019で推奨されました。

この動画でproviderが如何に便利か解説されています。

pubspac.yaml

providerを利用するには以下のようにpubspac.yamlを記述します。バージョンは配布サイトを参考に最新を指定してください。

dependencies:
  provider: ^4.3.3

Provider - 値を伝達するWidget

providerの値の伝達方法は以下のProviderを使用します。InheritedWidgetと同じように子のWidgetに値を伝達します。

Provider

値のライフサイクルをマネージメントするクラスです。BLoCのインスタンス化の為にStatefulWidgetを作成しなくてもProvider<T> classで扱うことができます。

Provider<Bloc>(
  create: (context) => Bloc(),
  dispose: (context, bloc) => bloc.dispose(),
  child: ...,
)

createでモデルの作成、disposeで破棄の処理を書きます。

または既存のモデルを使用する場合は、Provider<T>.valueを使用します。

Provider<Bloc>.value(
  value: _myBloc,
  child: ...,
)

.valueは他のProviderでも用意されています。

see also: Provider class - provider library - Dart API

サンプルコード:

class CounterBloc {
  final _incrementController = StreamController<void>();
  final _countController = StreamController<int>();

  var _count = 0;

  CounterBloc() {
    _incrementController.stream.map((v) => _count++).pipe(_countController);
  }

  Sink<void> get increment => _incrementController.sink;
  Stream<void> get counter => _countController.stream;

  void dispose() async {
    await _incrementController.close();
    await _countController.close();
  }
}

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Provider<CounterBloc>(
        create: (context) => CounterBloc(),
        dispose: (context, bloc) => bloc.dispose(),
        child: _MyHomePage(title: 'Flutter Demo Home Page'),
      ),
    );
  }
}

class _MyHomePage extends StatelessWidget {
  _MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(this.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Consumer<CounterBloc>(builder: (_, bloc, __) {
              return StreamBuilder<int>(
                stream: bloc.counter,
                initialData: 0,
                builder: (context, snapshot) {
                  return Text(
                    '${snapshot.data}',
                    style: Theme.of(context).textTheme.headline4,
                  );
                },
              );
            }),
          ],
        ),
      ),
      floatingActionButton: Consumer<CounterBloc>(
        builder: (_, bloc, __) {
          return FloatingActionButton(
            onPressed: () => bloc.increment.add(null),
            tooltip: 'Increment',
            child: Icon(Icons.add),
          );
        },
      ),
    );
  }
}

初期のカウンターのプロジェクトをProviderとBlocを使用するように変更しました。 StatefulWidgetを使用していません。

Provider.ofConsumerは祖先のWidgetのProviderから値を取得します。

Provider.ofの解説はこちら
Consumerの解説はこちらにあります。

MultiProvider

複数のProviderを子のWidgetから値を読み取れるようにします。

MultiProvider(
  providers: [
    Provider<FirstBloc>(create:(_) => FirstBloc() , dispose: (_,bloc) => bloc.dispose() ),
    Provider<SecondBloc>(create:(_) => SecondBloc() , dispose: (_,bloc) => bloc.dispose() ),
  ],
  child: Widget(),
)

providersProvider<T>の配列を指定します。 上記のコードの場合、FirstBlocまたはSecondBlocの値が取得できます。

see also: MultiProvider class - provider library - Dart API

サンプルコード:

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MultiProvider(
        providers: [
          Provider<FirstCounterBloc>(
            create: (context) => FirstCounterBloc(),
            dispose: (context, bloc) => bloc.dispose(),
          ),
          Provider<SecondCounterBloc>(
            create: (context) => SecondCounterBloc(),
            dispose: (context, bloc) => bloc.dispose(),
          ),
        ],
        child: _MyHomePage(title: 'Flutter Demo Home Page'),
      ),
    );
  }
}

class _MyHomePage extends StatelessWidget {
  _MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(this.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            _BlocCounterText(),
          ],
        ),
      ),
      floatingActionButton: Consumer2<
        FirstCounterBloc,
        SecondCounterBloc
        >(
        builder: (_, firstBloc, secondBloc, __) {
          return FloatingActionButton(
            onPressed: () {
              firstBloc.increment.add(null);
              secondBloc.increment.add(null);
            },
            tooltip: 'Increment',
            child: Icon(Icons.add),
          );
        },
      ),
    );
  }
}

class _BlocCounterText extends StatelessWidget {
  
  Widget build(BuildContext context) {
    final firstBloc = Provider
      .of<FirstCounterBloc>(context, listen: false);
    final secondBloc = Provider
      .of<SecondCounterBloc>(context, listen: false);
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        StreamBuilder<int>(
          stream: firstBloc.counter,
          initialData: 0,
          builder: (context, snapshot) {
            return Text(
              '${snapshot.data} : ',
              style: Theme.of(context).textTheme.headline4,
            );
          },
        ),
        StreamBuilder<int>(
          stream: secondBloc.counter,
          initialData: 0,
          builder: (context, snapshot) {
            return Text(
              '${snapshot.data}',
              style: Theme.of(context).textTheme.headline4,
            );
          },
        ),
      ],
    );
  }
}

上記のコードの場合は、FirstCounterBlocは通常のカウンター、SecondCounterBlocはカウンターを100倍を返すBlocです。画面には1 : 100と表示されます。

Consumer2は二個のモデルを取得出来ます。

ProxyProvider

他のProviderの値から新しいProviderを作成します。

MultiProvider(
  providers: [
    Provider<FirstModel>(
      create: (context) => FirstModel(),
    ),
    ProxyProvider<FirstModel, SecondModel>(
      update: (context, firstModel, secondModel) {
        return SecondModel(firstModel);
      },
    )
  ],
  child: Widget(),
)

createまたはupdateによって作成された値が伝達されます。updateは複数回実行される場合があります。最初の作成時、ProxyProviderが再作成されるか、指定したProviderが更新された場合に実行されます。

上記のコードのようにSecondModelFirstModelに依存しています。このような場合、ProxyProviderを使用します。

ProxyProviderはProxyProvider2など、指定できる型の数が多いものあります。

see also: ProxyProvider class - provider library - Dart API

サンプルコード:

class Counter with ChangeNotifier {
  var _count = 0;

  void increment() {
    ++_count;
    notifyListeners();
  }

  int get counter => _count;
}

class CounterMessage {
  Counter _counter;

  CounterMessage({ Counter counter}) {
    _counter = counter;
  }

  String get message => "Count ${_counter.counter} ";
}

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MultiProvider(
        providers: [
          ChangeNotifierProvider<Counter>(create: (_) => Counter()),
          ProxyProvider<Counter, CounterMessage>(
            update: (_, counter, counterMessage)
             => CounterMessage(counter: counter),
          ),
        ],
        child: _MyHomePage(title: 'Flutter Demo Home Page'),
      ),
    );
  }
}


class _MyHomePage extends StatelessWidget {
  _MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(this.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Consumer<CounterMessage>(
              builder: (_, counterMessage, __) {
                return Text(
                  counterMessage.message,
                  style: Theme.of(context).textTheme.headline4,
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: Consumer<Counter>(
        builder: (_, counter, __) {
          return FloatingActionButton(
            onPressed: () {
              counter.increment();
            },
            tooltip: 'Increment',
            child: Icon(Icons.add),
          );
        },
      ),
    );
  }
}

CounterMessageCounterを依存しています。Counterが更新されるとProxyProviderupdateが実行されます。_CounterTextが再ビルドされ”Count XX”と画面に表示されます。

StreamProvider

Streamの値を子のWidgetから値を読み取れるようにします。 データの受信のStreamなどに使用します。

final _streamController = StreamController<int>();


Widget build(BuildContext context) {
  return StreamProvider<T>(
    create: (context) => Stream<T>,
    child: Widget()

see also: StreamProvider class - provider library - Dart API

サンプルコード:

class _MyHomePage extends StatelessWidget {
  _MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(this.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'Stream Count:',
            ),
            StreamProvider<int>(
              initialData: 0,
              create: (_) => Stream<int>
                .periodic(Duration(seconds: 1), (count) => count + 1),
              child: Consumer<int>(builder: (_, counter, __) {
                return Text(
                  "$counter",
                  style: Theme.of(context).textTheme.headline4,
                );
              }),
            ),
          ],
        ),
      ),
    );
  }
}

上記のコードではカウンターの値のStreamを表示します。 ConsumerでStreamの値を取得して表示しています。

FutureProvider

Futureの値を子のWidgetから値を読み取れるようにします。

see also: FutureProvider class - provider library - Dart API

サンプルコード:

class MyHomePage extends StatelessWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            FutureProvider<String>(
              initialData: 'wait...',
              create: (context) {
                return Future
                  .delayed(Duration(seconds: 10), () => "Future delayed");
              },
              child: Consumer<String>(builder: (_, message, __) {
                return Text(
                  "$message",
                  style: Theme.of(context).textTheme.headline4,
                );
              }),
            )
          ],
        ),
      ),
    );
  }
}

画面に表示されるinitialDataから、三秒後にFuture delayedへ変化します。

ChangeNotifierProvider

ChangeNotifierの値を子のWidgetから値を読み取れるようにします。

ChangeNotifierProvider(
  create: (_) => new ChangeNotifier(),
  child: ...
)

サンプルコードは#proxyproviderと同じです。

ChangeNotifierProxyProvider

ProxyProviderと同じく、モデルからモデルを更新する場合に使用します。

ChangeNotifierProxyProvider<ParentModel, DependencyModel>(
  create: (_) => DependencyModel(),
  update: (_, parentModel, dependencyModel) => dependencyModel
    ..update(parentModel),
  child: ...
);

上記のコードの場合、 ParentModelは親のProviderで定義されています。 updateDependencyModelParentModelを設定して更新します。

see also: ChangeNotifierProxyProvider class - provider library - Dart API

サンプルコード:

class Counter with ChangeNotifier {
  int _counter = 0;

  get value => _counter;

  void increment() {
    _counter++;
    notifyListeners();
  }
}

class CounterMessage with ChangeNotifier {
  Counter _counter;

  void setCounter(Counter counter) {
    _counter = counter;
    notifyListeners();
  }

  String get message => "Count ${_counter.value} ";
}

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: ChangeNotifierProvider(
        create: (_) => new Counter(),
        child: MyHomePage(title: 'Flutter Demo Home Page'),
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  
  Widget build(BuildContext context) {
    return ChangeNotifierProxyProvider<Counter, CounterMessage>(
      create: (context) => CounterMessage(),
      update: (context, counter, counterMessage) =>
        counterMessage..setCounter(counter),
      child: Scaffold(
        appBar: AppBar(
          title: Text(title),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                'You have pushed the button this many times:',
              ),
              Consumer<CounterMessage>(
                builder: (_, counterMessage, __) {
                  return Text(
                    '${counterMessage.message}',
                    style: Theme.of(context).textTheme.headline4,
                  );
                },
              ),
            ],
          ),
        ),
        floatingActionButton: Consumer<Counter>(
          builder: (_, counter, __) {
            return FloatingActionButton(
              onPressed: () => counter.increment(),
              tooltip: 'Increment',
              child: Icon(Icons.add),
            );
          },
        ),
      ),
    );
  }
}

ListenableProvider

ChangeNotifierProviderの親クラスです。 使い方は同じです。 同じように使用できます。 AnimationなどListenableを自分で実装した場合に使用します。

see also: ListenableProvider class - provider library - Dart API

ListenableProxyProvider

ChangeNotifierProxyProviderの親クラスです。 使い方は同じです。

see also: ListenableProxyProvider class - provider library - Dart API

ValueListenableProvider

ValueNotifier で更新が通知された時、子のWidgetで値を取得できるようにします。

see also: ValueListenableProvider class - provider library - Dart API

サンプルコード:

class _MyHomePageState extends State<MyHomePage> {
  final _counter = new ValueNotifier(0);

  
  Widget build(BuildContext context) {
    return ValueListenableProvider<int>.value(
      value: _counter,
      child: 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:',
              ),
              Consumer<int>(builder: (_, value, __) {
                return Text(
                  '$value',
                  style: Theme.of(context).textTheme.headline4,
                );
              }),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () => _counter.value++,
          tooltip: 'Increment',
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

値の読み取り

一番近い祖先のProviderから値を取得できます。 取得方法は以下のようにあります。

  • Provider.of
  • Consumer
  • Selector
  • context.watch
  • context.read
  • context.selector

Provider.of

一番近い祖先のWidgetのProviderから値を取得します。

final T value = Provider.of<T>(
      context,
      listen: true(default) または false
);

listenはtrueの場合、値が変更されるとStatefulWidgetのリビルドが発生します。 falseの場合は発生しません。 falseのProvider.ofは、State.initStateとcreateで使用出来ます。

see also: of method - Provider class - provider library - Dart API

サンプルコード:

class Counter with ChangeNotifier {
  int _counter = 0;
  get value => _counter;
  void increment() {
    _counter++;
    notifyListeners();
  }
}

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: ChangeNotifierProvider<Counter>(
        create: (context) => Counter(),
        child: MyHomePage(title: 'Flutter Demo Home Page'),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  
  Widget build(BuildContext context) {
    final counter = Provider
      .of<Counter>(context, listen: false);
    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.value}',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => counter.increment(),
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

listen:falseのサンプルコードです。 値の更新が通知されても再ビルドされません。 trueにすると際ビルドされます。

Consumer

Consumer<T>(
  builder: (context, model, child) =>
     ...,
  )

親のProviderから値を取得して、builderに渡します。 builderは値が変更された場合などで呼び出されます。

BuildContextを指定しなくてもProviderから値を取得できます。 リビルドの範囲を指定しやすいのでパフォーマンスを最適化できます。

see also: Consumer class - provider library - Dart API

builderのchildはConstructorのchildのインスタンスが渡されます。

Consumer<T>(
  builder: (context, model, child) => //childはText("hoge")のオブジェクト
     ...,
  )
  child: Text("hoge"),

これはbuilderが複数回呼び出されても、 Text("hoge")のインスタンスは生成されないです。 これにより部分的に再ビルドできるので最適化できます。

MultiProviderのprovidersの内部でも使用できます。

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MultiProvider(
        providers: [
          ValueListenableProvider<bool>(
            create: (context) => ValueNotifier(true),
          ),
          Consumer<bool>(
            builder: (context, value, child) {
              return Provider<Counter>(
                create: (context) => Counter(flg: value),
                child: child,
              );
            },
          )
        ],
        child: MyHomePage(title: 'Flutter Demo Home Page'),
      ),
    );
  }
}

Consumerのbuilderのvalueパラメーターはtrueになります。 ValueListenableProviderの値が渡されます。

Selector

Consumerと同じ動作をします。 違いは、値の更新の判定をする為のモデルが設定できます。 判定する事で不要なリビルドを防ぐ事ができます。

Selector<Counter, int>(
  selector: (_, counter) => counter.value,
  builder: (_, data, __) {
    return Text('$data');
  }
)

selectorでcounter.valueの値を比較するようにしています。
値の比較は前回の値と==で比較します。

see also: Selector class - provider library - Dart API

context.watch

context.watchは以下の様な実装となっています。

T watch<T>() {
    return Provider.of<T>(this);
  }

thisはBuildContextを指しています。

context.read

context.readは以下の様な実装となっています。

T read<T>() {
    return Provider.of<T>(this, listen: false);
}

thisはBuildContextを指しています。

context.selector

context.selectorはSelectorと同じ様な動作をします。

final value = context.select((Counter counter) => Counter.value);
Text('$value');
iOS、Android、Web、APIサーバーなどのフロントエンド・バックエンドを開発するソフトウェアエンジニアです。 UI/UXが好きです。かっこいいUIやWebデザインを眺めるのが趣味です。 このブログはソフトウェア開発関係の内容を記事にしています。
web service:
GitHubQiitaTwitterはてなブログ
handle name:
nasust
real name:
hideki mori
job:
ソフトウェアエンジニア
develop:
target: ios, android, web page, single page application, api server, system service, cli tool, linux embedded device

lang: c/c++, go, swift, objective-c, java, kotlin, typescript, dart, javascript, ruby, python, php

tool: vscode, xcode, android studio, photoshop, vim, docker