Androidアプリを開発するにあたって「いいね問題」は課題としてよくあがります。例えば記事を表示するアプリで、記事一覧と記事詳細画面があるとします。記事詳細でいいねを押したあと、記事一覧に戻ったときにいいねをどう反映させるか、という問題です。
良いね問題への対処としてはflowのようなイベントを一覧画面でもサブスクライブしてイベントを受け取れるようにするという方法もあるようです。こちらは変更が少ないと機能すると思ます。しかし扱う変更が増えてくるとコードを追いかけるのも難しくなるでしょう。
一方Googleのアーキテキチャガイドやnow in androidのようなサンプルではroomのようなdatabaseを使う方法があります。droidkaigiの公式アプリでも使われていたらしく、この方法を使うことが推奨されます。
Source of truth(信頼できる唯一の情報源)
Google Developerのアーキテキチャガイドを見ているとSource of truth(信頼できる唯一の情報源)という言葉がよく出てきます。
repositoryもデータベースのような形で唯一の情報源を規定しておくと、テストもしやすくなりますし、どこでどのテータを使っているのか迷いもなくなります。この唯一の情報源という考え方はviewModelがuiStateを公開する時も同じであり、アーキテキチャガイドの根幹を支える思想だと私は思います。
Source of truthの考え方に乗っ取るならflowのようなEventを使って各画面のuiStateを更新するというのは避けたほうが良いと思います。情報源が画面ごとになってしまい、更新をし忘れると情報が乖離する状況となってしまうからです。
Googleでは強く推奨されているようですが世の中的にはあえてこの思想に乗っ取らないケースもあるようです。私もどちらが扱いやすいものになるかどうかはケースバイケースだとは思います。小規模なアプリなら信頼できる唯一の情報源の原則に乗っ取るコストの方が大きくなる場合もあります。しかしできるだけ原則に則ったほうが、見通しがよくなるでしょう。
オフラインファーストが不要でもdatabaseを作るべきか
アーキテキチャガイドでは下記のような記述があります。
オフライン ファーストのサポートを提供するため、信頼できる情報源としてローカル データソース(データベースなど)を使用することをおすすめします。
こちらオフラインファーストの機能はあるとよいですがそこまで求めていない場合もあります。しかし私は永続化の必要・不必要によらずroomのdatabaseを使うべきだと考えています(違ってたらご意見ください)それをこれから話します。
私も永続化する必要もないのになぜわざわざdatabaseを作らないといけないのか、と思っていました。というのもEntityはプリミティブ型を使わないといけなかったりして変換の必要もあるし、マイグレーションの処理を考えないといけないし、テストも追加で必要になります。しかしやってみると、一つのrepositoryにroomを導入するだけなら結構簡単だったです。
簡単というのは理由があってGoogleが推奨しているやり方なのでドキュメントやサンプルも揃っていますし、最低限のことができればいいなら端折れる部分もあるからです。端折れる部分のtipsでいうと下記のようなものです。
- Entityはかならずしも一対多とかの設計を厳密にやらなくていい。ListのPropertyがあれば、Jsonをパースして文字列になおして保存すると手っ取り早い。データ更新に関係ない部分に使っていける。
- databaseを永続化せずインメモリを使うやり方がある。これだとマイグレーションを考えなくて良い。
- サンプルを見ながらやれば、Entity、database、DAOの実装は簡単。
- テストもInstrumentationテストの例があり、テスタブルに設計されていてサンプルもある。
(まぁ、簡単というのは実装前に身構えていたよりも、ということなので、それなりに考慮することはあります。)
インメモリデータベースはこんな感じで作れます。
Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
また永続化する必要もないのにdatabaseをわざわざ使う理由として、ページングライブラリとの相性があります。Androidのページングはpaging3のライブラリを使うことになりますが、ページングしたデータをデータベース化するときにMediatorというクラスを使います。公式のサンプルではroomを使う例が紹介されていて、DAOの返り値にPagingeSourceを設定すれば直接PagingSourceを得ることができるようになっています。
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(users: List<User>)
@Query("SELECT * FROM users WHERE label LIKE :query")
fun pagingSource(query: String): PagingSource<Int, User>
@Query("DELETE FROM users")
suspend fun clearAll()
}
反面、room以外のデータベースを使うことは難しそうですし、repositoryにmutableな変数を用意して更新するといったケースも難しそうです。paging3ライブラリのMediatorはroomを使うにはかなり使いやすいですが、それ以外のやり方にカスタムしていこうとするとブラックボックス気味で難しいし、意図しない動作が出てきそうで不安があります。こちらにカスタムで実装しようとしている例をのせておきます。
実装難易度がそこまで高くないこと、ページングライブラリと相性の良いことから、永続化する必要がなくてもroomを使っていきたいですね。
最後に
今回はdatabaseについていろいろ触れてきましたが、私もまだ考慮が不十分なところがあるかもしれませんので随時更新していきたいと思います。なにか気づいたことがあれば連絡ください。