LoginSignup
22
17

More than 3 years have passed since last update.

4つのFlutterの状態管理を比較してみる。Statefull Widget, ChangeNotifier, StateNotifier, Riverpod まで

Posted at

現在Flutter界隈で見られる状態管理は

  • Statefull Widget
  • ChangeNotifier + Provider
  • StateNotifier + Provider ( + freezed)
  • StateNotifier + Riverpod

下に行くほど最新のものになっています。
BLoCパターンやReduxは分からないので扱いません。公式でもProviderが推奨されているみたいです。

この記事では、カウントアプリを4つの状態管理方法で作成し、比較してみようと思います。

見た目はこんな感じ。デフォルトで表示される物をいじっていきます。
Screen Shot 2020-08-08 at 18.19.34.png

Statefull Widget

簡単に、Statefull Widgetが状態を保つ仕組みを説明します。
StatelessWidget, StatefullWidget クラスはbuildのたびに作られますが、Stateクラスは一度作られたらそのまま保持されます。StatefullWidgetは紐付けたStateを参照して一貫性を保ちます。

setStateで状態を変更すると、UIに変更が通知され、該当の部分だけ再描写されるようになっています。

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text(
          _counter.toString(),
          style: Theme.of(context).textTheme.headline4,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

StatelessWidgetは不変なので変更があるたびWidgetを表示し直す、StatefullWidgetは変更部分だけがうまく再描写されるようになっているので効率的です。

しかし、アプリケーション全体の状態を管理するには向いてません。ロジックがUIの中で書かれてしまい、UIとの分離ができないからです。UIの状態のみ(タップされた、スクロールしている、など)を管理する場合には良いでしょう。

ChangeNotifier + Provider

ChangeNotifierは状態を保持し、変更を通知するクラスです。
Widgetクラスの中で、Providerを作ります。その中でChangeNotifierをインスタンス化します。Providerで囲われている範囲でUIはChangeNotifierインスタンスにアクセスすることができます。StatelessWidgetの中で使えます。

import 'package:provider/provider.dart';

...
// このクラスが状態管理、変更を担う
class CounterNotifer extends ChangeNotifier {
  var counter = 0;

  void incrementCounter() {
    counter++;
    notifyListeners();
  }
}

// Stateless WidgetでOK
class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => CounterNotifer(),
      child: CounterPage(),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Consumer<CounterNotifer>(
          // 囲う範囲は最小限にする。
          // Consumerで囲まず、context.watch<CounterNotifier>().counter でも取得可
          builder: (context, countNotifier, child) => Text(
            countNotifier.counter.toString(),
            style: Theme.of(context).textTheme.headline4,
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        // 状態を変更するだけで、監視する必要がないので listen: false
        onPressed: Provider.of<CounterNotifer>(context, listen: false)
            .incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

状態管理したい場所をProviderで囲うのでどうしても階層が深くなってしまいます。Providerを使う時に、contextを渡さないといけないので、build関数の中でしか使えません。

アプリケーション全体で使うような状態が複数ある場合、一括で複数のProviderを登録する、MultiProvidersで一番ルートに近い場所に置いておくのが分かりやすいです。

Stateless Widgetで非同期処理をする場合、FutureBuilderを使いbuild関数の中で実行する方法がありますが、UIの変更があるたびに、毎回リビルド(再描写)されてしまい、あまりスマートではありません。

非同期処理ならStatefull WidgetのBuildContextを呼べる関数(例えば didChangeDependencies ) の中で使った方がまだ良いです。StatefulWidgetも毎回リビルドしますが、効率的にされます。

このように、ProviderはBuildContextがある状況でしか呼べないのが辛いところです。その辛みを解消したRiverpodが注目を集めています。また、Widgetツリーに関係なく状態を管理したい時は、GetItも選択肢に入るかもしれません。
参考:https://qiita.com/gki/items/eedcdcc9eb446be989c6

StateNotifier + Provider

ChangeNotifierの変数の変更を通知するのには、関数内で変更をするたびに notifylisteners を呼ぶ必要がありました。この煩わしさを解決したのがStateNotifierです。名前から汲み取ると、ChangeNotifier(変更うを通知) からStateNotifier(状態そのものを通知) となり、より宣言的に状態管理ができていると思いました。

*後で知ったのですが、StateNotifierは元からある ValueNotifierをChangeNotifier並みの使い方ができるように拡張したものだそうです。

import 'package:state_notifier/state_notifier.dart';
import 'package:flutter_state_notifier/flutter_state_notifier.dart';
...

class CounterNotifer extends StateNotifier<int> {
  CounterNotifer() : super(0);
  void incrementCounter() {
    state++;
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StateNotifierProvider<CounterNotifer, int>(
      create: (_) => CounterNotifer(),
      child: CounterPage(),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text(
          context.watch<int>().toString(),
          style: Theme.of(context).textTheme.headline4,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: Provider.of<CounterNotifer>(context, listen: false)
            .incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

StateNotifierではstate を一つだけ持ちます。stateが変更されると、listener全員に通知されます。Widget側の、Provider.of、またcontext.watchがリスナーということになり、変更を監視できます。

気をつけるポイントは
- StateNotifierProvider<CounterNotifer, int>とStateNotifier とstateの型どちらも与えること
- context.watch<CounterNotifier>()ではなくcontext.watch<int>()でstateを取得する、ということです。

Note that watching MyController will not cause the listener to rebuild when StateNotifier.state updates.
https://pub.dev/documentation/flutter_state_notifier/latest/flutter_state_notifier/StateNotifierProvider-class.html

StateNotifier + Provider + freezed

一つのStateNotifierにつき一つのstateしか作れないので、複数の状態を管理したい時はまとめてオブジェクトを作って管理します。

この例では、freezedパッケージを使い、不変ImmutableなStateクラスを作成します。stateを変更したい時は新しく状態を作成して、代入します。これにより、stateが変更されたかどうかを監視しやすくしています。そして変更を検知して、UIに自動で変更を通知してくれます。

freezedパッケージは少々使い方が面倒です。ジェネレーターというものを使い、コードを生成します。生成されたコードではcopyWithという関数が作られ、一部を変更して代入するのが簡単にできるようになります。 詳しくは https://pub.dev/packages/freezed

freezedパッケージを導入し CounterState クラスを作ります。

pubspec.yaml
dependencies:
  freezed_annotation:
dev_dependencies:
  analyzer: 0.39.14 
  build_runner:
  freezed:

*flutter 1.20ではanalyzerを指定しなきゃエラーになりました。https://github.com/rrousselGit/freezed/issues/245

// counter_state.dart
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'counter_state.freezed.dart';

@freezed
abstract class CounterState with _$CounterState {
  factory CounterState({int count, bool isActive}) = _CounterState;
}

ターミナルでジェネレーターを起動します。

flutter pub run build_runner build

これで、copyWithが使えるようになりました。

// CounterNotifier
class CounterNotifer extends StateNotifier<CounterState> {
  CounterNotifer() : super(CounterState(count: 0, isActive: true));

  void incrementCounter() {
    if (state.isActive) state = state.copyWith(count: state.count + 1);
  }
}

Widgetで使う時は

context.watch<CounterState>().count.toString(),

のように使います。他にもcontext.read, selectなどが使えます。

Riverpod

これはProviderと同じ作者が新しく実験的に作っている新しくProviderを書き直したものです。flutterに依存しないピュアなDartパッケージとして作っているそう。

This project can be considered as an experimental provider rewrite
https://pub.dev/packages/riverpod

ProviderをBuildContextに依存せず、どこでも宣言できます(すごい)
さっきのConterNotifierを使って、書き直してみます。

final counterProvider = StateNotifierProvider((ref) => CounterNotifier());

void main() {
  runApp(
    ProvierScope(
        MyApp()));
}

スコープの範囲で使用できます。ProviderScopeはルートに書くことを推奨しています。

使う時は、Consumerで囲います。

Center(
        child: Consumer((context, watch) {
          final count = watch(counterProvider).count;
          return Text('$count');
        }),
)

Flutter Hooksを使うと useProvider(hogeProvider).stateなどと使うことができます。

公式の使い方が分かりやすくて詳しいので、ぜひ読んで見てください。
https://riverpod.dev/

まだ実験段階でライブラリが安定していないので、使う時には注意が必要です。

この記事はクロス投稿です。
https://shuent.qrunch.io/entries/MBooHnzeTUj2bnZp

22
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
17