この記事はFlutter 全部俺 Advent Calendar 7日目の記事です。
このアドベントカレンダーについて
このアドベントカレンダーは @itome が全て書いています。
基本的にFlutterの公式ドキュメントとソースコードを参照しながら書いていきます。誤植や編集依頼はTwitterにお願いします。
Providerパッケージ
6日目の記事で
紹介した InheritedWidget
のラッパーライブラリです。
今回のアドベントカレンダーで唯一標準パッケージでないパッケージの紹介ですが、状態管理とDIに関してデファクトスタンダードと言えるライブラリなのでピックアップしました。
インストール方法
pubspec.yaml
に以下を追加して、 $ flutter pub get
dependencies:
provider: ^3.1.0+1
Provider.value<T>()
Provider.of<T>()
Providerパッケージの最も基本的でわかりやすい使い方は、 Provider.value<T>()
と Provider.of<T>()
の組み合わせです。
Provider<String>.value(
value: 'Hello World',
child: MaterialApp(
home: Home(),
)
)
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text(
/// Don't forget to pass the type of the object you want to obtain to `Provider.of`!
Provider.of<String>(context)
);
}
}
Widget
ツリーの中に Provider.value()
を挟んでおくことで、それ以下の Widget
から値を取得できるようになります。
図にすると以下のようなイメージです。
Provider.of(context)
で値の変更を監視
Provider
の中には後述する StreamProvider
のように提供する値が動的に変化するものがあります。
Provider.of(context)
はそのような動的な値の変化を自動的に監視して、 Widget
ツリーをリビルドしてくれます。
リビルドは、Provider.of(context)
に渡した BuildContext
のスコープで行ってくれるので、最低限しか行われません。
値を監視したくないときには Provider.of(context, listen: false)
とします。
また、 Provider.of(context)
は内部では InheritedWidget
の inheritFromWidgetOfExactType
メソッドを使っているため、
initState
より後でしか呼ぶことができません。一方 Provider.of(context, listen: false)
は
InheritedWidget
の ancestorInheritedElementForWidgetOfExactType
を使っているので initState
でも使うことができます。
MultiProvider
Provider
を複数使いたいときは以下のようになります。
Provider<Foo>.value(
value: foo,
child: Provider<Bar>.value(
value: bar,
child: Provider<Baz>.value(
value: baz,
child: someWidget,
)
)
)
これは明らかに冗長で無駄なネストが入ってしまうので、複数の Provider
に対応した MutliProvider
が提供されています。
MultiProvider(
providers: [
Provider<Foo>.value(value: foo),
Provider<Bar>.value(value: bar),
Provider<Baz>.value(value: baz),
],
child: someWidget,
)
StreamProvider
StreamBuilder.controller()
Stream
に流れる値によって動的に値が変わる Provider
です。
StreamProvider<int>(
builder: (context) => countStream,
initialData: 0,
child: ...
)
StreamProvider.controller()
を使うことで、 StreamController
を使うことができます。
StreamProvider<int>(
builder: (context) => countStreamController,
initialData: 0,
child: ...
)
Stream
に明示的に値を流したい時はこちらを使います。
FutureProvider
指定した Future
のタスクが終了すると、値を動的に変更する Provider
です。下のサンプルコードは Provider.of<String>(context)
が
1秒後に ''
から 'Hello World'
へ変更されます。
FutureProvider<String>(
builder: (context) async {
await Future.delayed(const Duration(seconds: 1));
return 'Hello World';
},
initialData: '',
child: ...
)
FutureProvider
指定した Future
のタスクが終了すると、値を動的に変更する Provider
です。下のサンプルコードは Provider.of<String>(context)
が
1秒後に ''
から 'Hello World'
へ変更されます。
FutureProvider<String>(
builder: (context) async {
await Future.delayed(const Duration(seconds: 1));
return 'Hello World';
},
initialData: '',
child: ...
)
ListenableProvider
ChangeNotifierProvider
ValueListenableProvider
ChangeNotifier
系のクラスの変更を監視できる Provider
です。
class CountNotifier extends ValueNotifier<int> {
CountNotifier() : super(0);
void increment() => value++;
}
ValueNotifierProvider<int>(
builder: (_) => CountNotifier(),
child: ...
)
ProxyProvider
祖先にある Provider
から提供されている値を変換した値をさらに提供する Provider
です。図にすると以下のような感じです。
実際の開発での使い方
前述した通りProvider
パッケージの使い方は大きく分けて、 DI と 状態管理 です。
これらの機能は別々に使うこともできますが、多くの場合組み合わせて使われます。つまり、「状態管理をするクラスをDIする」という形で使われることが多いです。
パッケージのドキュメントの冒頭にも以下のように書いてあります。
A mixture between dependency injection (DI) and state management, built with widgets for widgets.
(このパッケージは) DIと状態管理の混ぜ合わせで、WidgetのためのWidgetによって作られています。
また、 Provider
は通常の StatefulWidget
を使った状態管理に比べてリビルドの範囲を抑えられるため、アプリのパフォーマンス改善にも使うことができます。
Provider
を使ったパフォーマンス改善は8日目の記事で紹介します。
今回はFlutterの scoped_model
を Provider
パッケージを使って実装してみましょう。作るアプリはFlutterのボイラープレートと全く同じです。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() => runApp(MyApp());
class CounterModel extends ChangeNotifier {
int count = 0;
void increment() {
count++;
notifyListeners();
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: ChangeNotifierProvider<CounterModel>(
builder: (_) => CounterModel(),
child: Scaffold(
appBar: AppBar(title: Text('Flutter Demo Home Page')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
CounterText(),
],
),
),
floatingActionButton: Consumer<CounterModel>(
builder: (_, model, __) {
return FloatingActionButton(
onPressed: model.increment,
tooltip: 'Increment',
child: Icon(Icons.add),
);
}
), // This trailing comma makes auto-formatting nicer for build methods.
),
),
);
}
}
class CounterText extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text(
Provider.of<CounterModel>(context).count.toString(),
style: Theme.of(context).textTheme.display1,
);
}
}
状態管理するクラスは CounterModel
です。count
をフィールドに持ち、
increment
メソッドが呼ばれると count
を1上げて変更を外部に通知します。
class CounterModel extends ChangeNotifier {
int count = 0;
void increment() {
count++;
notifyListeners();
}
}
次に ChangeNotifierProvider
を使って CounterModel
を子孫 Widget
から取得できるようにします。
...
home: ChangeNotifierProvider<CounterModel>(
builder: (_) => CounterModel(),
child: Scaffold(
...
CounterText
を定義します。 Provider.of
で取得した CounterModel
の count
を表示しています。
Provider.of
は祖先の Provider
で定義した値の変更を監視しています。今回の場合は CounterModel
の
notifyListeners
が呼ばれたときに変更された値を再描画します。
class CounterText extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text(
Provider.of<CounterModel>(context).count.toString(),
style: Theme.of(context).textTheme.display1,
);
}
}
最後に FloatingActionButton
のクリックイベントで CounterModel
の increment
メソッドを呼びだします。
しかし、ここで取得できる context
は MyApp
の build
メソッドに渡されたもので、 ChangeNotifierProvider
の祖先のものなので、
Provider.of<CounterModel>(context)
では CounterModel
を取得できません。
そのようなシチュエーションのために、 Provider
パッケージには Cousumer
Widgetが用意されています。
この Widget
を使うと、その位置で BuildContext
を再発行してくれるので、正しく CounterModel
を取得することができます。
...
floatingActionButton: Consumer<CounterModel>(
builder: (_, model, __) {
return FloatingActionButton(
onPressed: model.increment,
tooltip: 'Increment',
child: Icon(Icons.add),
);
}
), // This trailing comma makes auto-formatting nicer for build methods.
...
これで、カウンターアプリの完成です。 Provider
パッケージを正しく使うと、状態管理がわかりやすく、かつパフォーマンスにもすぐれたアプリを作ることができます。
6日目: FlutterのBuildContextとInheritedWidgetを理解する :
https://itome.team/blog/2019/12/flutter-advent-calendar-day6
8日目: Flutterのパフォーマンスを改善する : https://itome.team/blog/2019/12/flutter-advent-calendar-day8