📞

【Flutter】色んな状態管理で作ってみよう ①Provider編

2022/02/11に公開

※こちらの記事は【Flutter】色んな状態管理手法でカウンターアプリを作ってみるの一部として作成された記事です

今最も普及している状態管理手法? Provider

一発目はGoogleが公式に推している事もあり、最も普及している(であろう)状態管理手法のProviderでカウンターアプリを作っていきます

使用するPackage:

概要

  • 現在最もポピュラーな状態管理用パッケージ
  • Googleも状態管理パッケージの中でもオススメしているパッケージ
  • providerだけでなく、stateNotifierやRiverpod、freezedなど数多くの人気FlutterパッケージをリリースしているRemi Rousselet氏が開発
  • 初版リリースは2018年10月

全体像

特徴は2点、

  1. 依存関係がWidgetツリーに沿って下っていく
  2. 状態変更を明示的に通知する

状態を保持したクラスは依存関係を注入されたWidgetとそのWidgetツリー傘下のWidgetからアクセスが可能になります。Widgetツリーに沿って依存関係が下っていくようなイメージ。

Widgetは状態管理クラスの変数やメソッドをProviderを通してアクセスします。変数を変更した際は用意されている通知メソッドを実行し、変数を利用しているクラスに通知。その通知を受けったクラス達は新しい値で自身を再生成(リビルド)する事になります。

キーとなるクラスやメソッド

  • ChangeNotifierクラス:状態管理クラスが継承するクラス
  • ChangeNotifierProviderクラス:状態管理クラスを注入するクラス
  • notifyListenerメソッド:状態変更を通知するメソッド

準備

具体的にカウンターアプリを例に見ていきましょう。
サンプルコードはこちら

今回はcountフィールドを持つCounterObjクラスの状態管理をしていきます。

class CounterObj {
  CounterObj() : count = 0;
  int count;
}

状態を保持するクラスProviderCounterStateを準備。

  • ChangeNotifierクラスを継承する事で前述の通知メソッドnotifyListenerを使う事が出来ます。
  • クラスに定義したメソッドで保持している状態の値に変更を加え、notifiyListenerで変更を外部に通知します。

class ProviderCounterState extends ChangeNotifier {
  ProviderCounterState() : obj = CounterObj();
  CounterObj obj;

  void incrementCounter() {
    obj.count++;
    notifyListeners();
  }

  void decrementCounter() {
    obj.count--;
    notifyListeners();
  }

  void resetCounter() {
    obj.count = 0;
    notifyListeners();
  }
}

ChangeNotifierProviderを使って状態管理クラスをUIに注入。

  • createフィールドで状態管理クラスProviderCounterStateをインスタンス化
  • childフィールドに定義した_ProvoiderCounterPage widgetにインスタンスを注入

class ProviderCounterPage extends StatelessWidget {
  const ProviderCounterPage({Key key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => ProviderCounterState(),
      child: _ProviderCounterPage(),
    );
  }
}

これによりwidgetツリー上で_ProviderCounterPage widgetより下に位置する全てのWidgetでProviderCounterStateクラスにアクセスできる様になりました。

状態へのアクセス

状態管理クラスへのアクセス方法には二つあります。

単発的に状態管理クラスにアクセスするcontext.read()と状態変化を監視するcontext.watch()です。

メソッドなど定義が変更されないものはcontext.read()でアクセスし、状態値など変更を検知したい対象についてはcontext.watch()でアクセスします。

メソッドへのアクセス:

final ProviderCounterState unListenState = context.read<ProviderCounterState>(); 


FloatingActionButton(
  onPressed: unListenState.incrementCounter,
  tooltip: 'Increment',
  heroTag: 'Increment',
  child: Icon(Icons.add),
),

状態値の監視:

  • context.watch()では前述のnotifyListenersからの通知に応じて、値を取得し直します。
  • 取得し直した値を元にその値を扱うWidgetを再描画(リビルド)します。
  • その為、buildメソッド内でcontext.watch()を定義してしまうと状態が変化する度にwidgetが丸ごと再描画されてしまいます。
 
  Widget build(BuildContext context) { 
    // ProviderCounterStateの値が変わる度にbuildメソッドが走る
    final ProviderCounterState state =
        context.watch<ProviderCounterState>(); 
    return Scaffold(
      appBar: MainAppBar(
        title: 'ChangeNotifier x Provider',
        ...

  • notifiListenersメソッドの通知に応じてラップしているwidgetだけを再描画するConsumerクラスを使用する事で再描画される範囲を限定する事が出来ます。
...,
Consumer<ProviderCounterState>(
  builder: (context, state, _) => Text(
    '${state.obj.count}',
    style: Theme.of(context).textTheme.headline4,
  ),
),
...
  • 実際に状態値を参照して描画しているText widgetだけをConsumerクラスでラップする事で状態値が変更しても再描画されるのはラップされたText Widgetだけになりました
  • Consumerクラスの他に特定の状態値だけを監視し、Selectorクラスを使う事で再描画される条件をより絞る事が出来ます。

全体

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:state_management_examples/widgets/main_appbar.dart';

// 状態クラス
class CounterObj {
  CounterObj() : count = 0;
  int count;
}

// 状態管理クラス
class ProviderCounterState extends ChangeNotifier {
  ProviderCounterState() : obj = CounterObj();
  CounterObj obj;

  void incrementCounter() {
    obj.count++;
    notifyListeners();
  }

  void decrementCounter() {
    obj.count--;
    notifyListeners();
  }

  void resetCounter() {
    obj.count = 0;
    notifyListeners();
  }
}

// 依存関係を注入
class ProviderCounterPage extends StatelessWidget {
  const ProviderCounterPage({Key key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => ProviderCounterState(),
      child: _ProviderCounterPage(),
    );
  }
}

// カウンター本体
class _ProviderCounterPage extends StatelessWidget {
  const _ProviderCounterPage({Key key}) : super(key: key);

  
  Widget build(BuildContext context) {
    print('rebuild!');
    final ProviderCounterState unListenState =
        context.read<ProviderCounterState>();
    return Scaffold(
      appBar: MainAppBar(
        title: 'ChangeNotifier x Provider',
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Consumer<ProviderCounterState>(
              builder: (context, state, _) => Text(
                '${state.obj.count}',
                style: Theme.of(context).textTheme.headline4,
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FittedBox(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: <Widget>[
            FloatingActionButton(
              onPressed: unListenState.incrementCounter,
              tooltip: 'Increment',
              heroTag: 'Increment',
              child: Icon(Icons.add),
            ),
            const SizedBox(width: 16),
            FloatingActionButton(
              onPressed: unListenState.decrementCounter,
              tooltip: 'Decrement',
              heroTag: 'Decrement',
              child: Icon(Icons.remove),
            ),
            const SizedBox(width: 16),
            FloatingActionButton.extended(
              onPressed: unListenState.resetCounter,
              tooltip: 'Clear',
              heroTag: 'Clear',
              label: Text('CLEAR'),
            ),
          ],
        ),
      ),
    );
  }
}

実は...

Providerは状態管理手法の事じゃない

最もポピュラーな状態管理手法としてProviderが紹介される事が多いが、実際Providerパッケージから利用してるのは状態管理クラスをWidgteツリーに沿って流し込むChangeNotifierProviderであって、状態管理クラスのChangeNotifier自体ではありません。

そういう意味で言うとProviderは依存関係の注入を手助けするパッケージであって、状態管理手法自体ではありません。

どうもGoogleIOにてProviderパッケージを状態管理手法として紹介した事が元凶のようです。

Providerを使うのはProviderだけじゃない

禅問答の様ですが、先の捉え方がなぜややこしいかというとProviderが様々な状態管理手法で使われているからです。状態管理手法として有名なBLoCでもBlocProviderクラスによって依存関係をWidgetツリーに沿って流し込んでいきます。

Providerと比較されるべきなのはRiverpodやGetItでは?

BlocやRedux、GetXなど他の状態管理手法とProviderを同列で比較するような言い方が多いかと思いますが、「Provider = 依存関係の注入を手助けするパッケージ」という視点から考えれば、Providerパッケージと比較されるべきなのは、 厳密にはRiverpodやServiceLocatorであるGetItだと思います。

Providerパターン、Providerパッケージ

そんな訳でProviderという言葉を使うとこんがらがるのですが、状態管理手法としてのProviderと言う時は

Provider(パターン) = ChangeNotifier x ChangeNotifierProvider の組み合わせ」

と言うニュアンスで考えるのが良さそうです。ちょっと「Providerパターン」なんて名前があるかは定かじゃありませんが。

以上でした

Providerについては記事も溢れていて、特に目新しい事もないと思いますが、個人的には他の状態管理手法と比べていく中で見えた事が一番為になりました。言葉の定義や分類って難しいですねー

参考

Discussion