CloudFirestoreと仲良くなるために調べたこと
Firebaseを使ってアプリを作る上で、CloudFirestoreと仲良くなるべくNo SQLの調べてみた。その一環でNoSQLデータモデリング技法とRDB技術者のためのNoSQLガイドを読んだので備忘録を残しておく。
それぞれを簡単に紹介すると
NoSQLデータモデリング技法 · GitHub NoSQLのデータモデリングテクニックを解説している
RDB技術者のためのNoSQLガイド NoSQLデータベースの特性を解説している
それぞれが補間する形で知識の穴を埋められたように思う。
CloudFirestoreはどんなDB?
アプリ開発で採用される機会も増えてきたと思うが、特徴は以下に記載したものになる。
- ドキュメント指向のDB。(Mongo DBやCouchbaseに近い)
- Firebaseのクラウド機能のためスケーラブル
- リアルタイム更新やオフラインサポートなど機能が豊富
- Web / Sever / Mobileなどカバー範囲が広い
Flutterと組み合わせると、ひとつのコードでiOS / Android / Webのアプリを作ることも可能なため、素早く継続的な改善が必要な昨今の開発事情にはマッチしているように思う。
NoSQLが必要になってきた理由
昨今の開発でNoSQLが求められている理由を整理しておく。 書籍には↓のように紹介されている
- 大規模なデータを扱うことが求められている
- 高速にデータを処理することが求められている
- 扱うデータに多様性が生まれてきた
NoSQLとRDBの違い
一番しっくりときたのは
- 自分はどの答えを持っているか?
- 自分は何を知りたいのか?
というテーマの違いで、Flutterでアプリ開発する場合は、自分≒アプリケーションを利用するユーザーは何を知りたいのか?を意識してモデリングすることがキモになると思った。 NoSQLはRDBを置き換えるものではなく、用途に応じて使い分けるものだということだった。
今はToDoアプリに近いアプリを作っていく予定だが、日毎の達成率を知りたいときにどのようにデータモデリングするか悩ましいと感じた。RDBだとtodo_historyとtodo_listを別テーブルで作成して達成率を計算する方法がありそうだが、NoSQL(DocmentDB)の場合はどのようにモデリングするのが最適なのだろうか。
NoSQLデータモデリングの基本的原則
NoSQLデータモデリング技法 · GitHubでは17個のテクニックが紹介されている。 NoSQLは非正規化が基本になっているようで、正規化が基本のRDBとは真逆なのが新鮮に感じた。今回はCloudFirestoreを使ったアプリを作る上での知識をつけることが目的なのでDocmentDBに関連するものを集中的に調べれば良さそうだった。
DocmentDBに関連するもの
1. 非正規化(Denormalization)
2.集計(Aggregates)
3.アプリケーションサイドJoin (Application Side Join)
4.アトミックな集計(Atomic Aggregate)
6.次元削減(Dimensionality Reuction)
10.転置検索
11.ツリー集計(Tree Aggregation)
12.近接リスト(Adjacency Lists)
13.マテリアライズドパス (Materialized Paths)
14.ネストされたセット(Nested Sets)
16.ネストされたドキュメントの平準化:近傍クエリ(Nested Documents Flattening: Proximity Queries)
17.バッチグラフ処理
この辺りの知識を意識して、身につけていくのが良さそうに感じた。
まとめ
FlutterプロジェクトにRenovateを導入する
FlutterプロジェクトにRenovateを導入したので備忘録として
1. 対象リポジトリにRenovateのGithub Appsを導入
手順に従って進めれば設定完了できるので詳細は割愛。
2. renovavte設定用のPRが生成される
GitHub Appsが連携されると画像のようなPRが生成される。
renovate.jsonという設定ファイルを更新するとチューニングが可能。 Renovate Docsのページを見ながら進めると良い。 設定項目によってはプラットフォーム未対応だったりや不要な項目もある。
3. 設定をチューニング
今回、設定した内容はこちら
"extends": [ "config:base" ], "timezone": "Asia/Tokyo", "lockFileMaintenance": { "enabled": true }, "labels": ["dependencies"] }
config:baseの内容
Renovateに用意されているPresetsで中身は以下の設定になっていた。
dependencyDashboard
v26.0.0から導入された新機能。 Github内にIssueが作られてRenovateで生成されたPRが可視化されるらしいが、試したプロジェクト上では動いていないっぽい。
semanticPrefixFixDepsChoreOthers
CommitがSemanticPrefixのFixとChoreだと引き継いでくれるってことかな?良くわからなかった。
ignoreModulesAndTests
モッジュールやテストのディレクトリのRenovateの管理対象外にする設定
autodetectPinVersions
バージョンの固定や範囲指定を維持する設定
prHourlyLimit2
PRを1時間に2つまでにする設定
prConcurrentLimit20
同時に開くPRは20までにする設定
group:monorepos
複数のpackageをまとめて依存関係の更新を管理する設定を引き継ぐ利用する Monorepo Presetsで対象の一覧がみれる
group:recommended
monorepoでないpagakeの複数のpackageをまとめて依存関係の更新を管理する設定を利用する
Group Presetsで対象の一覧がみれる
workarounds:all
ワークアラウンドの設定を引き継ぐ Workaround Presetsで対象の一覧が見れる
追加した設定
timezone
日本時間でRenovateを動作させる
lockFileMaintenance
lockfileのメンテナンス設定をしないと依存先の更新が合わせてされないので、手間がかかるらしい dartでは2021/08の時点では、まだ未対応ですが、対応が進んでいる様子。 feat: Add pub lockfile maintenance by jld3103 · Pull Request #11132 · renovatebot/renovate · GitHub
labels
PRに指定したラベルをつける。この場合はdependencies。
{ "extends": [ ":dependencyDashboard", ":semanticPrefixFixDepsChoreOthers", ":ignoreModulesAndTests", ":autodetectPinVersions", ":prHourlyLimit2", ":prConcurrentLimit20", "group:monorepos", "group:recommended", "workarounds:all" ] }
4. PRが飛んでくる
DartパッケージのPR
AndroidパッケージのPR
所感
DartとAndroidとiOSの依存関係を管理する必要があるFlutterプロジェクトでは導入しておいて損はなさそう。
MVVM構成でStreamを扱う時に試したこと
Flutterの勉強をかねてflutter-architecture-blueprintsを参考にアプリの実装を進めているが、Streamの扱いをMVVMでどう実装すると良いのか迷ったので、書いておく。
MVVMへのStreamの追加
先述のレポジトリではMVVMの状態管理とDIにriverpodを利用している。 それとflutter_hooksやChangeNotifierを組み合わせてMVVMを実装している。 ※詳しくは下記で本人が解説してくれている記事を参照よければそちらを・・・!。
この構成を参考は下記のような実装を試していた。
View
// useProviderで取得。変更時にリビルドされる。 final goods = useProvider(goodsViewModelProvider.select((value) => value.goods)); useMemoized(() { goodsViewModel.subscribeGoods(); });
ViewModel
// ProviderからViewModelを生成 final goodsViewModelProvider = ChangeNotifierProvider((ref) => GoodsViewModel(ref.read(goodsRepositoryProvider))); class GoodsViewModel extends ChangeNotifier { GoodsViewModel(this._repository); final GoodsRepository _repository; List<Goods> goods; StreamSubscription disposable; void subscribeGoods() { disposable = _repository.getGoods() .listen((value) { goods = value; notifyListeners(); }); } @override void dispose() { super.dispose(); // 不要になったらStreamを開放する disposable.cancel(); } }
Repository
class GoodsRepositoryImpl implements GoodsRepository { //変更を即時反映するためにStreamを返す @override Stream<List<Goods>> getGoods() { CollectionReference ref = FirebaseFirestore.instance .collection("goods"); return ref.snapshots().map((snapshot) => snapshot.docs .map((docs) => docs.data()) .map((data) => Goods(data['id'], data['name']) ).toList() ); } }
この設計だと状態の変更は以下の流れでViewに伝わる。
- RepositoryでFirestoreのデータ変更時にStreamを流れる
- ViewModelでRepositoryを講読しgoodsに、notifyListeners()でViewに通知
- ViewModelのgoodsを監視して、変更があった場合にリビルドする
- 不要になった場合にViewModelのdisposeで開放をする
しかし、ViewModelでStreamをdisposeする方法では、コードの追加漏れで開放されないミスをしそうに感じていた。
StreamProvierを利用したアプローチ
調べてみたところ開放ミスの防止はStreamProviderとaudoDisposeでシンプルに実現できそうだったので修正を加えてみた。
View
final goodsStream = useProvider(goodsViewModelProvider(firebaseUser.uid).select((value) => value.goods)); // goodsのストリームを講読する final goods = useStream(useMemoized(() => goodsStream, [goodsStream.last.toString()]), initialData: []); if (goods != null && goods.data.isNotEmpty) { return RefreshIndicator( onRefresh: () async => {}, child: ListView.builder( itemCount: goods.data.length, padding: const EdgeInsets.only(top: 10.0), itemBuilder: (context, index) => _buildListItem(context, goods.data[index]), ), ); } else { return Text('Error Screen: '); } },
ViewModel
// Firestoreのデータに変更があった場にStreamが流れるようにwatchする final goodsViewModelProvider = AutoDisposeProvider( (ref) => GoodsViewModel(ref.watch(goodsStreamProvider.stream)) ); class GoodsViewModel extends ChangeNotifier { GoodsViewModel(this.goods); final Stream<List<Goods>> goods; }
Repository -> Provider
// AutoDisposeStreamProviderを使うと使われなると自動的に破棄される final goodsStreamProvider = AutoDisposeStreamProvider((ref) { CollectionReference ref = FirebaseFirestore.instance .collection("goods") return ref.snapshots().map((snapshot) => snapshot.docs .map((docs) => docs.data()) .map((data) => Goods(data['id'], data['name'], data['date'], data['user']) ).toList() ); });
このアプローチだと状態の変更は以下の流れになった。
- goodsStreamProviderでデータに変更時にStreamを流す
- ViewModelのgoodsでgoodsStreamProvidernのStreamをもつ
- ViewModelのgoodsを監視して、変更があった場合にリビルドする
- 不要になると自動で開放される
このアプローチだとStreamの開放もれを防ぐことは実現できたように思う。 反面、Providerがデータの変更を直接管理するため、Repositoryクラスにデータ取得ロジックの集約がしづらくなる面もありそうだと感じた。
引き続き良い方法を探していきたい。
Flutterを学ぶためにやったこと
細かいことを気にせずかける場が欲しくなったのでブログを開設してみた。
今回は半年間取り組んできたFlutterのキャッチアップについて備忘録的に書いておこうと思う。
動機とか
学び始めた主な理由は2つ。
- 作りたいアプリがある
- 所属会社の目標の一つ
というもの。半年でアプリのMVPまで作りきるところまで計画はしていたが、進捗が伸びず、アーキテクチャを1つ実践する方針に方向転換した。
Step1 公式ページを読む
やはり最初にあたるのは公式からと言うことで、Flutter documentation - Flutterやら
Dart documentation | Dart を読みといていた。今思えばTutorials - Flutterも一緒にやっておけばよかった。
Step2 Flutter×Firebaseの本を写経
Firebaseを使ったFlutterのアプリの作り方を身につけたかったので、下記の本のサンプルアプリを写経した。
サンプルアプリを作りながらStep by Stepで学んでいくスタンスの本だったので、この本を選んでよかったと感じている。次はzenn.devで見つけた本にも取り組んでみたいと思う。
Step3 MVVMにチャレンジ
より実践的なスキルを身につけたかったので、wasabeefさんが公開しているコードを元にサンプルアプリを作り変えてみた。
素早く実践を積むために、既に他プラットフォームで経験のあるMVVMを選んだ。下記の記事で内容の解説もしてくれており非常に参考になった。
このサンプルではMVVMを実装するために複数のライブラリを組み合わせているが、特にRiverpodが便利だったので、別途所感を描こうと思う。
取り組みの仕方について
所属会社の目標も絡んでいたのと、プライベートの状況的に気を抜くと時間を取れないと思ったので、社内でもくもく会を開催しながら進めていた。隔週1時間ではじめたが、時間が足りなくなったので、後半は毎週1時間にした。ペース配分は今後も工夫の必要がありそう。
もくもく会ではけんすうさんの記事を参考に作業中の画面を画面共有しながら進めてみた。
誰かにみられるかもと感じるとサボる気が起きにくくて、捗ったので今後も続けていこうと思う。
感想や反省など
Flutterについて
こんなに簡単にListつくれちゃっていいの!?という衝撃が1番大きかった。
AndroidやiOSでは結構コード量が多くなりがちで苦労していることもあり、Flutterが好かれている理由を垣間見た感じがした。
あとはUIウィジェットでガリガリUIを各感覚が新鮮で、通常のコードとレイアウトコードで脳味噌のスイッチがないとかなりストレスが減るもんだと感じた。Flutterに触れたことで、Jetpack ComposeやSwiftUIにより興味が湧いてきたことも良い変化だった。
プロセスについて
時間に追われながら学んでしまったという反省。この点はペース配分の工夫で改善できると思うので、次に生かしていきたい。元々の動機のアプリを作りきるところは年内中にはたどり着きたいと思う。
開設してみたものの、今後どうするか全く考えていないので、この記事だけで終わる可能性が微レ存です。さよなら!