本記事ではproviderの一部機能を自作してみることで、Flutterにおける「ウィジェットが子孫にデータとサービスを提供するためのメカニズム」の理解を深めてみようと思います。
- 本記事で言及するFlutter 公式サイトの内容、記述は執筆時点(2021年8月)のものとなります
はじめに
Flutterでのアプリ開発においていわゆる「アプリの状態管理」はいつもHOTな話題であり、これまで様々な手法や概念、ライブラリが現れ、シンプルなものから大規模で複雑な状況まで様々な場面に応じて使い分けられて来ています。
Flutterの公式Webサイトでも状態管理について触れられており、アプリの状態管理については「Simple app state management」という章で具体的な例と共に述べられています。
さて、その「Simple app state management」を読みはじめると冒頭から
On this page, we are going to be using the provider package.
このページでは、providerパッケージを使用します。
(Google翻訳)
とあり、また
Fortunately, Flutter has mechanisms for widgets to provide data and services to their descendants (in other words, not just their children, but any widgets below them).
幸い、Flutterには、ウィジェットが子孫(つまり、子だけでなく、その下のウィジェット)に
データとサービスを提供するためのメカニズムがあります。
(Google翻訳)
とありますが
We won’t be covering those here, because they are a bit low-level for what we’re trying to do.
Instead, we are going to use a package that works with the low-level widgets but is simple to use. It’s called provider.
これらは、私たちがやろうとしていることに対して少し低レベルであるため、ここでは取り上げません。
代わりに、低レベルのウィジェットで動作するが使いやすいパッケージを使用します。
それはproviderと呼ばれます。
(Google翻訳)
と、providerパッケージを利用する事を前提にしています。
確かに本題である状態管理の話をする上ではそこに言及すると長くなってしまうのですが、何かいいところが飛ばされてしまったというか「出来上がったものがこちらです」感というか...があります。provider自身はその「子孫にデータとサービスを提供するためのメカニズム」というのを実際にどう使ったの...?
そこで今回、この「Simple app state management」でも取り上げられている ChangeNotifierProvider
について、最小限の機能を持つ互換品を自作してみます。
- 例えば
InheritedWidget
の使用方自体は様々な形で見かけるのですが、「一式揃ったChangeNotifierProviderの互換品作り」をされている方や実装例にこれまで巡り合う事が出来ておらず、仕方ないので自分でやってみたという感じです...二番、三番煎じでしたらご容赦下さい
最小限ということで、
If you want something simpler, see what the simple Counter app looks like when built with provider.
もっとシンプルなものが必要な場合は、providerでビルドしたときのシンプルなCounterアプリが
どのように見えるかを確認してください。
(Google翻訳)
と紹介されているカウンターアプリで必要とされる範囲を実装してみます。
- ショッピングカートのサンプルで使える互換品を作るにはもう少し広い範囲の実装が必要なため、より簡単な範囲までとしました
環境
執筆時点のstable、v2.2.3を使います。
[✓] Flutter (Channel stable, 2.2.3, on macOS 11.5 20G71 darwin-x64, locale ja-JP)
コード全文と概要
まずはコード全文を掲載します!(「詳細」をクリックすると開きます)
全部で98行になりました。
import 'package:flutter/widgets.dart';
@immutable
class _Provider<T extends ChangeNotifier> extends InheritedWidget {
const _Provider({
required this.value,
required Widget child,
Key? key,
}) : super(key: key, child: child);
final T value;
@override
bool updateShouldNotify(_Provider<T> oldWidget) => true;
}
@immutable
class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget {
const ChangeNotifierProvider({
required this.create,
required this.child,
Key? key,
}) : super(key: key);
final T Function(BuildContext context) create;
final Widget child;
static T of<T extends ChangeNotifier>(
BuildContext context, {
bool listen = true,
}) {
final provider = listen
? context.dependOnInheritedWidgetOfExactType<_Provider<T>>()
: context
.getElementForInheritedWidgetOfExactType<_Provider<T>>()
?.widget as _Provider<T>?;
if (provider == null) {
throw Error();
}
return provider.value;
}
@override
_ChangeNotifierProviderState createState() =>
_ChangeNotifierProviderState<T>();
}
class _ChangeNotifierProviderState<T extends ChangeNotifier>
extends State<ChangeNotifierProvider<T>> {
late T value;
void listener() => setState(() {});
@override
void initState() {
super.initState();
value = widget.create(context);
value.addListener(listener);
}
@override
void dispose() {
value
..removeListener(listener)
..dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return _Provider<T>(value: value, child: widget.child);
}
}
@immutable
class Consumer<T extends ChangeNotifier> extends StatelessWidget {
const Consumer({
required this.builder,
this.child,
Key? key,
}) : super(key: key);
final Widget Function(BuildContext context, T value, Widget? child) builder;
final Widget? child;
@override
Widget build(BuildContext context) {
return builder(context, ChangeNotifierProvider.of<T>(context), child);
}
}
extension ReadContext on BuildContext {
T read<T extends ChangeNotifier>() {
return ChangeNotifierProvider.of<T>(this, listen: false);
}
}
カウンターアプリの実装の中で使われる ChangeNotifierProvider
の機能は
MyApp
をchild
として持てる、またcreate
でCounter
の生成を記述出来るChangeNotifierProvider
Counter
の状態を監視出来るConsumer
ウィジェットcontext.read()
によるCounter
への監視無しアクセス
の3つとなるので、これが行えるものを実装しました。
カウンターアプリにおいて、このコードを適当な名前で保存し 'package:provider/provider.dart'
の代わりに import することで、本家 provider
と置き換えて使えるかと思います。
実装に際してはChangeNotifierProviderが初登場したバージョン2の頃のproviderのコードを土台とし、汎用的に作られている部分の簡略化やnull-safety対応などを施したものとなっています。
解説
では、以下におおまかな実装内容を順に解説していきます。
なお「本家providerのあの機能をどう実装したか」の解説となり、もともとの ChangeNotifierProvider
の機能や役割、動作等については既に把握されている前提であることをご了承ください。
InheritedWidget
@immutable
class _Provider<T extends ChangeNotifier> extends InheritedWidget {
const _Provider({
required this.value,
required Widget child,
Key? key,
}) : super(key: key, child: child);
final T value;
@override
bool updateShouldNotify(_Provider<T> oldWidget) => true;
}
ChangeNotifier
を継承したクラスのインスタンス、すなわちChangeNotifierを利用した状態管理オブジェクトを value
として持つ InheritedWidget
です。
InheritedWidget
を使う事で、dependOnInheritedWidgetOfExactType()
などを使い末端のウィジェットからこのウィジェット、さらにはこのウィジェットが持つ value
へ低コストでたどり着くことが出来ます。
updateShouldNotify
については、リビルド通知について直前と比べて細かく制御する必要がない使い方をするため、常に true
としています。
ChangeNotifierProvider
@immutable
class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget {
const ChangeNotifierProvider({
required this.create,
required this.child,
Key? key,
}) : super(key: key);
final T Function(BuildContext context) create;
final Widget child;
ChangeNotifierProvider
ウィジェットの役割の1つに ChangeNotifier
を継承した状態管理オブジェクトの保持、がありますが、Flutterにおいてウィジェットツリー内で状態を保持するには StatefulWidget
の利用が基本なので、そのように実装しました。
また StatefulWidget
は State
と組になりますが、後ほど State
の機能である「自身の状態の更新によるリビルド」を活用して「状態管理オブジェクトの更新によるリビルド」を実装します。
- 実はFlutterは状態保持に必ず StatefulWidget を使わなければいけないわけでもなく、例えば現行(v5.0.0等)の本家providerでは状態管理オブジェクトの保持に
StatefulWidget
を使っていなかったりします。興味のある方は調べてみても面白いかと思います。
「ChangeNotifier
を継承した状態管理オブジェクト」を生成してもらう関数 create
をコンストラクタの引数に持っています。
static T of<T extends ChangeNotifier>(
BuildContext context, {
bool listen = true,
}) {
final provider = listen
? context.dependOnInheritedWidgetOfExactType<_Provider<T>>()
: context
.getElementForInheritedWidgetOfExactType<_Provider<T>>()
?.widget as _Provider<T>?;
if (provider == null) {
throw Error();
}
return provider.value;
}
子孫ウィジェットで、いわゆる ChangeNotifierProvider<T>.of(context)
を行えるようにします。
ChangeNotifierProvider
クラス自身は StatefulWidget
なので dependOnInheritedWidgetOfExactType()
や dependOnInheritedWidgetOfExactType()
で指し示す事は出来ませんが、この後 ChangeNotifierProvider
は InheritedWidget
である _Provider
を使うように実装する予定であり、その _Provider
が持っている value
を返す、としています。
_ChangeNotifierProviderState
class _ChangeNotifierProviderState<T extends ChangeNotifier>
extends State<ChangeNotifierProvider<T>> {
late T value;
void listener() => setState(() {});
@override
void initState() {
super.initState();
value = widget.create(context);
value.addListener(listener);
}
@override
void dispose() {
value
..removeListener(listener)
..dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return _Provider<T>(value: value, child: widget.child);
}
}
ChangeNotifierProvider
と組になっている State
です。
「ChangeNotifier
を継承した状態管理オブジェクト」である value
を持ち、以下の実装によって状態管理オブジェクトの生成、保持、破棄と、状態変化時の反応(リビルド)を実現します。
initState
時にcreate()
を呼び出し状態管理オブジェクトを返してもらってvalue
を初期化し保持- 状態管理オブジェクト
value
に自身のlistener
関数を登録 dispose
時にvalue
へのlistener
関数登録を削除、またvalue
のdispose()
呼び出し
value
に登録する listener
関数の内容は setState()
の実行です。最小限の実装という事で無条件に setState()
を空打ちしています。
State
の 「setState()
を呼ぶとリビルドがかかる」と相まって、状態管理オブジェクトで notifyListeners()
を呼び出すとリビルドがかかる、という動作を実現します。
@override
Widget build(BuildContext context) {
return _Provider<T>(value: value, child: widget.child);
}
自身の保持する value
を設定した _Provider
で child を wrap したため ChangeNotifierProvider
の子孫ウィジェットは先祖に _Provider
を持つ事になり、結果として先だって実装した ChangeNotifierProvider<T>.of(context)
が動作するようになります。
Consumer
@immutable
class Consumer<T extends ChangeNotifier> extends StatelessWidget {
const Consumer({
required this.builder,
this.child,
Key? key,
}) : super(key: key);
final Widget Function(BuildContext context, T value, Widget? child) builder;
final Widget? child;
@override
Widget build(BuildContext context) {
return builder(context, ChangeNotifierProvider.of<T>(context), child);
}
}
ChangeNotifierProvider
が持つ状態管理オブジェクト、正確には ChangeNotifierProvider
が使う _Provider
が持っている状態管理オブジェクト、を of()
で参照し、取得したオブジェクトを引数にしつつ builder()
を呼びます。
listen: false
を指定していないので、
of()
の中でdependOnInheritedWidgetOfExactType()
が使われる- すなわち
InheritedWidget
である_Provider
が要リビルド、となった場合Consumer
自身にリビルドがかかり、再度builder()
が呼ばれる
となり、結果「状態変化に合わせてリビルド」が実現出来ます。
context.read()
extension ReadContext on BuildContext {
T read<T extends ChangeNotifier>() {
return ChangeNotifierProvider.of<T>(this, listen: false);
}
}
ChangeNotifierProvider.of()
を listen: false
で呼ぶ、という extension です。
おわりに
以上、ChangeNotifierProvider
を自作してみました。
InheritedWidget
等、Flutterから提供される機能、ウィジェットの組み合わせで ChangeNotifierProvider
を作る事が出来ました。
実際のアプリ開発では今回のようにイチから実装せずライブラリを有効に活用すべきかと思いますし、他の状態管理手法はまた違う設計や構成を持っていたりしますが、代表的な例である provider
での ChangeNotifierProvider
について基本的な仕組みや考え方やをより深く理解することで provider
の適切な利用に役立ったり、さらには他の状態管理手法との使い分けや比較の助けになるかな、と思います。