LoginSignup
37
24

More than 3 years have passed since last update.

RiverpodでFirestoreとFirebase AuthをStreamProviderで組み合わせる

Last updated at Posted at 2020-12-26

この記事は

先日書いた「RiverpodのStreamProviderをモック化して、依存するProviderを単体テストする」という記事で、RiverpodStreamProviderCloud Firestoreリアルタイムアップデートを使う方法を解説しました。しかし、Firebase Authenticationと組み合わせて、Firestoreのユーザ別の領域にアクセスするケースに対応した方法の紹介までは行えていませんでした。
この記事ではFirestoreのデータとFirebase Authenticationの認証状態を両方とも監視して、リアルタイムに表示に反映させる方法を解説します。さらにデータと認証状態を監視しているStreamProviderをモック化して、それを使って表示の制御を行っているProviderの単体テストも行います。想定しているアーキテクチャは前回の記事のこちらをご参照ください。

サンプルアプリ

この記事で解説したい内容をサンプルアプリで説明します。

ある動物園が展示している動物一覧を提供しています(動物はkotlin-fakerで作成)。Sign in with GitHub ボタンでGitHubにログインすると、その人が付けた「いいね!」がリスト表示に適用されます。右上のログアウトボタンを押すと、最初の表示に戻ります。

データ

動物一覧はFirestoreでこのようなデータになっています。

動物

動物はanimalsコレクションのドキュメントに格納します。ドキュメントにはnameフィールドがあります。

スクリーンショット 2020-12-26 20.31.24.png

いいね!

いいね!はusersコレクションにUser IDと同名のドキュメントを作り、そこにlikesコレクションを作成し、いいね!した動物を参照するドキュメントを作成します。
animalIdフィールドがanimalsコレクションのドキュメントのIDです。

※ このスクリーンショットではUser IdをdummyUserとしています。

スクリーンショット 2020-12-26 20.35.40.png

サンプルコード

今回の記事のサンプルコードです。Githubアカウントでログインできますが、簡単のため、全員ダミーユーザでログインした動作になります。

tfandkusu/observe_firestore_with_auth

参考文献

この記事とサンプルアプリは、こちらのイギリスのGoogle Developer Expertの人が作成したサンプルコードを参考にして作成しました。

bizz84/starter_architecture_flutter_firebase

StreamProviderを作成する

動物一覧のStreamProviderです。

animal_repository.dart
final animalListStreamProvider = StreamProvider.autoDispose((_) {
  CollectionReference ref = FirebaseFirestore.instance.collection('animals');
  return ref.snapshots().map((snapshot) {
    final list = snapshot.docs
        .map((doc) => Animal(doc.id, doc.data()['name']))
        .toList();
    list.sort((a, b) => a.name.compareTo(b.name));
    return list;
  });
});

認証状態を取得するためのStreamProviderです。

user_repository.dart
/// 認証状態プロバイダー
final authStreamProvider = StreamProvider.autoDispose((_) {
  return FirebaseAuth.instance.authStateChanges().map((user) => user != null);
});

/// UserIdプロバイダー
final userIdStreamProvider = StreamProvider.autoDispose((_) {
  return FirebaseAuth.instance.authStateChanges().map((user) {
    if (user != null) {
      return user.uid;
    } else {
      return null;
    }
  });
});

いいね!のStreamProviderです。いいね!が付いている動物のドキュメントIDセットを流します。

like_repository.dart
final likeSetStreamProvider = StreamProvider.autoDispose((ref) {
  final userIdAsyncValue = ref.watch(userIdStreamProvider);
  var userId = userIdAsyncValue?.data?.value;
  if (userId != null) {
    CollectionReference cref =
        FirebaseFirestore.instance.collection('/users/$userId/likes');
    return cref.snapshots().map((snapshot) =>
        snapshot.docs.map((doc) => doc.data()['animalId']).toSet());
  } else {
    return Stream.value(Set<String>());
  }
});

いいね!のStreamProviderが提供するStreamはuserIdに依存します。そしてuserIdもStreamProviderが提供するStreamから流れてくるものです。当初、別のStreamProviderの最新の値に依存するStreamProviderの作り方が分からなかったですが、bizz84/starter_architecture_flutter_firebaseのおかげで分かりました。

表示を制御するためのProviderを作成する

Presenter層に相当するProviderの作り方は前回と同様です。

main_provider.dart
/// ログアウトボタン表示Provider
final logoutProvider = Provider.autoDispose((ref) async {
  // 認証状態ならば表示
  final futureAuth = ref.watch(authStreamProvider.last);
  return await futureAuth;
});

/// 動物といいね!のリスト表示Provider
final mainUiModelProvider = Provider.autoDispose((ref) async {
  // 動物リスト
  final futureAnimalList = ref.watch(animalListStreamProvider.last);
  // いいね!セット
  final futureLikeSet = ref.watch(likeSetStreamProvider.last);
  // 認証状態
  final futureAuth = ref.watch(authStreamProvider.last);
  // Futureで包まれているので、待つ。
  final animalList = await futureAnimalList;
  final likeSet = await futureLikeSet;
  final auth = await futureAuth;
  // リスト表示する項目を作成する
  final items = animalList
      .map((animal) => MainListItem(animal.name, likeSet.contains(animal.id)))
      .toList();
  return MainUiModel(items, !auth /* 認証していないならばログインボタンを表示 */);
});

画面を作成する

前節で紹介したProviderを使うWidgetの構築は、この記事の主題から外れコードが長いのでこちらにリンクを設置します。

main.dart

単体テスト

前回の記事同様に、StreamProviderをモック化して依存するProviderを単体テストすることができます。

main_presenter_test.dart
void main() {
  // ログインしていない時のリスト表示を確認
  test('List animals without login', () async {
    final animalList = [
      Animal('1', 'alligator'),
      Animal('2', 'alpaca'),
      Animal('3', 'ape'),
      Animal('4', 'cat')
    ];
    final container = ProviderContainer(overrides: [
      animalListStreamProvider.overrideWithValue(AsyncValue.data(animalList)),
      authStreamProvider.overrideWithValue(AsyncValue.data(false)),
      userIdStreamProvider.overrideWithValue(AsyncValue.data(null))
    ]);
    final futureMainUiModel = container.read(mainUiModelProvider);
    final mainUiModel = await futureMainUiModel;
    // ログインボタンを表示する
    expect(mainUiModel.showAuth, true);
    // いいねはどの動物にもついていない
    expect(mainUiModel.items, [
      MainListItem('alligator', false),
      MainListItem('alpaca', false),
      MainListItem('ape', false),
      MainListItem('cat', false)
    ]);
  });

  // ログインしている時のリスト表示を確認
  test('List animals without login', () async {
    final animalList = [
      Animal('1', 'alligator'),
      Animal('2', 'alpaca'),
      Animal('3', 'ape'),
      Animal('4', 'cat')
    ];
    // idが2であるalpacaと4であるcatにいいねを付けている。
    final likeSet = Set.from(['2', '4']);
    final container = ProviderContainer(overrides: [
      animalListStreamProvider.overrideWithValue(AsyncValue.data(animalList)),
      likeSetStreamProvider.overrideWithValue(AsyncValue.data(likeSet)),
      authStreamProvider.overrideWithValue(AsyncValue.data(true))
    ]);
    final futureMainUiModel = container.read(mainUiModelProvider);
    final mainUiModel = await futureMainUiModel;
    // ログインボタンは表示しない
    expect(mainUiModel.showAuth, false);
    // alpacaとcatにいいねがついている
    expect(mainUiModel.items, [
      MainListItem('alligator', false),
      MainListItem('alpaca', true),
      MainListItem('ape', false),
      MainListItem('cat', true)
    ]);
  });
  // ログインしているときはログアウトボタンを表示する
  test('Show logout button', () async {
    final container = ProviderContainer(overrides: [
      authStreamProvider.overrideWithValue(AsyncValue.data(true))
    ]);
    final futureLogout = container.read(logoutProvider);
    final logout = await futureLogout;
    expect(logout, true);
  });
  // ログインしていない時はログアウトボタンを表示しない。
  test('Hide logout button', () async {
    final container = ProviderContainer(overrides: [
      authStreamProvider.overrideWithValue(AsyncValue.data(false))
    ]);
    final futureLogout = container.read(logoutProvider);
    final logout = await futureLogout;
    expect(logout, false);
  });
}
37
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37
24