Riverpod を使用しているアプリで、ブロック機能を実装する


はじめに

この記事は Flutter Advent Calendar 2021 カレンダー 1 の 5 日目の記事です。 私がアプリを作成していたときにブロック機能の実装で躓いたので、そのポイントについて記述できればと思います。

ブロック機能について

Flutter でアプリ開発している方には、iOS と Android の両アプリを同時に作成できることをメリットに感じている方も多いと思います。そして、Google Play と App Store の両方に自分のアプリを出すことをモチベーションにしている方も多いのでは?と思っています(私もその一人です)。ただ、 App Store に提出するためには Apple のガイドライン に遵守し、 Apple の厳しい審査をパスしないといけません( Google Play については詳しくありません、すみません。)。そのガイドラインの項目の一つに、1.2 ユーザー生成コンテンツ という項目があります。この項目は、ユーザーが投稿できるアプリは必ずブロック機能や通報機能を実装しなければいけない、というものです。私が作成していたアプリではユーザーがコンテンツを投稿し他ユーザーがいいね!やお気に入りをする類のアプリであったため、この規約を守る必要がありました。

実装方針

本記事ではブロック機能に焦点を当てます。ブロック機能の実装方針については、以下の 2 パターンがあるかと思います。

  • ユーザーごとにブロックリストを持っており、バックエンドと通信することで登録・解除を行う。
  • 端末ごとにブロックリストを持っており、端末保存によって登録・解除を行う。

上記の中から、私は後者を選択しました。アプリの仕様が以下であり、前者は不可能だと考えたからです。

  1. 未ログインユーザーもいる
  2. 匿名ログインといった機能は実装しておらず、未ログインのユーザーはそれぞれを判別できない

アプリの構成

以下で実装します。

仕様パッケージ

設計

Clean Architecture を採用しています。
(インターフェイスや細かい別の処理は省略)

ArticleListUseCase は BlockRepository を持っており、 ブロック一覧リストが Stream で流れてくる getListStream メソッドを呼ぶことができます。そして流れてきたブロック一覧をもとにプロパティの記事一覧をフィルターし、自身が持っている Stream にイベントを流します。

実装

上記の図の BlockRepository を実装していきます。StreamingSharedPreferences を使用すると、以下のようになります。

block_repository_impl.dart
////////// エラーになるコード /////////////

final blockRepositoryProvider = Provider.autoDispose<BlockRepository>(
  (_) {
    return BlockRepositoryImpl();
  },
);

class BlockRepositoryImpl implements BlockRepository {
  BlockRepositoryImpl() {
    Future(() async {
      _instance = await StreamingSharedPreferences.instance;
    });
  }

  final StreamingSharedPreferences _instance;

  @override
  Stream<List<String>> getListStream() {
    return _instance.getStringList('articles', defaultValue: []);
  }
}

しかし、このままでは getListStream() を呼ぶと実行時にエラーになってしまいます。原因は StreamingSharedPreferences のインスタンスを取得するメソッドが非同期であるため、 getListStream が呼ばれたタイミングではまだインスタンスがなく Stream を返せなかったためです。これを解消するためには、 Riverpod で提供されている override 機能を用います。

まずは以下の Provider を実装します。

streaming_shared_preferences.dart
final sharedPreferencesProvider =
    Provider<StreamingSharedPreferences>((_) => throw UnimplementedError());

ここではエラーを投げるようにします。Provider 内でインスタンスを生成する方針だと FutureProvider を利用する必要があり、非同期処理と変わらなくなってしまうためです。

上記の Provider の中身は、main 文の中で上書きします。

main.dart
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final streamSharedPreference = await StreamingSharedPreferences.instance;

  runApp(
    ProviderScope(
      overrides: [
        sharedPreferencesProvider.overrideWithValue(
          streamSharedPreference,
        ),
      ],
      child: App(),
    ),
  );
}

これにより sharedPreferencesProvider から StreamingSharedPreferences を取得できるようになりました。 BlockRepository 内のインスタンス生成部分を書き換えます。

block_repository_impl.dart
final blockRepositoryProvider = Provider.autoDispose<BlockRepository>(
  (ref) {
    final blockRepository = BlockRepositoryImpl(
      ref.watch(sharedPreferencesProvider),
    );
    return blockRepository;
  },
);

class BlockRepositoryImpl implements BlockRepository {
  BlockRepositoryImpl(
    this._instance,
  );

  final StreamingSharedPreferences _instance;

  @override
  Stream<List<String>> getListStream() {
    return _instance.getStringList('articles', defaultValue: []);
  }
}

まとめ

アプリを作成する際に必須となるブロック機能について、Riverpod + StreamingSharedPreferences で実装する方法について記述しました。他にも、強制アップデートの仕組みを実装する際に使われることが多い PackageInfo など、インスタンス取得が非同期のものについては同じ手法を使えます。同じように Riverpod で非同期のインスタンス生成をする必要がある方の助けになりましたら幸いです。

以上で 5 日目の記事を終了します。ありがとうございました!