flutter_bloc を使って firestore のデータを表示する

## 前提

firestore から取得可能なデータは Stream として表現をすることができて、データの変更があるとすぐに Stream を通じて更新が通知されます。この Stream を繋ぎっぱなしにしておくことでアプリではデータが更新されたらすぐに UI の更新をすることが可能です。

また、 flutter では bloc パターンを実現するライブラリとして [felangel/bloc](https://github.com/felangel/bloc) が提供されています。bloc は UI 層に対して Stream を通じて State を通知して UI は Widget の更新を行います。

## やったこと

### リポジトリーの実装

```dart
import 'dart:async';

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:meta/meta.dart';

// firestore の Document に対応するデータ
class User { ... }

// リポジトリーの実装
class UserRepository {
  final Firestore firestore;

  UserRepository({@required this.firestore})
    : assert(firestore != null);

  // firestore から取得をしたデータのストリームを外部に公開するための Stream
  final StreamController<User> _userStream = StreamController();
  Stream<User> get user => _userStream.stream;

  // firestore からデータを取得する。結果は user から取得する。
  void fetch(String token) {
     firestore
      .collection('users')
      .where('toke', isEqualTo: token)
      .snapshots()
      .listen((query) {
        query.documents.map(_mapDocumentToUser).forEach(_userStream.add);
      });
  }

  // DocumentSnapshot の map から User を生成する
  User _mapDocumentToUser(DocumentSnapshot doc) { ... }
}
```

リポジトリーはアプリケーション内で一つのインスタンスとなります。外部に公開している `user` を参照することで、仮にユーザー情報を表示する箇所がアプリ内に複数存在しても常に最新のデータの表示が可能です。firestore へのアクセス用に `fetch` を用意しました。firestore から取得をしたデータは `user` へ通知されるので、画面に表示をするためには `user` を購読する必要があります。

### Bloc の実装

UI 層とリポジトリーをつなぐために Bloc を用意します。Bloc はリポジトリーから取得したストリームをそのまま UI に渡します。UI は受け取ったストリームを利用して Widget を作ることができます。


```dart
import 'dart:async';

import 'package:app/repositories/repositories.dart';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';

abstract class UserEvent extends Equatable {
  AccountEvent([List props = const []]) : super(props);
}

class FetchUser extends UserEvent {}

abstract class UserState extends Equatable {
  AccountState([List props = const []]) : super(props);
}

class UsertNotLoaded extends UserState {}

class UserLoading extends UserState {}


// リポジトリーから取得したストリームを UI に伝えるための State
class UserStream extends UserState {
  final Stream<User> userStream;

  UserStream({@required this.userStream}) : assert(userStream != null);
}

class UserBloc extends Bloc<UserState, UserEvent> {

  final UserRepository userRepository;

  UserBloc({@required this.userRepository): assert(userRepository != null);

  @Override 
  UserState get initialState => UserNotLoaded();

  @Override
  Stream<UserState> mapEventToState(UserEvent event) async* {
    if (event is FetchUser) {
      yield UserLoading();
      userRepository.fetch();
      // UI は firestore から取得した Stream を参照して Widget を構築する
      yield UserStream(userStream: userRepository.user);
    }
  }
}
```

Bloc では `UserStream` を利用して UI に firestore の Stream を伝えます。UI が発行する `FetchUser` をトリガーに firestore へのクエリーが実行されますが、一度 Stream を取得することで firestore の更新を自動的に追従できるようになります。

今回は `User` というデータなのでイメージがつきにくいかもしれないですが、別のクライアントから自分のユーザー情報を操作した場合でも、アプリ利用者にはすぐに変更が反映されることが期待されます。

### UI の実装

```dart
import 'package:app/bloc/bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class UserWidget extends StatefulWidget {

  @Override
  _UserWidgetState createState() => _UserWidgetState();

}

class _UserWidgetState extends State<UserWidget> {

  @override
  Widget build(BuildContext context) {
    var bloc = BlocProvider.of<UserBloc>(); // 事前に BlocProviderTree を作っておく
    // State の変更によって StreamBuilder が再生成されることを防ぐためキャッシュする
    StreamBuilder<User> userWidget;
    assert(bloc != null);
    return BlocBuilder(
      bloc: bloc,
      builder: (BuildContext context, UserState state) {
        if (state is UserNotLoaded) { 
          bloc.dispatch(FetchUser());
          return CircularProgressIndicator();
        }
        if (state is UserStream) {
          if (userStream == null) {
             // firestore の Stream を表示するため StreamBuilder を生成する
             userStream = StreamBuilder(
               stream: state.userStream,
               builder: (BuildContext context, AsyncSnapshot<User> snapshot) {
                 switch (snapshot. connectionState) {
                   case ConnectionState.waiting:
                     return CircularProgressIndicator();
                   default:
                     if (snapshot.data == null) {
                       // bloc.dispatch(CureateUser()); TODO
                       // ユーザーを firestore に作る
                       return Text('no user');
                     } else {
                       return Text('user ${user.name}');
                     }
                 }
               }
             )
          }
          return userStream;
        }
      }
  }
```

UI では `StreamBuilder` を使って Widget を作ります。 `StreamBuilder` を使うことで firestore の変更にリアルタイムで追従できるようになるようです。このとき、 bloc の State が更新されて Widget の再生性が行われる際、 `StreamBuilder` が何度も生成されてしまうと firestore -> bloc -> ui -> bloc -> firestore のデータの流れが無限ループに陥る可能性があります。そのため、 `StreamBuilder` をキャッシュして再生成を防ぎます。

## まとめ

firestore から取得した Stream を bloc アーキテクチャーで構築されたアプリ上で表現する方法を実装しました。