前提
firestore から取得可能なデータは Stream として表現をすることができて、データの変更があるとすぐに Stream を通じて更新が通知されます。この Stream を繋ぎっぱなしにしておくことでアプリではデータが更新されたらすぐに UI の更新をすることが可能です。
また、 flutter では bloc パターンを実現するライブラリとして felangel/bloc が提供されています。bloc は UI 層に対して Stream を通じて State を通知して UI は Widget の更新を行います。
やったこと
リポジトリーの実装
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 を作ることができます。
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 の実装
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 アーキテクチャーで構築されたアプリ上で表現する方法を実装しました。