InheritedWidget をより便利に扱えるように改良されたライブラリである、Provider の解説を行なっています。
状態管理ライブラリとして学習したいと考えている方の参考になれば幸いです。
InheritedWidget をより便利に扱えるように改良されたライブラリである、Provider の解説を行なっています。
状態管理ライブラリとして学習したいと考えている方の参考になれば幸いです。
今日も暑いですね。
こんにちは、さとしです。
前回は、Widget についての簡単な説明と、Flutter 関係の記事で取り上げられることの多い StatefulWidget、 StatelessWidget、 InheritedWidget の解説をしていきました。
StatefulWidget は、自身を再描画させるための State を持つ Widget, State を持たない StatelessWidget、Elementツリーの下位にある Widget から、参照したい状態を持つ上位の Widget へ直接アクセスすることが出来る InheritedWidget の3つの Widget について解説をしました。
今回は、Flutter の状態管理ライブラリの一つである Provider について、前回利用したカウンターアプリを元に解説していきます。
ソースコードは下記のリポジトリを参照いただければと思います。
https://github.com/EMoshU/Provider-counter-blog/blob/main/lib/main.dart
なお、本記事執筆にあたり利用した Provider ライブラリのバージョンは 6.1.2になります。
Provider は、InheritedWidget をより使いやすく、便利にしたライブラリです。
例えば、状態の更新を伝播させたいといった時、InheritedWidget では自身をリビルドするための仕組みがないため、自身を再描画することのできる setState メソッドを持つ StatefulWidget の実装が必要でしたが、Provider を利用することでこれをなくすことができます。
また、InheritedWidget には下位のツリーに対して再描画を抑制する手段がないため、下位の Widget で状態が利用される、されないに関わらず再描画が伝播してしまいます。
そこで、Provider では Consumer、 Selector という2つの便利な Widget があり、これらを使うと描画する対象を制御できます。
今回のブログでは取り上げませんが、詳しい情報を知りたい方は以下のリンクなどを参考にしてください
Provider には複数の種類があり、目的に応じて使い分ける必要があります。
ChangeNotifierProvider は、ChangeNotifier を生成するための Provider です。
生成された ChangeNotifier は、自身が持つデータに更新が加わった際、データを参照している Widget をリビルドして再描画を行います。
ChangeNotifierProxyProvider は、ChangeNotifier が保持している状態の更新を監視し、その状態に応じて自身をリビルドさせる Provider です。
StreamProvider は、連続した値を受け取り再描画を行う Provider です。
FutureProvider は、非同期で行われている他の処理を待って再描画を行うことができる Provider です。
async・await の形式で記述し、await の後ろに再描画のトリガーとなる処理を定義することで、その処理の完了を検知して再描画を行います。
例えば以下のコードは、初期値が空、10秒後に "Finish" の String 型の返り値を返す FutureProvider になります。
void main() {
runApp(
FutureProvider(
create: (context) async {
await Future.delayed(const Duration(seconds: 10));
return "Finish";
},
initialData: null,
child: const MyApp(),
),
);
}
MultiProvider は、Provider を一つのリスト形式で保持できる Provider です。
MultiProvider を利用せずに複数の Provider を利用する場合は、Provider の子に Provider をネストさせる必要があり、見通しが悪くなることを避けることができます。
Provider は、InheritedWidget をより使いやすくしたライブラリであり、内部では InheritedWidget が自身にアクセスした Widget に再描画を掛ける仕組みが働いています。
今回もシーケンス図の形で説明していきます。
まずは、初期表示時に Provider の更新の監視、データ取得、Provider が更新された際に、更新されたデータを取得して自身を再描画するようにする処理を追っていきます。
前回同様に、カウンターアプリを用いて解説を行なっていきます。
このカウンターアプリのツリー構造は以下のようになっています。
図1.サンプルアプリのツリー構造
ツリー上には出ていませんが、MyApp 直下にある ChangeNotifierProvider が、内部で子 Widget として _InheritedProviderScope という InheritedWidget を継承した Widget を保持しており、その対になる Element である _InheritedProviderScopeElement という InheritedElement を生成します。
また、ChangeNotifierProvider がビルドされたタイミングで、CounterState という状態を管理するためのクラスを生成します。
CounterState クラスは ChangeNotifier クラスを継承しており、notifyListeners メソッドを呼び出すことができます。
このメソッドは、CounterState クラスに属する状態が更新された時に、その状態を参照している Element に変更を通知する役割を持っています。
ChangeNotifierProvider を利用して状態管理を行う際には、この CounterState に 、保持したい状態や、状態を更新するためのメソッドを記述していきます。
今回も、FloatingActionButton のクリックイベントでカウンターの値を1増やし、更新されたデータを読み取って Text(カウンター) が再描画される仕組みとなっています。
まずは図2 のシーケンス図を見ながら、 WidgetがProvider.of メソッドを呼び出し、CounterState の更新を監視しつつCounterState を取得するまでの流れを追っていきます。
Provider.of メソッドには、listen という bool 値を引数として渡すことができ、デフォルト値は true となっています。listen の引数は、Provider の更新に依存するかどうかでセットする値が異なります。
この後説明しますが、 true をセットした場合は dependOnInheritedWidgetExactType メソッドを通じて 呼び出し元の Element が Provider の状態を参照するようになり、状態が更新された場合はリビルドの対象になります。
false の場合はこのメソッドは呼ばれないため、Provider の状態の更新によってリビルドされることはありません。
先ほど Provider の便利な箇所を説明した際に出てきた Consumer、 Selector は、Widget 内部で Provider.of メソッドを呼び出しています。
Consumer 、Selector をツリー上位で利用してしまうと、下位の Widget は状態を利用する、しないに関わらず再描画の対象とされてしまうため、なるべく下位のツリーで利用するようにしましょう。
Provider.of メソッドの中で _InheritedElementOf メソッドが呼ばれます。このメソッドは、最終的に値を取り出すための_InheritedProviderScopeElement を返します。
_InheritedElementOf メソッド内部で 1-1 getElementForInheritedWidgetOfExactType メソッド が呼ばれます。
このメソッドは、自身に一番近い _InheritedProviderScopeElement への参照を取得します。
また、取得した _InheritedProviderScopeElement からデータを取得します。
_InheritedProviderScopeElement は、_DelegateState からvalueを取りに行きます。
_DelegateStateは、 実際に値を生成、保持したり、値が更新した際のリスナメソッドを登録するクラスです。
Provider.of メソッドを呼び出す際に渡す引数として true を渡している、もしくは bool 値を渡していない場合は、1-2 dependOnInheritedWidgetOfExactType メソッドが呼ばれることにより、引数として渡された BuildContext(Element) は、InheritedElement(Provider) の状態が更新された時にリビルドする対象として追加されます。
Provider の内部処理では、InheritedElement からデータを取得する時に、1-11 _startListening というメソッドを呼び出します。
このメソッド内で ChangeNotifierProvider にリスナメソッドを追加します。ここでは、Provider のリビルドを行う markNeedsNotifyDependants メソッドを追加しています。
この処理と、後に紹介する notifyListeners メソッドと組み合わせることで、Provider のデータに変更があった際、自身の再描画を行うことを可能としています。
図2.MyHomePage が _InheritedProviderScopeElement の持つデータを取得するまでの流れ
次に、図3 を基に、ボタン押下後の処理を解説していきます。
FloatingActionButton を押下して数値が 1 増える処理は、まずCounterStateが保持している数値を読み取り、そこに2-1 incrementCounterメソッドによって数値を増やし、リビルドを通知することでCounterStateの更新を監視しているBuildContextに対してリビルドをかける仕組みになっています。
ます、FloatingActionButton の onPress イベントで Provider.of メソッドが呼ばれます。
今回は、増加させる元となる値を読み取るだけで良いので、listen を false に設定しています。
まず、Provider.of メソッドで現在の値(ボタンを押す前の Counter の数値)を取得します。
取得フローは、先ほどの Provider.of メソッドと同様ですが、listen = false が設定されているため、Stateの更新に依存するためのメソッドは呼ばれません。
次に、Provider.of メソッドで取得した State に対して increment メソッドを呼び出しています。
このメソッドは CounterState に定義されており、、カウンターの数値を +1した後に notifyListener を呼び出しています。
2-2 notifyListener メソッドの中では、リスナーとしてセットしたメソッドを呼び出す処理が行われており、先ほど追加した 2-4 markNeedsNotifyDependants メソッドが呼び出されます。
markNeedsNotifyDependants メソッドが呼ばれると、2-5 markNeedsBuild が呼び出されます。
このメソッドは、呼び出したElementをリビルドの対象とするメソッドでした。
そして、2-6 scheduledForRebuild によってリビルドのスケジューリングが行われた Provider は、BuildOwnerによってリビルドされていきます。
図3. _InheritedProviderScopeElement がリビルド対象になるまでのシーケンス図
最後に、図4 を基に、_InheritedProviderScopeがリビルドされてからMyHomePageがリビルドされるまでの処理を追いかけます。
リビルドされた Provider は、3-1 rebuild、 3-2 performRebuild を経て 3-3 build でリビルドされます。
そして build メソッド内で 3-4 notifyClients メソッドが呼ばれます。
ここでは for文を利用して、 _InheritedProviderScopeElement に依存している Element に対して3-5 notifyDependent メソッドを呼んでいます。
notifyDependant では、リビルドされた Provider に依存している Element に対し、それがリビルドする対象かどうかを 3-6 updateShouldNotify メソッドで判定し、対象である Element に対して 3-7 didChangeDependencies を呼び出すメソッドです。
didChangeDependencies メソッドでは、 Provider の更新を監視している MyHomePage で 3-8 markNeedsBuild が呼び出され、リビルドが行われていきます。
ここからは先はフレームワーク側の処理となり、前回解説した通りの処理になります。
図4. _InheritedProviderScopeElement がリビルドされてから、MyHomePage がリビルド対象となるまでのシーケンス図
Provider4.1 で Provider.dart に WatchContext と ReadContext という BuildContext を拡張した Extension が追加され、watch と read の2つのメソッドが利用できるようになりました。
watch が State を監視して、更新されたらリビルド、read はリビルドなしで初期表示時のデータ読み取りのみです。
現在は Provider.of の代わりにこれらのメソッドを呼び出すことが主流となっていますが、内部では Provider.of が呼び出されており、第2引数に true を渡すか false を渡すかの違いとなっています。
今回は、Flutter の状態管理に用いられるライブラリである Provider について解説していきました。
Providerを利用することによって、InheritedWidget ではできなかったワンストップでの状態管理、状態更新が可能となりました。
前回のコードである InheritedWidget のサンプルアプリでは、InheritedWidget 単体で状態の更新を行うことができなかったので、状態が更新された時にリビルド、再描画を行うために StatefulWidget を利用する必要がありました。
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MyCounter(
child: MaterialApp(
home: MyHomePage(),
),
);
}
}
class MyCounter extends StatefulWidget {
const MyCounter({
super.key,
required this.child,
});
final Widget child;
static MyCounterState of(BuildContext context, {bool rebuild = true}) {
return rebuild
? context.dependOnInheritedWidgetOfExactType<_InheritedCounter>()!.data
: (context
.getElementForInheritedWidgetOfExactType<_InheritedCounter>()!
.widget as _InheritedCounter)
.data;
}
@override
State createState() => MyCounterState();
}
class MyCounterState extends State {
int count = 0;
void increment() => setState(() {
count++;
});
@override
Widget build(BuildContext context) {
return _InheritedCounter(
data: this,
child: widget.child,
);
}
}
class _InheritedCounter extends InheritedWidget {
const _InheritedCounter({
required this.data,
required super.child,
});
final MyCounterState data;
@override
bool updateShouldNotify(_InheritedCounter oldWidget) => true;
}
今回のコードでは、 Provider が生成した ChangeNotifier である CounterState が状態の保持、更新まで行なっていることがわかります。
Widgetを余分に利用しないことで、コード量も減っています。
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => CounterState(),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
));
}
}
class CounterState extends ChangeNotifier {
int counter = 0;
void incrementCounter() {
counter++;
notifyListeners();
}
}
また、Consumer や Selector といった Widget を利用することで、無駄な再描画を省き、アプリのパフォーマンス向上に貢献します。
現在の Flutter 開発では、Provider ライブラリを改良した Riverpod というライブラリを用いることが主流になっています。
次回は、Riverpod ライブラリの解説をしながら、Provider ライブラリの何が改良されたのかを説明していきたいと思います。
EMoshU アプリチームでは、自社アプリで Flutter を採用すべく Flutter エンジニアを募集しております。
・今の会社・環境では、現場が変わると人間関係がリセットされてしまい、新しく人間関係を築き続けるのに疲れる。寂しい
・一緒にワイワイ開発出来る仲間が欲しい!
・チームを強くする経験を積みたい!
・自社プロダクトでガンガン Flutter を使っていきたい方
という方、ぜひ募集要項をご覧ください。カジュアル面談なども実施しております。
Flutter を使ったアプリ開発で世の中に貢献したい方、ぜひご応募ください!!
募集要項