
「COMPOSE を用いた ANDROID アプリ開発の基礎」の学習支援⑱ -ユニット6パスウェイ2
皆さん、こんにちは!又はこんばんは!初めての方は初めまして!
Google Codelabsの「COMPOSE を用いた ANDROID アプリ開発の基礎」コースのお手伝いをする「りおん」です。
今回は、ユニット6「データの永続化」のパスウェイ2「Roomを使用してデータを永続化する」です。
この記事はCodelabsの補足を目的としているため、「COMPOSE を用いた ANDROID アプリ開発の基礎」コースで心配になった時、エラーが起きて詰まった時や、分からないことがあった時、軽く復習したい時に見てください!
また、目次を見て自分に必要なところだけ見るのをお勧めします!
この記事を作成するにあたり使用しているAndroid StudioのバージョンはGiraffeです。バージョンによってはUIが違うことがあるのでご了承ください。
また、2024年4月30日現在の「COMPOSE を用いた ANDROID アプリ開発の基礎」コースを参考にしています。
学習内容
①Kotlin Flowの実践
動画は、「データをFlowに読み込んで変換し、ビューに表示する」方法を紹介していました。
Flowの概要
動画内では湖からデータを取ってくる方法を例にsuspend関数とFlowの説明をしていました。
suspend関数はワンショットコールなので、新しいデータが必要になったらまた呼び出さないといけません。
Flowはデータストリーム、すなわち「上流から下流に流れるもの*」を表すのでデータソースに変更があると自動的にビューに流れます。
そのため、変更される可能性があるデータを取得する際はFlowを用いることが推奨されます。
(*「Wikipedia」https://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%88%E3%83%AA%E3%83%BC%E3%83%A0_(%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0)より)

suspendはデータを取りに行く必要がある
Flowはデータがパイプを伝って流れてくるイメージ
RoomのFlow
Roomライブラリのようなデータソースで依存している有名なライブラリでは私たちがFlowを作成する必要はありません。
Roomでは、Room DAO内で型X(Xは自由という意味)のFlowを公開することによりデータベース(動画ではプロデューサー)更新が発生するたびクエリのコンテンツを出力します。

Flowの変換
レイヤが変わると、Flowの型(Flow<X>のX)を変換する必要が生じることがあります。このようにFlowを変換する場合は、map演算子のような中間演算子を使用します。
下のコードではDataレイヤからUIレイヤに移るにあたり、Flow<List<Item>>からFlow<HomeUiState>に変更しています。

Flowの収集
データの収集はUIレイヤで行います。その際にストリーム内に出力された全ての値を取得する場合はsuspend関数のcollectを使用します。
UIからFlowを取得する際の懸念点
1.UIが画面に表示されていない時にFlowからの収集を停止する
2.アプリの構成の変更
1.UIが画面に表示されていない時にFlowからの収集を停止する
アプリが表示されていない時もFlowからの収集を行うのは端末のリソースを無駄にすると同時にクラッシュする危険性もあります。
この問題を解決する方法として以下の2つ紹介されました
①asLiveData演算子を用いる
-> FlowをUIが画面に表示されている時のみアイテムを監視するLiveDataに変換する演算子。
②repeatOnLyfecycleを用いる
-> repeatOnLyfecycleはLifecycle.Stateをパラメータとして受け取るsuspend関数。ライフサイクルが指定された状態に移行するとコルーチンを起動し、それ以外になるとコルーチンはキャンセルされます。
2.アプリの構成の変更
画面を回転させると、アクティビティは破棄されますがViewModelは破棄されません。このようにライフサイクルに違いがあるためViewModelからアクティビティにFlowをそのまま公開することはできません。湖の例で例えると突然パイプが切れる可能性があるようなものです。
そこで、StateFlowというデータの貯蔵庫を作ることで安全に何度でもデータを取得することが出来ます。
StateFlowがあるアクティビティが破棄されるとStateFlowの上にあるFlowはすべてキャンセルされます。その際タイムアウトが存在し、この時間内だけはキャンセルされないので画面の変更などの一瞬のアクティビティ破棄と再作成の時に重宝します。
任意のFlowをStateFlowにする際はstateInを使用します。

まとめると、StateFlowを用いてViewModelからFlowを取得するかasLiveDataを使用することが推奨されています。
④Roomを使用してデータを永続化する
このアクティビティでは主に3つのことを学習しました。
1.Roomの概要
2.Roomエンティティの作成
3.Room DAOの作成
4.Room Databaseクラスの作成
Roomの概要
Roomとは、SQLite データベースの上に位置する抽象化レイヤのことです。
つまり、SQLite データベースを簡単にセットアップ、構成、操作するためのライブラリです。
SQLite APIを直接使用するのではなく、Roomライブラリを使用することは公式からも推奨されています。
Roomを用いた際のアプリアーキテクチャは以下の通りになります。
※Roomの位置づけはDatasourceです。
Room DAOを介してSQLiteを操作しているので、私たちが直接SQLiteを操作していないことがこの図から分かると思います。

「Android Room とビュー - Kotlin」を参考
https://developer.android.com/codelabs/android-room-with-a-view-kotlin
Roomのコンポーネント
Roomは以下の3つのコンポーネントから構成されています
①Room Entity
②Room DAO
③Room Database

①Room Entity
Room Entityは、アプリのデータベースのテーブルを記述するアノテーション付きのクラスです

②Room DAO
Room DAOは関数に対する SQL クエリのマッピングを行います。
つまり、データベース内のデータを取得、更新、挿入、削除するというSQLクエリを関数で提供します。

③Room Database
Room Databaseは、データベースを保持し、そのデータベースに関連付けられているDAOのインスタンスをアプリに提供します。
アプリはこの DAO を使用して、関連するデータエンティティオブジェクトのインスタンスとしてデータベースからデータを取得できます。
(「Room を使用してデータを永続化する」https://developer.android.com/codelabs/basic-android-kotlin-compose-persisting-data-roomより)


Roomの使用方法
1.Roomの依存関係をbuild.gradle.kts(Module: app)ファイルのdependenciesブロックに追加する
KSPとはアノテーションプロセッサです。コンパイル時にKotlinアノテーションを解析してくれます。
val room_version = "2.6.1"
ksp("androidx.room:room-compiler:$room_version")
implementation("androidx.room:room-ktx:$room_version")
implementation("androidx.room:room-runtime:$room_version")
2.エンティティを作成する
クラスに@Entityを付けて、テーブルを表すEntityクラスを作成します。
テーブル内のエンティティインスタンスを一意に識別するためにつけられる主キーは@PrimaryKeyを付けることで定義できます。また、この際にautoGenerateをtrueにすることで主キーの値を自動生成させることが出来ます。

3.DAOを作成する
※DAOはインターフェースであることに注意してください。
interfaceの上に@Daoを付けます。
インターフェースの本体内にクエリ毎に関数を定義します。追加は@Insert、排除は@Delete、変更は@Update、その他の操作は@Queryを関数の上につけます。
コルーチンを使用するので勿論suspend関数です。または、関数の戻り値がFlowの場合はsuspendを付けません。

4.データベースを作成する
entityを含む@Databaseがつけられた抽象化クラスを作成します。
DAOクラスのインスタンスを返す抽象メソッドを定義します。
データースのインスタンスは非常に高コストなので、データベースオブジェクトをインスタンス化する際にはシングルトーンパターンに従うことが推奨されています。その為Codelabsではコンパニオンオブジェクトを用いました。
また、複数のスレッドがデータベースインスタンスを同時に要求し、データベースが複数作成されるという競合状態を避けるため、データベースを取得するコードをsyncronizedブロックで囲んでいます。

⑤Roomによるデータの読み取りと更新
データベースのテスト
Codelabsだ学習したデータベースのテスト法をまとめます。
1.buid.gradle.kts(Module : app)ファイルにEspressoとJUnitに関する依存関係を追加します。
JUnitは今まで何度も使用した単体テスト用のフレームワークです。
EspressoはUIテスト用のフレームワークです。(今回は使用していない)
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
2.データベースを作成します。
どのテストよりも先にデータベースの作成を行うようするために@Beforeを付けます。
テストとして追加、排除した情報が永続化しないようにするためinMemoryDatabaseBuilder()関数を用います。
また、今回は.allowMainThreadQueries()を使用してメインスレッドでDAOのクエリを実行しています。

3.データベースを閉じるための関数を用意します
それぞれのテストの後に実行したいため@Afterを付けます。

4.テストを作成します
DAOクエリが正常に機能しているかそれぞれテストします。

さいごに
今回もCodelabsでの学習お疲れさまでした!
Roomライブラリの使用法は結構大変だというのが今回のCodelabsで分かったと思いますが、その分作成できるアプリの幅は格段に増えたと思います。
次回は、簡単なデータを永続化させるDataBaseの学習です。リストの絞り込み内容のようにRoomを使用するには勿体ないほど小さなデータを簡単に実装できるようになります。
是非、引き続きCodelabsでの学習を行ってください!