LoginSignup
24
19

More than 3 years have passed since last update.

Flutterのいろんな状態管理を簡単なカウンターアプリで実装して説明してみる

Posted at

Flutterの状態管理(StatefulWidget,provider,riverpod)についてそれぞれ簡単に説明・比較してみます。

そもそも状態管理とは?

20年前のホームページとは違い、最近のアプリやWebは、ユーザの操作によって動的に画面が変わります。
例えば、スマホの設定画面でWiFiのスイッチを押すと、スイッチが動的にその場で切り替わります。
例えば、Twitterでダークモードを選択するとその場で画面が真っ黒になります。
このように使われるアプリケーションの状態(State)を反映するように画面を描画することを宣言的(declarative)と言います。Flutterはこの宣言的UIが作れるようにできています。

宣言的UIを実現するためにはアプリケーションの状態(State)を管理(management)する必要があります。

参考: https://flutter.dev/docs/development/data-and-backend/state-mgmt/intro

ん、具体的に何を管理するの?

例えば・・・

認証したユーザの名前やアイコン(ログインしてるのか否かも含む)
たくさんあるリストのチェックボックスにチェックが入ったかどうか
WiFiをONにするかOFFにするか
書き込んだ日付

っていうか画面を描画するために必要な変数全般です。逆に、描画に関係ない情報は当たり前ですが普通持ちません。

状態管理のサンプル

有名そうなパターンについて簡単なサンプルコードを作ってみたので、それらを解説しながら具体的に説明していきます。
それぞれの方法の詳細やアーキテクチャはそれぞれの公式を参照してください。

カウンターアプリをいろんな方法で実装してみる

Flutterのプロジェクトを立ち上げた時にでき上がるアプリ(ボタンを押したことをカウントする)を敢えてそれぞれの状態管理を使って実現してみます。
ボタンを押した回数を今回状態として保持します。
(プロジェクト作成時に出来上がるlib/main.dartの中身とpubspec.ymlを書き換えて実行してください。)

作ったソースコード全部はこちら

StatefulWidget

簡単に説明

Flutterのあらゆる教材で一番最初に出てきます。まっさらなflutterアプリを作るとこのコードが最初から入っているはずです。
setStateという関数をウィジェットが入っているクラスの中で使うことによって、状態の変更を更新することができます。
1個2個の状態であればこれで十分ですが、たくさんの状態を管理することになると、setStateばっかりになってしまい、どこでどんなふうな変更したか追いかけるのが大変になってしまいます。

コード(lib/main.dart)

StatefulWidget

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

  void _incrementCounter() { 
    // 2.呼び出されると状態が更新されて画面に反映される
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter, // 1.ボタンを押したら関数が呼ばれる
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

Provider

簡単に説明

Flutter公式でもおすすめされている状態管理の方法です。
ChangeNotifierを継承したクラスで状態を定義します。notifyListnersを都度書いて画面更新することになります。
オブジェクトをリッスンしないと描画できません。

コード(lib/main.dart)

provider

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// 1.状態を定義する(ChangeNotifierを拡張したクラスを定義する)
class CountState extends ChangeNotifier {
  int count = 0;
  void addCount() {
    count++;
    // これがないと描画してくれない
    notifyListeners();
  }
}

void main() {
  runApp(CountApp());
}

class CountApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // CounterStateを呼び出す、ただしWidgetの中で!
    final CountState countState = CountState();

    // 2.ウィジェット全体をChangeNotifierProviderで囲む
    return ChangeNotifierProvider<CountState>.value(
      value: countState,
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: Text('CounterApp')),
          body: _Body(),
          floatingActionButton: _FloatingActionButton(),
        ),
      ),
    );
  }
}

class _Body extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 3.Stateを取り出す
    final CountState countState = Provider.of<CountState>(context);
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text(
            'You have pushed the button this many times:',
          ),
          Text(
            countState.count.toString(),
            style: Theme.of(context).textTheme.headline4,
          ),
        ],
      ),
    );
  }
}

class _FloatingActionButton extends StatelessWidget {
  Widget build(BuildContext context) {
    // 3.Stateを取り出す
    final CountState counterState = Provider.of<CountState>(context);
    return FloatingActionButton(
      onPressed: () => counterState.addCount(),
      child: Icon(Icons.add),
    );
  }
}

Riverpod

簡単に説明

providerの作者の方がより使いやすくなるように作り直した状態管理のパッケージがriverpodです。
グローバルに定義することができるので、どこのウィジェットからでも状態を読み書きできます。
また、実行時ではなくコンパイル時にプログラミングエラーを見つけることができます。

以下の流れで使うことができます。
①まず最初にApp全体をProviderScopeで囲む
②StateProviderを使ってProviderを宣言。グローバルに宣言
③Listenしておかなくても、providerを読んで更新できる
(④ConsumerWidgetを使って、providerを読む)

コード(lib/main.dart)

riverpod

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(
    // ①まず最初にApp全体をProviderScopeで囲む
    const ProviderScope(child: MyApp()),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: Home());
  }
}

/// ②StateProviderを使ってProviderを宣言。グローバルに宣言。
final StateProvider counterProvider = StateProvider((ref) {
  return 0;
});

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(
        child: GoodText(context),
      ),
      floatingActionButton: FloatingActionButton(
        // ③Listenしておかなくても、providerを読んで更新できる。
        onPressed: () => context.read(counterProvider).state++,
        child: const Icon(Icons.add),
      ),
    );
  }
}

class GoodText extends ConsumerWidget {
  GoodText(BuildContext context);

  Widget build(BuildContext context, ScopedReader watch) {
    // ④ConsumerWidgetを使って、providerを読む
    final count = watch(counterProvider).state;
    print("iine is rewrite $count");
    return Text('$countいいね');
  }
}

Riverpod + Flutter Hooks

簡単に説明

React Hooksっぽく書くことができます。

コード(lib/main.dart)

riverpod&Flutter Hooks

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:state_notifier/state_notifier.dart';

void main() {
  // 1.まず最初にApp全体をProviderScopeで囲む
  runApp(
    ProviderScope(
      child: CounterApp(),
    ),
  );
}

// 2.StateNotifierProviderを使ってcounterProviderを宣言。グローバルに宣言。
final counterProvider = StateNotifierProvider((_) => Counter());


// StateNotifier<**> -> **はStateという変数の型になる
class Counter extends StateNotifier<int> {
  Counter() : super(0);
  void increment() => state++;
}

// Note: CounterApp is a HookWidget, from flutter_hooks.
class CounterApp extends HookWidget {
  @override
  Widget build(BuildContext context) {
    // 3.useProviderを使って状態を取得
    final state = useProvider(counterProvider.state);
    final counter = useProvider(counterProvider);

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('CounterApp')),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                'You have pushed the button this many times:',
              ),
              Text(
                state.toString(),
                style: Theme.of(context).textTheme.headline4,
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          //作って置いた関数を呼び出して値を変えることもできる
          onPressed: () => counter.increment(),
          child: Icon(Icons.add),
        ), // This trailing comma makes auto-formatting nicer for build methods.
      ),
    );
  }
}

で、どれ使えば良い?

上記のコードだけで判断しないでください・・・。一通り公式を見ながらサンプル実装してみた方がいいです。

グローバルに定義できてどこからでも状態を読み書きできるriverpodに慣れてしまうと便利で辞められないというのが個人的な意見です。
一方で宗教上の都合で(どんな宗教か分かりませんが)Flutter公式しか使いたくないならproviderがよいと思います。
また、簡単な画面1枚の趣味アプリだったらStatefulWidgetでも十分かもしれません。

既に開発したプロダクトの規模がある程度大きくなっていて、既に使っている状態管理があればそれを使った方が良いです。
また、複数人で作る場合はどの状態管理を使うか明示的に共有しないとカオスになるので、そこはチームで話し合ってどれにするか決めた方がいいはずです。

Flutterは新しいパッケージが次々と出てくるのであと数ヶ月したらもうdeprecatedなんてこともあるかもしれません。
その時期その時期の良い方法を使っていった方が良いと思いますS。

24
19
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
24
19