Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails以外の開発一般

Flutter: 最小限のChangeNotifierProviderを自作してみる

本記事では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 の機能は

  • MyAppchild として持てる、また createCounter の生成を記述出来る 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 の利用が基本なので、そのように実装しました。
また StatefulWidgetState と組になりますが、後ほど State の機能である「自身の状態の更新によるリビルド」を活用して「状態管理オブジェクトの更新によるリビルド」を実装します。

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() で指し示す事は出来ませんが、この後 ChangeNotifierProviderInheritedWidget である _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 関数登録を削除、また valuedispose() 呼び出し

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 の適切な利用に役立ったり、さらには他の状態管理手法との使い分けや比較の助けになるかな、と思います。



CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。