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 : 値の作成と破棄を管理します。
- MultiProvider: 複数のProviderを管理します。
- ProxyProvider: 他のモデルの更新通知を受け取って依存するモデルを更新できます。
- StreamProvider: Streamを監視して値を受信すると再ビルドします。
- FutureProvider: Futureを監視して完了すると再ビルドします。
- ChangeNotifierProvider: ChangeNotifierの通知を監視して再ビルドします。
- ChangeNotifierProxyProvider: ChangeNotifierを作成して値の更新と同期します。
- ListenableProvider: Listenableのイベントを受信して再ビルドします。
- ListenableProxyProvider: Listenableを作成して値の更新と同期します。
- ValueListenableProvider: ValueListenableのイベントを受信して再ビルドします。
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.of
とConsumer
は祖先の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(
…
),
)
providers
はProvider<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が更新された場合に実行されます。
上記のコードのようにSecondModel
はFirstModel
に依存しています。このような場合、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),
);
},
),
);
}
}
CounterMessage
はCounter
を依存しています。Counter
が更新されるとProxyProvider
のupdate
が実行されます。_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で定義されています。
update
でDependencyModel
をParentModel
を設定して更新します。
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');