📝
【Flutter】BLoCパターンを使用したTodoアプリの実装 with sqflite
BLoC パターンでの実装方法を学びたいと思い、メモとして記しておきます。この Todo アプリでは、BLoC パターンを使用してデータフローを管理し、SQLite でローカルデータを保存しています。今回は簡単な CRUD 機能を実装するのみとします。以下の記事を参考に実装しております。
今回作成したプロジェクト
プロジェクトの構成
まず必要なパッケージをpubspec.yaml
に追加します:
dependencies:
sqflite: ^2.3.0 # SQLiteデータベース用
path_provider: ^2.1.1 # ファイルパス取得用
ディレクトリ構成
lib/
├── bloc/
│ └── todo_bloc.dart
├── dao/
│ └── todo_dao.dart
├── database/
│ └── database.dart
├── model/
│ └── todo.dart
├── repository/
│ └── todo_repository.dart
└── ui/
└── home_page.dart
データモデルの実装
まずTodo
モデルクラスを作成します:
class Todo {
int id;
String description;
bool isDone;
// コンストラクタ - isDoneはデフォルトでfalse
Todo({
this.id,
this.description,
this.isDone = false
});
// JSONからTodoオブジェクトを生成するファクトリメソッド
factory Todo.fromDatabaseJson(Map<String, dynamic> data) => Todo(
id: data['id'],
description: data['description'],
// SQLiteにはboolean型がないため、0/1で管理
isDone: data['is_done'] == 0 ? false : true,
);
// TodoオブジェクトをJSONに変換するメソッド
Map<String, dynamic> toDatabaseJson() => {
"id": this.id,
"description": this.description,
"is_done": this.isDone == false ? 0 : 1,
};
}
データベースの実装
SQLite データベースを管理するクラスを作成します。データベースに関連する詳細設定などを行います。
class DatabaseProvider {
static final DatabaseProvider dbProvider = DatabaseProvider();
Database _database;
// データベースのシングルトンインスタンスを取得
Future<Database> get database async {
if (_database != null) return _database;
_database = await createDatabase();
return _database;
}
// データベースの作成
createDatabase() async {
// アプリのドキュメントディレクトリを取得
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, "TodoApp.db");
var database = await openDatabase(
path,
version: 1,
onCreate: initDB,
onUpgrade: onUpgrade
);
return database;
}
// テーブルの作成
void initDB(Database database, int version) async {
await database.execute("""
CREATE TABLE Todo (
id INTEGER PRIMARY KEY,
description TEXT,
is_done INTEGER
)
""");
}
// データベースの更新時の処理
void onUpgrade(Database database, int oldVersion, int newVersion) {
if (newVersion > oldVersion) {
// マイグレーション処理を記述
}
}
}
データアクセスレイヤーの実装
Todo の CRUD 操作を行う DAO クラスを実装します。この DAO クラス内で sqflite の CRUD 処理を閉じ込めることで、sqflite の実装を変更する範囲を小さくします。
class TodoDao {
final dbProvider = DatabaseProvider.dbProvider;
// Todo作成
Future<int> createTodo(Todo todo) async {
final db = await dbProvider.database;
var result = db.insert('Todo', todo.toDatabaseJson());
return result;
}
// Todo一覧取得
Future<List<Todo>> getTodos({String? query}) async {
final db = await dbProvider.database;
List<Map<String, dynamic>> result;
if (query != null && query.isNotEmpty) {
result = await db
.query('Todo', where: 'description LIKE ?', whereArgs: ["%$query%"]);
} else {
result = await db.query('Todo');
}
List<Todo> todos = result.isNotEmpty
? result.map((item) => Todo.fromDatabaseJson(item)).toList()
: [];
return todos;
}
// 更新と削除は省略
}
リポジトリレイヤーの実装
TodoRepository はデータソースの抽象化を行います:
class TodoRepository {
final todoDao = TodoDao();
Future getAllTodos({String? query}) => todoDao.getTodos(query: query);
Future insertTodo(Todo todo) => todoDao.createTodo(todo);
// 更新と削除は省略
}
BLoC の実装
TodoBLoC はビジネスロジックとデータの流れを管理します。例えば、TODO リストの新規追加を行ったあとに、すべての TODO リストの読み込む処理の流れを実装する。
class TodoBloc {
final _todoRepository = TodoRepository();
// StreamControllerでTodoリストのストリームを管理
final _todoController = StreamController<List<Todo>>.broadcast();
// Todoリストを取得するためのストリーム
Stream<List<Todo>> get todos => _todoController.stream;
TodoBloc() {
getTodos();
}
// Todoリストの取得
getTodos({String query}) async {
_todoController.sink.add(
await _todoRepository.getAllTodos(query: query)
);
}
// 新規Todo追加
addTodo(Todo todo) async {
await _todoRepository.insertTodo(todo);
getTodos();
}
// 更新と削除は省略
}
UI の実装
最後に UI を実装します。StreamBuilder
を使用して Todo
リストを表示します:
-
TodoBloc
を宣言して、todos
プロパティを用いて値を表示します。 - 追加・更新・削除は、メソッドを用いて処理します。
-
TextEditingController
を用いて、クエリを用いて該当するtodo
を検索できます。
コード量が多くなるため、詳細は Github をご覧ください。
まとめ
BLoC パターンを使用することで、以下のメリットが得られます:
- ビジネスロジックと UI の分離
- コードの再利用性の向上
- テストの容易さ
- 状態管理の一元化
このアプリケーションでは、Stream と StreamController を使用してデータの流れを管理し、SQLite でデータを永続化しています。各レイヤー(Model、DAO、Repository、BLoC)が明確な責務を持ち、保守性の高いコードとなっています。
参照
Discussion