Pragmatic State Management in Flutter (Google I/O'19)の中でこれからFlutter始めるならproviderを推奨するって言われてたので少しだけ触ってみました。
今回はChangeNotifierProviderのみです。
Providerとは
A dependency injection system built with widgets for widgets. provider is mostly syntax sugar for InheritedWidget, to make common use-cases straightforward.
READMEに書かれている通り、ウィジェット用のDIするものです。
下記のように親で提供したものが子孫で使えるようになり、引数で渡し続けなくて良くなります。
reactのcontext APIのproviderとかreact-reduxのproviderとかと同じ感じだと思います。
void main() => runApp(
Provider<String>.value(
value: 'Hello World', // これが子孫で使えるようになる
child: MaterialApp(
home: Child(),
),
),
);
class Child extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Grandchild();
}
}
class Grandchild extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text(Provider.of<String>(context)); // 今回はここで使用している。下位ウィジェットでも同様に取得できる。
}
}
ChangeNotifierProviderを利用した実装
下のgifのような動きをするアプリケーションをChangeNotifierProviderを利用して実装しただけのものです。
コードは下記になります。
流れとしては、「ChangeNotifierProviderがChangeNotifierをmixinしたCounterを子孫ウィジェットに提供する。Counterは状態変更したらnotifyListenersで変更通知を行う。変更通知により再レンダリングが行われる。」って感じです。
適宜ChangeNotifyProviderが関連する箇所にコメント入れてあります。
実装
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:first_flutter_provider_example/counter.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ChangeNotifierProvider<Counter>( // ChangeNotifierProviderを利用して子孫ウィジェットでCounterを使えるようにする
builder: (_) => Counter(0),
child: MyHomePage(title: 'First Flutter Provider Example'),
),
);
}
}
class MyHomePage extends StatelessWidget {
final String title;
MyHomePage({Key key, @required this.title}) : super(key: key);
@override
Widget build(BuildContext context) {
var counter = Provider.of<Counter>(context); // 上位のウィジェットから提供されているCounterを取得
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'${counter.value}', // counterを表示
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
onPressed: counter.increment, // counterをインクリメント
child: Icon(Icons.add),
),
SizedBox(height: 10),
FloatingActionButton(
onPressed: counter.decrement, // counterをデクリメント
child: Icon(Icons.remove),
)
],
),
);
}
}
import 'package:flutter/material.dart';
class Counter with ChangeNotifier { // ChangeNotifierをmixin
int _value;
Counter(this._value);
int get value => this._value;
increment() {
_value++;
notifyListeners(); // 変更通知により再描画
}
decrement() {
_value--;
notifyListeners(); // 変更通知により再描画
}
}
テスト
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:first_flutter_provider_example/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
testWidgets('Counter decrements smoke test', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
expect(find.text('0'), findsOneWidget);
expect(find.text('-1'), findsNothing);
await tester.tap(find.byIcon(Icons.remove));
await tester.pump();
expect(find.text('0'), findsNothing);
expect(find.text('-1'), findsOneWidget);
});
}
終わりに
次はprovider使ったBlocをやってみようかと思います。
今回のリポジトリはこちら