Deploy Angular Dart Tutorial to Firebaseでは、Angular DartのTOUR OF HEROESをFirebaseのDatabaseに置き換えました。最小限の修正で、チュートリアルを理解する目的だけならあれでもありかと思いますが、
- データが更新されてなくても、参照するたびにいちいちバックエンドに取りに行ってる。
- データが更新されても、リアルタイムに反映されない。
など、プロダクトとしてはありえないレベルだったため、改良してメモに残します。
あくまで問題なければの話ですが、firebase-dartを使用したリスト表示処理として横展開も可能だと思います。何かお気づきになられましたら、ご指摘いただけるとうれしいです。
データ更新
まず最初にリストデータをonce()で取得して、それ以降はchild_added/child_changed/child_removedイベントをリスニングしてデータを更新すればよいかと思ったのですが、、、
var e = await _fbRefHeroes.once('value');
e.snapshot.forEach((child) {
Hero hero = new Hero.fromJson(child.val());
_heroes.add(hero);
_idMax = max(hero.id, _idMax);
});
...
しかし、child_addedは初回だけ既存データも渡ってくる仕様で、once()でリストを初期化しておくと二重に処理を行うことになってしまうため、全てchildイベントでリストデータの追加・更新・削除を行うようにしました。
Future<Hero> getHero(int id) async =>
_heroes.firstWhere((hero) => hero.id == id);
Future<List<Hero>> getHeroes() async {
if (_heroes == null) {
try {
_heroes = [];
_idMax = 0;
_fbRefHeroes.onChildAdded.listen((e) {
Hero hero = new Hero.fromJson(e.snapshot.val());
_idMax = max(hero.id, _idMax);
_heroes.add(hero);
});
_fbRefHeroes.onChildRemoved.listen((e) {
Hero hero = new Hero.fromJson(e.snapshot.val());
_heroes.remove(_heroes.firstWhere((h) => h.id == hero.id));
});
_fbRefHeroes.onChildChanged.listen((e) {
Hero hero = new Hero.fromJson(e.snapshot.val());
_heroes
.firstWhere((h) => h.id == hero.id)
.name = hero.name;
});
} catch (e) {
throw _handleError(e);
}
}
return _heroes;
}
このあたりは、iOSやAndroidでのアプリ実装を考えれば、とくに違和感はないと思います。
更新通知
仕組みをよく理解していませんが、Angularは表示している参照データの更新を検知して再表示を走らせてくれるようなので、上記戻り値の参照を元にしているリスト表示は、これだけでリアルタイムに、追加・更新・削除が反映されるようになります。ただし、ダッシュボードでは、
Future<Null> ngOnInit() async {
heroes = (await _heroService.getHeroes()).skip(1).take(4).toList();
}
と、取得した参照データを加工した別の参照をもとに表示しているため、上記のようにchildイベントでリストの初期化も兼ねてしまうと、既存データの追加すら表示に反映されなくなってしまい、(まあ、そうでなかったとしても普通は)更新を伝播させる仕組みが必要となります。そこで、AngularDartとFirebase AuthenticationのAuthServiceを参考に、StreamControllerでコンポーネント側が更新を検出できる仕組みを組み込みました。
class HeroService {
...
StreamController<List<Hero>> _onChangedController;
...
Stream<List<Hero>> get onChanged => _onChangedController.stream;
...
HeroService() {
...
_onChangedController = new StreamController<List<Hero>>.broadcast();
}
...
Future<List<Hero>> getHeroes() async {
if (_heroes == null) {
try {
...
_fbRefHeroes.onChildAdded.listen((e) {
...
_onChangedController.add(_heroes);
});
_fbRefHeroes.onChildRemoved.listen((e) {
...
_onChangedController.add(_heroes);
});
_fbRefHeroes.onChildChanged.listen((e) {
...
_onChangedController.add(_heroes);
});
} catch (e) {
throw _handleError(e);
}
}
return _heroes;
}
Future<Null> ngOnInit() async {
...
_heroService.onChanged.listen((heroes) {
this.heroes = heroes.skip(1).take(4).toList();
});
}
HeroSearchServiceもHeroServiceを注入して書き換えます。
class HeroSearchService {
final HeroService _heroService;
HeroSearchService(@Inject(HeroService) this._heroService);
Future<List<Hero>> search(String term) async {
final regExp = new RegExp(term, caseSensitive: false);
List<Hero> heroes = await _heroService.getHeroes();
return heroes.where((hero) => hero.name.contains(regExp)).toList();
}
}
これで、バックエンドへのアクセスを抑え、自身およびバックエンドのデータ更新に対し、ダッシュボードとリスト表示がリアルタイムに更新されるようになっていると思います。