はじめに
Flutter + Riverpod + freezedでTodoアプリを作成したので、作成手順を共有します。
イメージ
下記のREADMEに作成後の動画があります。
https://github.com/life-with-meat/flutter-todo-riverpod/blob/main/README.md
目次
- ソースコード
- Flutter create
- パッケージ追加
- Model作成
- StateNotifier作成
- Provider作成
- 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.dartimport '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を設定します。
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;
},
);
}
}