すらぼうの開発ノート

モバイルアプリエンジニアのメモ

【Flutter】Riverpodの使い方

Riverpodとは

RiverpodはFlutterにて状態管理を行うためのパッケージ。

pub.dev

同じく状態管理向けパッケージのproviderと同じ作者が作成したもので、 providerと比較して様々な改良がなされている。

note-tmk.hatenablog.com

使い方

インストール

dependencies:
  flutter_riverpod: ^2.4.0

providerをグローバルで宣言

グローバルにてfinalでproviderを作成する。

final myProvider = Provider((ref) {
  return [ 管理する状態 ]
} );

providerには以下の種類があり、用途によって使い分ける。

  • Provider
    • キャッシュなどで使用する(サンプルのTodoアプリ)
  • StateNotifierProvider
    • 自作クラスなどを通知する際に使用する
    • 自作クラスはStateNotifierを継承する
  • FutureProvider
  • StreamProvider
  • StateProvider
    • 外部から状態を変更したい場合使用
    • 組み込み型(int, String, bool...)などの軽量な状態を管理するのに向いている
    • 自作クラスなどを管理する場合、StateNotifierProviderの使用が推奨されている
  • ChangeNotifierProvider

アプリ全体をProviderScopeでラップ

providerを伝播させるため、ProviderScopeでルートウィジェットをラップする。

void main() {
  runApp(
    const ProviderScope(
      child: [ ルートウィジェット ],
    ),
  );
}

従来のproviderだとMultiProviderなどでラップしてproviderを登録する必要があったが、 riverpodではproviderの登録は不要。

ConsumerWidgetなどを継承したクラスを作成

ConsumerWidgetなどのウィジェットを継承してウィジェットを作成する。


class MyWidget extends ConsumerWidget {
  const MyWidget({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ...
    // refを参照してproviderから状態を取得
  }
}

buildメソッド内でrefを参照して登録されているproviderから状態を取得できる。 通常のStatelessWidgetとの違いはWidgetRefを使用できる点のみ。 なので状態参照が不要なウィジェットConsumerWidgetなどを継承する必要はない。

providerを参照するためのウィジェットは以下がある。

  • ConsumerWidget
    • StatelessWidgetの代替
  • ConsumerStatefulWidget+ConsumerState
    • StatefulWidget+Stateの代替
  • HookConsumerWidget
    • HookWidget の代替
    • flutter_hooksで使用する
  • StatefulHookConsumerWidget
    • HookWidgetの代替
    • flutter_hooksで使用する
  • Consumer

riverpod.dev

providerから状態を取得

ref.watch

providerの変更を監視するならばref.watch()を使用する。

ref.watch([ 取得したいprovider名 ])

ref.listen

provider更新を監視し、更新タイミングで何かしらの処理を実行したい場合ref.listen()を使用する。

ref.listen([ 取得したいprovider名 ], ([ 変更前provider ], [ 変更後provider ]){ [実行したい処理] })

ref.listen()の使用ケースは、変更をユーザーに通知したい場合など。 例えばproviderで管理した値の変化をsnackbarやダイアログで表示するケース。 変更前と変更後を取得できるので、「〇〇を[変更前]→[変更後]に更新しました」などの文言が出せる。

ref.read

providerの監視が不要な場合、ref.read()を使用する。

ref.read( [取得したいprovider名] )

公式ではあまり使用が推奨されていない様子。 使用ケースはproviderの値が不要でメソッドを実行したい場合など。

riverpod.dev

サンプル

カウンターアプリ

  • StateProviderでint値を管理
  • インクリメントとリセットボタンを用意
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// カウンター管理用provider
final counterProvider = StateProvider<int>((_) => 0);

void main() {
  runApp(
    // アプリをラップしてproviderを伝播させる
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Counter')),
        body: Center(
          child: Text(
            // カウンター値を表示
            "${ref.watch(counterProvider)}",
            style: const TextStyle(fontSize: 48),
          ),
        ),
        floatingActionButton: Column(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            FloatingActionButton(
              child: const Icon(Icons.add),
              // stateをインクリメント
              onPressed: () => ref.watch(counterProvider.notifier).state++,
            ),
            const SizedBox(
              height: 20,
            ),
            FloatingActionButton(
              backgroundColor: Colors.red,
              child: const Icon(
                Icons.exposure_zero,
                color: Colors.white,
              ),
              // stateをリセット
              onPressed: () => ref.refresh(counterProvider),
            ),
          ],
        ),
      ),
    );
  }
}


Todoアプリ

  • todoの追加、完了状態変更
  • 完了状態のみ表示
    • 完了todoのみを表示する場合はProviderでキャッシュ

リポジトリは以下。

github.com

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

// provider --------------------------------

/// 全todo
final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
  return TodosNotifier();
});

/// 完了済みtodo
final completedTodosProvider = Provider<List<Todo>>((ref) {
  final todos = ref.watch(todosProvider);

  return todos.where((todo) => todo.isCompleted).toList();
});

// model --------------------------------

/// todoモデル
class Todo {
  Todo(this.id, this.description, this.isCompleted);
  final int id;
  final bool isCompleted;
  final String description;

  Todo copyWith(int id, String description, bool isCompleted) {
    return Todo(id, description, isCompleted);
  }
}

// StateNotifier --------------------------------

/// todo通知用
class TodosNotifier extends StateNotifier<List<Todo>> {
  TodosNotifier() : super([]);

  /// 追加
  void addTodo(String description) {
    int maxId = 0;

    for (Todo todo in state) {
      if (todo.id > maxId) {
        maxId = todo.id;
      }
    }

    final newTodo = Todo(maxId + 1, description, false);
    state = [...state, newTodo];
  }

  /// 完了状態を更新
  void toggle(int todoId) {
    state = [
      for (Todo todo in state)
        todo.id == todoId
            ? todo.copyWith(todo.id, todo.description, !todo.isCompleted)
            : todo
    ];
  }
}

// view --------------------------------

void main() {
  runApp(
    // アプリをラップしてproviderを伝播させる
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      routes: {
        "/": (context) => const TodoListPage(),
        "/create": (context) => const TodoCreatePage()
      },
    );
  }
}

/// todo一覧ページ
class TodoListPage extends ConsumerStatefulWidget {
  const TodoListPage({super.key});

  @override
  ConsumerState<TodoListPage> createState() => _TodoListPageState();
}

class _TodoListPageState extends ConsumerState<TodoListPage> {
  bool isFiltered = false;

  @override
  Widget build(BuildContext context) {
    final todos = ref.watch(todosProvider);
    final completedTodos = ref.watch(completedTodosProvider);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Todo List'),
        actions: [
          IconButton(
              onPressed: () {
                Navigator.of(context).pushNamed("/create");
              },
              icon: const Icon(Icons.add))
        ],
      ),
      body: Center(
          child: Column(
        children: [
          Container(
            padding: EdgeInsets.all(16),
            // height: 80,
            width: double.infinity,
            child: Row(children: [
              Row(children: [
                Checkbox(
                    value: isFiltered,
                    onChanged: (value) {
                      setState(() {
                        isFiltered = value!;
                      });
                    }),
                const Text("completed only"),
              ])
            ]),
          ),
          isFiltered
              ? Expanded(
                  child: ListView.builder(
                      itemCount: completedTodos.length,
                      itemBuilder: (context, index) {
                        return Card(
                          child: CheckboxListTile(
                            title: Text(completedTodos[index].description),
                            value: completedTodos[index].isCompleted,
                            onChanged: (value) {
                              ref
                                  .read(todosProvider.notifier)
                                  .toggle(todos[index].id);
                            },
                          ),
                        );
                      }),
                )
              : Expanded(
                  child: ListView.builder(
                      itemCount: todos.length,
                      itemBuilder: (context, index) {
                        return Card(
                          child: CheckboxListTile(
                            title: Text(todos[index].description),
                            value: todos[index].isCompleted,
                            onChanged: (value) {
                              ref
                                  .read(todosProvider.notifier)
                                  .toggle(todos[index].id);
                            },
                          ),
                        );
                      }),
                ),
        ],
      )),
    );
  }
}

/// todo作成ページ
class TodoCreatePage extends ConsumerStatefulWidget {
  const TodoCreatePage({super.key});

  @override
  ConsumerState<TodoCreatePage> createState() => _CreateTodoState();
}

class _CreateTodoState extends ConsumerState<TodoCreatePage> {
  final _controller = TextEditingController();

  @override
  void dispose() {
    super.dispose();
    _controller.dispose();
  }

  @override
  Widget build(
    BuildContext context,
  ) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Create Todo"),
        automaticallyImplyLeading: false,
      ),
      body: Column(
        children: [
          Container(
            padding: EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text("Description"),
                TextField(
                  controller: _controller,
                ),
              ],
            ),
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                onPressed: () => Navigator.of(context).pop(),
                child: const Text("Cancel"),
                style: ElevatedButton.styleFrom(backgroundColor: Colors.grey),
              ),
              const SizedBox(
                width: 10,
              ),
              ElevatedButton(
                  onPressed: () {
                    if (_controller.text.isNotEmpty) {
                      ref
                          .read(todosProvider.notifier)
                          .addTodo(_controller.text);
                      Navigator.of(context).pop();
                    }
                  },
                  child: const Text("Create")),
            ],
          )
        ],
      ),
    );
  }
}