0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Flutter + Riverpod + freezedでTodoアプリ作成🍖

Last updated at Posted at 2025-05-12

はじめに

Flutter + Riverpod + freezedでTodoアプリを作成したので、作成手順を共有します。

イメージ

下記のREADMEに作成後の動画があります。
https://github.com/life-with-meat/flutter-todo-riverpod/blob/main/README.md

目次

  1. ソースコード
  2. Flutter create
  3. パッケージ追加
  4. Model作成
  5. StateNotifier作成
  6. Provider作成
  7. Ui作成

サンプルのソースコード

今回作成するTodoアプリのサンプルコードになります。
この記事で書かれているディレクトリ構成とは違っているため、ファイル単体で参考にしてください。

Flutter create

Flutterプロジェクトを作成します。

flutter create --platforms=android,ios --empty my_app
cd my_app

パッケージ追加

必要なパッケージを追加します。

flutter pub add flutter_riverpod freezed_annotation
flutter pub add --dev build_runner freezed

Model作成(Freezed)

TodoのModelをFreezedを使って作成します。

コードを見るlib/model/todo_state.dart
import 'package:freezed_annotation/freezed_annotation.dart';

part 'todo_state.freezed.dart';

@freezed
abstract class TodoState with _$TodoState {
  factory TodoState({
    required String id,
    required String title,
    required bool isDone,
  }) = _TodoState;
}

モデルを作成したあとには、次のコマンドで .freezed.dart ファイルを生成します:

flutter pub run build_runner build --delete-conflicting-outputs

StateNotifier作成

TodoPage の状態管理を行う StateNotifier を作成します。

コードを見る

lib/controller/todo_page_controller.dart

class TodoPageController extends StateNotifier<List<TodoState>> {
  TodoPageController() : super([]);

  void add(String title) {
    final newTodo = TodoState(
      id: DateTime.now().toIso8601String(),
      title: title,
      isDone: false,
    );
    state = [...state, newTodo];
  }

  void toggle(String id) {
    state = [
      for (final todo in state)
        if (todo.id == id) todo.copyWith(isDone: !todo.isDone) else todo,
    ];
  }

  void delete(String id) {
    state = state.where((todo) => todo.id != id).toList();
  }

  void edit(String id, String newTitle) {
    state = [
      for (final todo in state)
        if (todo.id == id) todo.copyWith(title: newTitle) else todo,
    ];
  }
}

Provider作成

TodoPageControllerをアプリ全体で使えるように、StateNotifierProviderを定義します。

コードを見る

lib/controller/todo_page_controller.dart


final todoPageControllerProvider =
    StateNotifierProvider<TodoPageController, List<TodoState>>((ref) {
      return TodoPageController();
    });

エラーメッセージの表示状態を管理するための StateProvider を作成します。
入力フォームでバリデーションエラーが発生したときに使用します。

lib/controller/todo_page_controller.dart;

final editErrorProvider = StateProvider<String?>((ref) => null);

ProviderScopeを設定します。

Providerを使用出来るようにするため、ProviderScopeを設定します。

コードを見る

lib/main.dart

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

Ui作成

Pageを作成します。

コードを見る

lib/page/todo_page.dart


class TodoPage extends ConsumerWidget {
  const TodoPage({super.key});
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final todoPageState = ref.watch(todoPageControllerProvider);
    final todoPageController = ref.read(todoPageControllerProvider.notifier);

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Todo リスト'),
      ),

      body: Center(
        child: ListView.builder(
          itemCount: todoPageState.length,
          itemBuilder: (context, index) {
            final todo = todoPageState[index];
            return CheckboxListTile(
              title: Text(
                todo.title,
                style: TextStyle(
                  decoration: todo.isDone ? TextDecoration.lineThrough : null,
                ),
              ),
              value: todo.isDone,
              activeColor: todo.isDone ? Colors.grey : null,
              tileColor: todo.isDone ? Colors.grey.shade300 : null,
              onChanged: (value) {
                todoPageController.toggle(todo.id);
              },
              secondary: PopupMenuButton<String>(
                icon: const Icon(Icons.more_vert),
                onSelected: (String value) {
                  if (value == 'edit') {
                    showDialog(
                      context: context,
                      builder: (BuildContext context) {
                        return TodoDialog(
                          initialTitle: todo.title,
                          dialogTitle: 'タスクを編集',
                          errorProvider: editErrorProvider,
                          onSave: (newTitle) {
                            todoPageController.edit(todo.id, newTitle);
                          },
                        );
                      },
                    );
                  } else if (value == 'delete') {
                    todoPageController.delete(todo.id);
                  }
                },
                itemBuilder:
                    (BuildContext context) => <PopupMenuEntry<String>>[
                      const PopupMenuItem<String>(
                        value: 'edit',
                        child: Text('編集'),
                      ),
                      const PopupMenuItem<String>(
                        value: 'delete',
                        child: Text('削除'),
                      ),
                    ],
              ),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          showDialog(
            context: context,
            builder: (BuildContext context) {
              return TodoDialog(
                initialTitle: '',
                dialogTitle: '新しいタスク',
                errorProvider: editErrorProvider,
                onSave: (newTitle) {
                  todoPageController.add(newTitle);
                },
              );
            },
          );
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

Widgetを作成します。

Todo編集ダイアログのWidgetです。

コードを見る

lib/widget/todo_dialog.dart


class TodoPage extends ConsumerWidget {
  const TodoPage({super.key});
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final todoPageState = ref.watch(todoPageControllerProvider);
    final todoPageController = ref.read(todoPageControllerProvider.notifier);

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Todo リスト'),
      ),

      body: Center(
        child: ListView.builder(
          itemCount: todoPageState.length,
          itemBuilder: (context, index) {
            final todo = todoPageState[index];
            return CheckboxListTile(
              title: Text(
                todo.title,
                style: TextStyle(
                  decoration: todo.isDone ? TextDecoration.lineThrough : null,
                ),
              ),
              value: todo.isDone,
              activeColor: todo.isDone ? Colors.grey : null,
              tileColor: todo.isDone ? Colors.grey.shade300 : null,
              onChanged: (value) {
                todoPageController.toggle(todo.id);
              },
              secondary: PopupMenuButton<String>(
                icon: const Icon(Icons.more_vert),
                onSelected: (String value) {
                  if (value == 'edit') {
                    showDialog(
                      context: context,
                      builder: (BuildContext context) {
                        return TodoDialog(
                          initialTitle: todo.title,
                          dialogTitle: 'タスクを編集',
                          errorProvider: editErrorProvider,
                          onSave: (newTitle) {
                            todoPageController.edit(todo.id, newTitle);
                          },
                        );
                      },
                    );
                  } else if (value == 'delete') {
                    todoPageController.delete(todo.id);
                  }
                },
                itemBuilder:
                    (BuildContext context) => <PopupMenuEntry<String>>[
                      const PopupMenuItem<String>(
                        value: 'edit',
                        child: Text('編集'),
                      ),
                      const PopupMenuItem<String>(
                        value: 'delete',
                        child: Text('削除'),
                      ),
                    ],
              ),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          showDialog(
            context: context,
            builder: (BuildContext context) {
              return TodoDialog(
                initialTitle: '',
                dialogTitle: '新しいタスク',
                errorProvider: editErrorProvider,
                onSave: (newTitle) {
                  todoPageController.add(newTitle);
                },
              );
            },
          );
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

入力欄のWidgetです。

コードを見る

lib/widget/app_text_field.dart

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

class AppTextField extends ConsumerStatefulWidget {
  final String initialValue;
  final String label;
  final StateProvider<String?> errorProvider;
  final ValueChanged<String> onChanged;

  const AppTextField({
    super.key,
    required this.initialValue,
    required this.label,
    required this.errorProvider,
    required this.onChanged,
  });

  @override
  ConsumerState<AppTextField> createState() => _AppTextFieldState();
}

class _AppTextFieldState extends ConsumerState<AppTextField> {
  late final TextEditingController _controller;

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController(text: widget.initialValue);
  }

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

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: _controller,
      decoration: InputDecoration(
        labelText: widget.label,
        errorText: ref.watch(widget.errorProvider),
      ),
      onChanged: (value) {
        widget.onChanged(value);
        ref.read(widget.errorProvider.notifier).state = null;
      },
    );
  }
}

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?