YOUTRUSTアプリを支えるデータストアの技術 〜改良版〜

どうも、株式会社YOUTRUSTのアプリ開発のリードエンジニアをやっているashdikこと朝日(YOUTRUST / X)です。

最近は、以前お話ししたTHE BLACKLISTを見終わり、 2周目に突入しました笑
さすがに長すぎるので1.5倍速で見てるのですが、「こんな人いたなぁ」「本当は、悪いやつなのに全然悪い顔してないなぁ」なんて思いながら見ててこれはこれで楽しいです。

そして、話は変わり、個人所有のボードゲームは150個を超え、そろそろ店開きなよと言われる事が多くなってきましたw 最近のお気に入りはIT'S A WONDERFUL WORLDです。

⚓️ 概要

以前、YOUTRUSTアプリを支えるデータストアの技術という記事を公開しました。

が、当時は StateProvider という将来非推奨となるAPIを使っての実装となっておりました。

今ではその実装を riverpod annotation を使った実装へと移行していっています。
本記事では、新しい機構での Store をご紹介できればと思っております。

何気に、おまけが一番有用な情報かもなので最後まで読んでいってくださいね笑

💻 筆者環境 (執筆時)

name version
Flutter 3.19.5
Dart 3.3.3
flutter_hooks 0.20.4
hooks_riverpod 2.5.1

変更点

変更点を一言でまとめると、「ProviderStateProvider@riverpod に変更した」となります笑
ご想像通りかとは思いますが、前回の例を使って解説していきたいと思います。

実装

Before

/// 公開用
/// 指定したTodoIdを持つTodoインスタンスを返す
final todoProvider = Provider.family<Todo?, int>(
  (ref, todoId) => ref.watch(_todosStateProvider)[todoId],
);

/// 公開用
/// 管理している全Todoを返す
final todosProvider = Provider(
  (ref) => ref.watch(_todosStateProvider),
);

/// 内部用
/// 内部で管理するためのProvider
final _todosStateProvider = StateProvider<List<Todo>?>(
  (ref) => null;
);

class TodoStore {
  const TodoStore._();

  /// 保持している全てのTodoを返す
  static List<Todo>? getAll({
    required Reader read,
  }) {
    return read(todosProvider);
  }

  /// 保持している指定されたtodoIdを持つTodoを返す
  /// なければnull
  static Todo? get({
    required Reader read,
    required int todoId,
  }) {
    return read(todoProvider(todoId));
  }

  /// 保持している配列の先頭に指定された配列を追加する
  static void prepend({
    required Reader read,
    required List<Todo> toods,
  }) {
    read(_todosStateProvider.notifier).update(
      (oldState) => // todos + oldState から重複を排除
    );
  }  

  /// 保持している配列の末尾に指定された配列を追加する
  static void append({
    required Reader read,
    required List<Todo> todos,
  }) {
    read(_todosStateProvider.notifier).update(
      (oldState) => // oldState + todos から重複を排除
    );
  }

  // 保持している配列を置換する
  static void replace({
    required Reader read,
    required List<Todo> todos,
  }) {
    read(_todosStateProvider.notifier).update(
      (_) => todos,
    );
  }
}

After

@Riverpod(keepAlive: true)
class TodoStore extends _$TodoStore {
  @override
  List<Todo>? build() {
    return null;
  }

  /// 保持している全てのTodoを返す (getAll相当)
  List<Todo>? get todos => state;

  /// 保持している全てのTodoを設定する (replace相当)
  set todos(List<Todo>? todos) {
    state = todos;
  }

  /// 保持している指定されたtodoIdを持つTodoを返す
  /// なければnull
  Todo? get({
    required int todoId,
  }) {
    return (todos ?? []).firstWhereOrNull((e) => e.id == todoId);
  }

  /// 保持している配列の先頭に指定された配列を追加する
  static void prepend({
    required List<Todo> todos,
  }) {
    // 重複排除は省略
    state = [
      ...todos,
      ...?state,
    ];
  }  

  /// 保持している配列の末尾に指定された配列を追加する
  static void append({
    required List<Todo> todos,
  }) {
    // 重複排除は省略
    state = [
      ...?state,
      ...todos,
    ];
  }
}

provider 達がいなくなったこともありますが、だいぶ行数が削れましたね。

利用 (View)

Before

@override
Widget build(BuildContext context, WidgetRef ref) {
  final todos = ref.watch(todosProvider);
  ...
}

After

@override
Widget build(BuildContext context, WidgetRef ref) {
  final todos = ref.watch(todosStoreProvider);
  ...
}

利用 (Facade)

以前、ご紹介したFacadeレイヤーからの呼び出しは以下の様になっています。

Before

class TodoFacade {
  static Future<void> fetch({
    required Reader read,
  }) async {
    final response = // 通信により取得

    TodoStore.replace(
      read: read,
      todos: response,
    );
  }
}

After

class TodoFacade {
  static Future<void> fetch({
    required Reader read,
  }) async {
    final response = // 通信により取得

    read(todoStoreProvider.notifier).todos = response;
  }
}

おまけ

Notifier の呼び出し部分がちょっと長い上に冗長的だったりするので、以下の様なエイリアスを作っていたりします。

static TodoStore _todoStore(Reader read) {
  return read(todoStoreProvider.notifier);
}

これにより、

read(todoStoreProvider.notifier).todos = response;

が、

_todoStore(read).todos = response;

となり少し楽になったりします。

同時に、テストでは以下のエイリアスを作っています。

extension on ProviderContainer {
  TodoStore get todoStore => read(
        todoStoreProvider.notifier,
  );
}

これにより、

container.read(todoStoreProvider.notifier).replace(...);

が、

container.todoStore.replace(...);

となりこれまた少し記述が楽になったりします。

🎥 最後に

いかがでしたか?
以前の記事を参照しながらブログを書いていると、あぁ書いていて良かったなぁと思うのは僕だけでしょうか笑

エンジニア募集中っ!

  • このシリーズを読んで、実際に触ってみたい!と思っていただけた方
  • 純粋にYOUTRUSTに興味がある方

YOUTRUSTでは、エンジニアを募集しています! ぜひ、下のリンクよりご応募お待ちしておりまっす!

herp.careers