Chapter 10

10. StetaNotifierでプロバイダを構築しよう(todo編)

antman
antman
2021.12.04に更新

view modelを担当するフォルダを作る

  • view_model
    • todo
      • todo_provider.dart
    • timeline
      • timeline_provider.dart
        それぞれのdartファイルの役割は下記の通りです
  1. todo_provider.dart
    • DBへの読み込み、追加、削除などを行います。
    • DBへの操作が行われるたびに更新通知を送り、画面を再描画します。
  2. timeline_provider.dart
    • DBからデータをすべて読み込みviewで扱いやすいようにデータを変換します。
    • データの読み込みから変換までが終わると、更新通知を送り、画面を再描画します。

todo_provider.dartを記述しよう。

todo_provider.dart
import 'package:(パッケージ名)/model/freezed/todo_model.dart';
import 'package:(パッケージ名)/model/db/todo_db.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:drift/drift.dart';

class TodoDatabaseNotifier extends StateNotifier<TodoStateData> {
  //データベースの状態が変わるたびTodoのviewをビルドするようにするクラスです。
  TodoDatabaseNotifier() : super(TodoStateData());
  //ここからはデータベースに関する処理をこのクラスで行えるように記述します。
  final _db = MyDatabase();
  //書き込み処理部分
  writeData(TempTodoItemData data) async {
    if (data.title.isEmpty) {
      return;
    }
    TodoItemCompanion entry = TodoItemCompanion(
      title: Value(data.title),
      description: Value(data.description),
      limitDate: Value(data.limit),
      isNotify: Value(data.isNotify),
    );
    state = state.copyWith(isLoading: true);
    await _db.writeTodo(entry);
    readData();
    //書き込むたびにデータベースを読み込む
  }

  //削除処理部分
  deleteData(TodoItemData data) async {
    state = state.copyWith(isLoading: true);
    await _db.deleteTodo(data.id);
    readData();
    //削除するたびにデータベースを読み込む
  }

  //更新処理部分
  updateData(TodoItemData data) async {
    if (data.title.isEmpty) {
      return;
    }
    state = state.copyWith(isLoading: true);
    await _db.updateTodo(data);
    readData();
    //更新するたびにデータベースを読み込む
  }

  //データ読み込み処理
  readData() async {
    state = state.copyWith(isLoading: true);

    final todoItems = await _db.readAllTodoData();

    state = state.copyWith(
      isLoading: false,
      isReadyData: true,
      todoItems: todoItems,
    );
    //stateを更新します
    //freezedを使っているので、copyWithを使うことができます
    //これは、stateの中身をすべて更新する必要がありません。例えば
    //state.copyWith(isLoading: true)のように一つの値だけを更新することもできます。
    //複数の値を監視したい際、これはとても便利です。
  }
}

final todoDatabaseProvider = StateNotifierProvider((_) {
  TodoDatabaseNotifier notify = TodoDatabaseNotifier();
  notify.readData();
  //初期化処理
  return notify;
});

ではTodoDatabaseNotifierから解説していこうと思います。

class TodoDatabaseNotifier extends StateNotifier<TodoStateData> {
  //データベースの状態が変わるたびTodoのviewをビルドするようにするクラスです。
  TodoDatabaseNotifier() : super(TodoStateData());
  //ここからはデータベースに関する処理をこのクラスで行えるように記述します。
  final _db = MyDatabase();
  //書き込み処理部分
  writeData(TempTodoItemData data) async {
    if (data.title.isEmpty) {
      return;
    }
    TodoItemCompanion entry = TodoItemCompanion(
      title: Value(data.title),
      description: Value(data.description),
      limitDate: Value(data.limit),
      isNotify: Value(data.isNotify),
    );
    state = state.copyWith(isLoading: true);
    await _db.writeTodo(entry);
    readData();
    //書き込むたびにデータベースを読み込む
  }

  //削除処理部分
  deleteData(TodoItemData data) async {
    state = state.copyWith(isLoading: true);
    await _db.deleteTodo(data.id);
    readData();
    //削除するたびにデータベースを読み込む
  }

  //更新処理部分
  updateData(TodoItemData data) async {
    if (data.title.isEmpty) {
      return;
    }
    state = state.copyWith(isLoading: true);
    await _db.updateTodo(data);
    readData();
    //更新するたびにデータベースを読み込む
  }

  //データ読み込み処理
  readData() async {
    state = state.copyWith(isLoading: true);

    final todoItems = await _db.readAllTodoData();

    state = state.copyWith(
      isLoading: false,
      isReadyData: true,
      todoItems: todoItems,
    );
    //stateを更新します
    //freezedを使っているので、copyWithを使うことができます
    //これは、stateの中身をすべて更新する必要がありません。例えば
    //state.copyWith(isLoading: true)のように一つの値だけを更新することもできます。
    //複数の値を監視したい際、これはとても便利です。
  }
}

writeData関数ではTempTodoItemData型のインスタンス変数に格納されたtodoのデータを受取り、TodoItemCompanion型のインスタンス変数を作成します。このTodoItemCompanionはbuild_runnerがよしなに作ってくれたクラスで、primal keyであるidを自動的に割り振ってくれます。なのでid以外のデータをこのように与えてあげればいいわけです。

TodoItemCompanion entry = TodoItemCompanion(
      title: Value(data.title),
      description: Value(data.description),
      limitDate: Value(data.limit),
      isNotify: Value(data.isNotify),
    );

また、すべての関数の終わりにreadData関数を呼び出すようにしています。これはdbの操作が行われたあとにstateを更新するためです。

readData() async {
    state = state.copyWith(isLoading: true);

    final todoItems = await _db.readAllTodoData();

    state = state.copyWith(
      isLoading: false,
      isReadyData: true,
      todoItems: todoItems,
    );
    //stateを更新します
    //freezedを使っているので、copyWithを使うことができます
    //これは、stateの中身をすべて更新する必要がありません。例えば
    //state.copyWith(isLoading: true)のように一つの値だけを更新することもできます。
    //複数の値を監視したい際、これはとても便利です。
  }

このようにdbから読み込んだ最新のデータによって状態が更新されるので常に最新のデータを保持することができます。
残りの関数は特に目新しいものもないと思うので割愛します。

final todoDatabaseProvider = StateNotifierProvider((_) {
  TodoDatabaseNotifier notify = TodoDatabaseNotifier();
  notify.readData();
  //初期化処理
  return notify;
});

最終的にはStateNotifierProviderによってProviderとして扱えるようになります。
また、無名関数の中に処理を書くことによって初期化処理を行えるようになります。
今回はdbからすべてのデータを読み込んでいますね。
これにより、todoDatabaseProviderが保持しているtodoItemsは常に最新のものになります。
また、ProviderReferenceを使うことでproviderから別のproviderを参照することができます。
例えばaProviderというProviderがあるとすると、

final todoDatabaseProvider = StateNotifierProvider((ref) {
  final a=ref.watch(aProvider);
  TodoDatabaseNotifier notify = TodoDatabaseNotifier();
  notify.readData();
  //初期化処理
  return notify;
});

このように、Providerの無名関数の引数であるrefを使って参照を行えます。
またこの場合ref.watchを使用して参照しているのでaProviderの状態が変わるたびProviderが再生成されます。
再生成を避けたい場合はref.readを使うことで解決します。
しかしreadの説明文を読むと、
"If possible, avoid using [read] and prefer [watch], which is generally safer to use."
となっておりwatchのほうが作者に推奨されているようです。

まとめ

以上でtodo画面で使用するProviderの制作は完了です。
次回はtimeline画面で使用するProviderの制作を行います。