📝

【Flutter】BLoCパターンを使用したTodoアプリの実装 with sqflite

2025/02/18に公開

BLoC パターンでの実装方法を学びたいと思い、メモとして記しておきます。この Todo アプリでは、BLoC パターンを使用してデータフローを管理し、SQLite でローカルデータを保存しています。今回は簡単な CRUD 機能を実装するのみとします。以下の記事を参考に実装しております。

https://vaygeth.medium.com/reactive-flutter-todo-app-using-bloc-design-pattern-b71e2434f692

今回作成したプロジェクト

https://github.com/muranakar/sqflite_bloc

プロジェクトの構成

まず必要なパッケージを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 をご覧ください。

https://github.com/muranakar/sqflite_bloc/blob/main/lib/ui/home_page.dart#L5-L143

まとめ

BLoC パターンを使用することで、以下のメリットが得られます:

  • ビジネスロジックと UI の分離
  • コードの再利用性の向上
  • テストの容易さ
  • 状態管理の一元化

このアプリケーションでは、Stream と StreamController を使用してデータの流れを管理し、SQLite でデータを永続化しています。各レイヤー(Model、DAO、Repository、BLoC)が明確な責務を持ち、保守性の高いコードとなっています。

参照

https://vaygeth.medium.com/reactive-flutter-todo-app-using-bloc-design-pattern-b71e2434f692

Discussion