19
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

前提

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 アーキテクチャーで構築されたアプリ上で表現する方法を実装しました。

19
14
2

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
19
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?