はじめに
今回、Flutterにおける状態管理の手法の一つであるRiverpod, StateNotifier, Freezedと
FlutterのローカルDBのMoorを使用して簡単なTodoアプリを作成しました。
Flutterを始めてまだ数ヶ月の初学者ですので内容に不十分な点があるかもしれませんが、
アウトプットも兼ねて記事を書くことにしました。
※Riverpod, StateNotifier, freezed, Moorのそれぞれの詳しい説明は本記事では割愛してます。
※現在Flutter2.0.5が最新版ですが、今回のTodoアプリではバージョン1.22.6を使用しています。
※Riverpodも最新版ではありませんが、以下を参照すれば対応可能だと思います。
インストール
まずは今回のTodoアプリを作成するのに必要なパッケージをpubspec.yamlに追記します。
Moorについては以下を参照し、パッケージを追加しました。
dependencies:
flutter:
sdk: flutter
//以下はRiverpod, StateNotifier, freezedで使用。
freezed_annotation: ^0.11.0
flutter_riverpod: ^0.12.1
flutter_state_notifier: ^0.6.1
//以下Moorで使用。
moor: ^3.4.0
sqlite3_flutter_libs: ^0.4.0
path_provider: ^1.6.28
path: ^1.7.0
//View側をいい感じにするために使用。
flutter_form_builder: ^4.2.0
date_time_picker: ^1.1.1
flutter_slidable: ^0.5.7
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^1.10.0
freezed: ^0.11.2
moor_generator: ^3.3.1
pub get
を実行します。
テーブル定義
MoorのGetting started参考に以下のように書きました。
import 'package:moor/moor.dart';
part 'db.g.dart';
class TodoItem extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 1, max: 24)();
TextColumn get sentence => text().withLength(min: 1, max: 100)();
TextColumn get createdAt => text()();
TextColumn get limitDate => text()();
}
@UseMoor(tables: [TodoItem])
class MyDatabase {
}
ここで、一旦以下のコマンドを実行します。
$ flutter packages pub run build_runner build
修正を行った場合など、2回目以降build_runner
を走らせる場合は、
$ flutter packages pub run build_runner build --delete-conflicting-outputs
で実行します。
db.g.dart
が作成されますので、ここから先程のdb.dart
に追記をしていきます。
import 'dart:io';
import 'package:moor/ffi.dart';
import 'package:moor/moor.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
part 'db.g.dart';
class TodoItem extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 1, max: 24)();
TextColumn get sentence => text().withLength(min: 1, max: 100)();
TextColumn get createdAt => text()();
TextColumn get limitDate => text()();
}
LazyDatabase _openConnection() {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'db.sqlite'));
return VmDatabase(file);
});
}
//以下主な変更部分
@UseMoor(tables: [TodoItem])
class MyDatabase extends _$MyDatabase {
static MyDatabase _instance;
static MyDatabase getInstance() {
//シングルトン対応
if (_instance == null) {
_instance = new MyDatabase();
}
return _instance;
}
MyDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
//全てのデータ取得
Future<List<TodoItemData>> readAllTodoData() => select(todoItem).get();
//追加
Future writeTodo(TodoItemData data) => into(todoItem).insert(data);
//更新
Future updateTodo(TodoItemData data) => update(todoItem).replace(data);
//削除
Future deleteTodo(int id) =>
(delete(todoItem)..where((it) => it.id.equals(id))).go();
}
MoorのGetting startedを参考にしていけば、なんとかここまでは書けました。
ちなみにMoorではなくShared_Preferencesを使用してもいいと思います。
(Shared_Preferencesのほうが参考文献が多いので始めての方はやりやすいかもしれません。)
自分の理解が乏しく、上記のようなことを記載していましたが、Moorと同じようには使えるものではありません。全くの別物として考えてください。
↓Shared_Preferences
//全てのデータ取得
Future<List<TodoItemData>> readAllTodoData() => select(todoItem).get();
//追加
Future writeTodo(TodoItemData data) => into(todoItem).insert(data);
//更新
Future updateTodo(TodoItemData data) => update(todoItem).replace(data);
//削除
Future deleteTodo(int id) =>
(delete(todoItem)..where((it) => it.id.equals(id))).go();
get
でデータ取得
insert
で追加
replace
で更新
go
で削除
となるようです。
Repository
先程のdb_dart
とやり取りを行うRepositoryです。
class MoorRepository {
MoorRepository() {
_database = MyDatabase.getInstance();
}
MyDatabase _database;
//全てのデータ取得
Future<List<TodoItemData>> readAllTodoItems() async =>
await _database.readAllTodoData();
//追加
Future writeTodoData(TodoItemData data) async =>
await _database.writeTodo(data);
//更新
Future deleteTodoData(int id) async => await _database.deleteTodo(id);
//削除
Future updateTodoData(TodoItemData data) async =>
await _database.updateTodo(data);
}
State
では次にStateを作ります。
こちらではfreezedを使用しています。
import 'package:.../moor/db.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'todo_state.freezed.dart';
@freezed
abstract class TodoState with _$TodoState {
const factory TodoState({
@Default(false) bool isLoading,
@Default(false) bool isReadyData,
List<TodoItemData> todoItems,
}) = _TodoState;
}
エラーが出てる状態ですが、ここまで書いて先程の
$ flutter packages pub run build_runner build
を実行するとtodo_state.freezed.dart
が自動生成され、エラーが消えます。
List<TodoItemData> todoItems
の部分ですが、下記のdb.dart
で定義した部分が、
class TodoItem extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 1, max: 24)();
TextColumn get sentence => text().withLength(min: 1, max: 100)();
TextColumn get createdAt => text()();
TextColumn get limitDate => text()();
}
build_runnerによって生成されたdb.g.dart
内で以下のようになっており、
このTodoItemData
を使い、List<TodoItemData> todoItems
とします。
class TodoItemData extends DataClass implements Insertable<TodoItemData> {
final int id;
final String title;
final String sentence;
final String createdAt;
final String limitDate;
TodoItemData(
{@required this.id,
@required this.title,
@required this.sentence,
@required this.createdAt,
@required this.limitDate});
//以下省略
StateNotifier
では次にStateNotifierの部分を書いていきます。
import 'package:.../moor/db.dart';
import 'package:.../moor/repository/moor_repository.dart';
import 'package:.../moor/state/todo_state.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class TodoStateNotifier extends StateNotifier<TodoState> {
TodoStateNotifier() : super(TodoState()) {
readData(); //最初に実行される。
}
MoorRepository _repository = MoorRepository();
//書き込み処理部分
writeData(TodoItemData data) async {
state = state.copyWith(isLoading: true);
await _repository.writeTodoData(data);
readData();
}
//削除処理部分
deleteData(TodoItemData data) async {
state = state.copyWith(isLoading: true);
await _repository.deleteTodoData(data.id);
readData();
}
//データ読み込み処理
readData() async {
state = state.copyWith(isLoading: true);
final todoItems = await _repository.readAllTodoItems();
state = state.copyWith(
isLoading: false,
isReadyData: true,
todoItems: todoItems,
);
}
}
先程作成したtodo_repository.dart
とやり取りを行うクラスです。
state
の中身はこちらも先程作成したtodo_state.dart
となってます。
View
ここからいよいよ画面のコードを書いていきます。
ここに関しても詳しい解説は割愛します。
import 'package:.../moor/db.dart';
import 'package:.../moor/todo_state_notifier.dart';
import 'package:date_time_picker/date_time_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
//ここでtodo_state_notifierを呼び出す。
final toDoStateNotifierProvider =
StateNotifierProvider((ref) => TodoStateNotifier());
//ConsumerWidgetで囲む。
class TodoScreen extends ConsumerWidget {
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context, ScopedReader watch) {
//stateを呼び出す。
final state = watch(toDoStateNotifierProvider.state);
TodoItemData itemData;
return Scaffold(
body: SafeArea(
child: Stack(
children: [
Container(
child: Center(
child: state.isReadyData
? _createBody(state.todoItems)
: Container(),
),
),
state.isLoading
? Container(
color: Color(0x88000000),
child: Center(
child: CircularProgressIndicator(),
),
)
: Container(),
],
),
),
floatingActionButton: _createFloatingActionButton(context, itemData),
);
}
Widget _createBody(List<TodoItemData> itemData) {
return ListView.builder(
itemCount: itemData.length,
itemBuilder: (context, index) {
final data = itemData[index];
return _buildTodoItem(context, data);
},
);
}
Widget _buildTodoItem(BuildContext context, TodoItemData data) {
return Slidable(
actions: [
IconSlideAction(
caption: 'delete',
icon: Icons.delete,
color: Colors.red,
onTap: () {
//メソッドの呼び出しはcontext.readを使う。
context.read(toDoStateNotifierProvider).deleteData(data);
},
)
],
actionPane: SlidableDrawerActionPane(),
child: Container(
margin: EdgeInsets.only(top: 5),
child: ListTile(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'タイトル:${data.title}',
style: TextStyle(fontSize: 15),
),
SizedBox(height: 5),
Text(
data.sentence,
style: TextStyle(fontSize: 20),
),
],
),
subtitle: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('作成日:${data.createdAt.toString()}'),
Text(
'期日:${data.limitDate}',
style: TextStyle(color: Colors.red[400]),
),
],
),
Divider(
color: Colors.grey,
)
],
),
),
),
);
}
Widget _createFloatingActionButton(
BuildContext context, TodoItemData itemData) {
return FloatingActionButton(
child: Icon(Icons.edit),
onPressed: () {
_showInputDialog(context, itemData);
},
);
}
Future _showInputDialog(BuildContext context, TodoItemData itemData) {
final today = DateTime.now();
TextEditingController _titleController = TextEditingController();
TextEditingController _sentenceController = TextEditingController();
TextEditingController _limitDateController = TextEditingController();
return showDialog(
context: context,
builder: (_) {
return AlertDialog(
content: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: _titleController,
decoration: InputDecoration(
hintText: 'タイトルを入力してください',
icon: Icon(Icons.text_fields),
),
textInputAction: TextInputAction.next,
validator: (value) {
if (value.isEmpty) {
return '未入力です';
}
return null;
},
),
TextFormField(
controller: _sentenceController,
decoration: InputDecoration(
hintText: '内容を入力してください',
icon: Icon(Icons.text_snippet),
),
textInputAction: TextInputAction.next,
validator: (value) {
if (value.isEmpty) {
return '未入力です';
}
return null;
},
),
DateTimePicker(
controller: _limitDateController,
dateMask: 'yyyy年MM月dd日',
type: DateTimePickerType.date,
firstDate: DateTime(2019),
lastDate: DateTime(2050),
decoration: InputDecoration(
hintText: '期限を選択してください',
icon: Icon(Icons.calendar_today),
),
validator: (dateTime) {
if (dateTime.isEmpty) {
return '日付が未入力です。';
}
return null;
},
),
],
),
),
actions: [
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('キャンセル'),
),
ElevatedButton(
onPressed: () async {
final formatter = DateFormat('yyyy-MM-dd');
var todo = TodoItemData(
id: null,
title: _titleController.text,
sentence: _sentenceController.text,
createdAt: formatter.format(today),
limitDate: _limitDateController.text,
);
if (_formKey.currentState.validate()) {
await context.read(toDoStateNotifierProvider).writeData(todo);
Navigator.of(context).pop();
}
},
child: Text('保存'),
),
],
);
},
);
}
}
完成形
簡易的ですが完成形は以下のような感じです。
ひとまず動けば良し、で作ったのでコードは改善すべき部分が多いと思います。
少しでも誰かの参考になれば幸いです。