1. 始める前に
はじめに
このユニットで、SQL と Room を使用してデバイスにデータをローカルで保存する方法を学びました。SQL と Room は強力なツールです。ただし、リレーショナル データを保存する必要がない場合には、DataStore のシンプルなソリューションを利用できます。DataStore Jetpack コンポーネントは、少ないオーバーヘッドで小さくシンプルなデータセットを保存できる優れた方法です。DataStore には、Preferences DataStore
と Proto DataStore
の 2 種類の実装があります。
Preferences DataStore
は Key-Value ペアを格納します。値は、String
、Boolean
、Integer
などの Kotlin の基本データ型にできます。複雑なデータセットは保存されません。定義済みのスキーマは必要ありません。Preferences Datastore
の主なユースケースは、ユーザー設定をデバイスに保存することです。Proto DataStore
はカスタムデータ型を格納します。proto 定義をオブジェクト構造にマッピングする事前定義スキーマが必要です。
この Codelab では Preferences DataStore
についてのみ説明します。Proto DataStore
の詳細については、DataStore のドキュメントをご覧ください。
Preferences DataStore
は、ユーザー管理の設定を保存する優れた方法です。この Codelab では、DataStore
を実装してこれを行う方法について学びます。
前提条件:
- Room によるデータの読み取りと更新の Codelab を通じて、「Compose での Android の基礎」コースワークを完了していること
必要なもの
- Android Studio がインストールされた、インターネットに接続できるパソコン。
- デバイスまたはエミュレータ
- Dessert Release アプリのスターター コード
作成するアプリの概要
Dessert Release アプリには、Android リリースのリストが表示されます。アプリバーのアイコンで、グリッドビュー レイアウトとリストビュー レイアウトを切り替えます。
現在の状態では、アプリはレイアウトの選択を保持しません。アプリを閉じてもレイアウトの選択は保存されず、設定はデフォルトの選択に戻ります。この Codelab では、DataStore
を Dessert Release アプリに追加し、これを使用してレイアウトの選択設定を保存します。
2. スターター コードをダウンロードする
次のリンクをクリックして、この Codelab のコードをすべてダウンロードします。
また、必要に応じて、GitHub から Dessert Release コードのクローンを作成することもできます。
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-dessert-release.git $ cd basic-android-kotlin-compose-training-dessert-release $ git checkout starter
- Android Studio で
basic-android-kotlin-compose-training-dessert-release
フォルダを開きます。 - Android Studio で Dessert Release アプリのコードを開きます。
3.依存関係を設定する
app/build.gradle.kts
ファイルの dependencies
に以下を追加します。
implementation("androidx.datastore:datastore-preferences:1.0.0")
4. ユーザー設定リポジトリを実装する
data
パッケージで、UserPreferencesRepository
という名前の新しいクラスを作成します。
UserPreferencesRepository
コンストラクタで、Preferences
型のDataStore
オブジェクト インスタンスを表すプライベート値プロパティを定義します。
class UserPreferencesRepository(
private val dataStore: DataStore<Preferences>
){
}
DataStore
は Key-Value ペアを格納します。値にアクセスするにはキーを定義する必要があります。
UserPreferencesRepository
クラス内にcompanion object
を作成します。booleanPreferencesKey()
関数を使用してキーを定義し、そのキーにis_linear_layout
という名前を渡します。SQL テーブル名と同様に、このキーにはアンダースコア形式を使用する必要があります。このキーは、線形レイアウトを表示するかどうかを示すブール値にアクセスするために使用されます。
class UserPreferencesRepository(
private val dataStore: DataStore<Preferences>
){
private companion object {
val IS_LINEAR_LAYOUT = booleanPreferencesKey("is_linear_layout")
}
...
}
DataStore へ書き込む
DataStore
内の値を作成、変更するには、ラムダを edit()
メソッドに渡します。ラムダに MutablePreferences
のインスタンスが渡され、これを使用して DataStore
の値を更新できます。このラムダ内のすべての更新は、単一のトランザクションとして実行されます。つまり、更新はアトミックで、一度に行われます。この更新により、一部の値が更新されても他の値が更新されない状況が回避されます。
- suspend 関数を作成し、
saveLayoutPreference()
という名前を付けます。 saveLayoutPreference()
関数で、dataStore
オブジェクトに対してedit()
メソッドを呼び出します。
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
dataStore.edit {
}
}
- コードを読みやすくするために、ラムダ本体で提供される
MutablePreferences
の名前を定義します。このプロパティを使用して、定義したキーと、saveLayoutPreference()
関数に渡されたブール値を使用して値を設定します。
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
dataStore.edit { preferences ->
preferences[IS_LINEAR_LAYOUT] = isLinearLayout
}
}
DataStore から読み取る
isLinearLayout
を dataStore
に書き込む方法を構築しました。これを読み取る手順は次のとおりです。
UserPreferencesRepository
に、isLinearLayout
というFlow<Boolean>
型のプロパティを作成します。
val isLinearLayout: Flow<Boolean> =
DataStore.data
プロパティを使用してDataStore
値を公開できます。isLinearLayout
をDataStore
オブジェクトのdata
プロパティに設定します。
val isLinearLayout: Flow<Boolean> = dataStore.data
data
プロパティは、Preferences
オブジェクトの Flow
です。Preferences
オブジェクトは、DataStore 内のすべての Key-Value ペアを格納します。DataStore のデータが更新されるたびに、新しい Preferences
オブジェクトが Flow
に出力されます。
- map 関数を使用して、
Flow<Preferences>
をFlow<Boolean>
に変換します。
この関数は、現在の Preferences
オブジェクトをパラメータとして持つラムダを受け入れます。以前に定義したキーを指定して、レイアウト設定を取得できます。saveLayoutPreference
がまだ呼び出されていない場合、値が存在しない可能性があるため、デフォルト値も指定する必要があります。
- 線形レイアウト ビューをデフォルトにするには、
true
を指定します。
val isLinearLayout: Flow<Boolean> = dataStore.data.map { preferences ->
preferences[IS_LINEAR_LAYOUT] ?: true
}
例外処理
デバイスでファイル システムを操作する際には、なんらかの失敗が発生する可能性があります。たとえば、ファイルが存在しない、ディスクに空きがない、ディスクがマウントされていない、といったことが起こります。DataStore
でファイルのデータの読み書きを行う際、DataStore
へのアクセス時に IOExceptions
が発生する場合があります。例外をキャッチしてこれらの失敗を処理するには、catch{}
演算子を使用します。
- コンパニオン オブジェクトに、ロギングに使用する不変の
TAG
文字列プロパティを実装します。
private companion object {
val IS_LINEAR_LAYOUT = booleanPreferencesKey("is_linear_layout")
const val TAG = "UserPreferencesRepo"
}
Preferences DataStore
は、データの読み取り中にエラーが発生した場合、IOException
をスローします。isLinearLayout
の初期化ブロックで、map()
の前にcatch{}
演算子を使用して、IOException
をキャッチできるようにします。
val isLinearLayout: Flow<Boolean> = dataStore.data
.catch {}
.map { preferences ->
preferences[IS_LINEAR_LAYOUT] ?: true
}
- catch ブロックに
IOexception
がある場合は、エラーを記録してemptyPreferences()
を出力します。別のタイプの例外がスローされた場合は、その例外を再スローすることをおすすめします。エラーがある場合にemptyPreferences()
を出力することで、map 関数で引き続きデフォルト値にマッピングできます。
val isLinearLayout: Flow<Boolean> = dataStore.data
.catch {
if(it is IOException) {
Log.e(TAG, "Error reading preferences.", it)
emit(emptyPreferences())
} else {
throw it
}
}
.map { preferences ->
preferences[IS_LINEAR_LAYOUT] ?: true
}
5. DataStore を初期化する
この Codelab では、依存関係挿入を手動で処理する必要があります。したがって、UserPreferencesRepository
クラスに Preferences DataStore
を手動で指定する必要があります。DataStore
を UserPreferencesRepository
に挿入する手順は次のとおりです。
dessertrelease
パッケージを見つけます。- このディレクトリ内に
DessertReleaseApplication
という新しいクラスを作成し、Application
クラスを実装します。これは DataStore のコンテナです。
class DessertReleaseApplication: Application() {
}
DessertReleaseApplication.kt
ファイル内、ただしDessertReleaseApplication
クラスの外部で、LAYOUT_PREFERENCE_NAME
というprivate const val
を宣言します。LAYOUT_PREFERENCE_NAME
変数に文字列値layout_preferences
を割り当てます。これは、次のステップでインスタンス化するPreferences Datastore
の名前として使用できます。
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
DessertReleaseApplication
クラス本体の外側にあるDessertReleaseApplication.kt
ファイル内で、preferencesDataStore
デリゲートを使用してContext.dataStore
というDataStore<Preferences>
型のプライベート値プロパティを作成します。preferencesDataStore
デリゲートのname
パラメータのLAYOUT_PREFERENCE_NAME
を渡します。
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = LAYOUT_PREFERENCE_NAME
)
DessertReleaseApplication
クラス本文内で、UserPreferencesRepository
のlateinit var
インスタンスを作成します。
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = LAYOUT_PREFERENCE_NAME
)
class DessertReleaseApplication: Application() {
lateinit var userPreferencesRepository: UserPreferencesRepository
}
onCreate()
メソッドをオーバーライドします。
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = LAYOUT_PREFERENCE_NAME
)
class DessertReleaseApplication: Application() {
lateinit var userPreferencesRepository: UserPreferencesRepository
override fun onCreate() {
super.onCreate()
}
}
onCreate()
メソッド内で、dataStore
をパラメータとしてUserPreferencesRepository
を作成して、userPreferencesRepository
を初期化します。
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = LAYOUT_PREFERENCE_NAME
)
class DessertReleaseApplication: Application() {
lateinit var userPreferencesRepository: UserPreferencesRepository
override fun onCreate() {
super.onCreate()
userPreferencesRepository = UserPreferencesRepository(dataStore)
}
}
AndroidManifest.xml
ファイルの<application>
タグ内に次の行を追加します。
<application
android:name=".DessertReleaseApplication"
...
</application>
このアプローチでは、DessertReleaseApplication
クラスをアプリのエントリ ポイントとして定義します。このコードの目的は、MainActivity
を起動する前に、DessertReleaseApplication
クラスで定義された依存関係を初期化することです。
6. UserPreferencesRepository を使用する
リポジトリを ViewModel に提供する
UserPreferencesRepository
が依存関係の挿入により利用可能となったので、DessertReleaseViewModel
でこれを使用できます。
DessertReleaseViewModel
で、コンストラクタ パラメータとしてUserPreferencesRepository
のプロパティを作成します。
class DessertReleaseViewModel(
private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
...
}
ViewModel
のコンパニオン オブジェクトのviewModelFactory initializer
ブロックで、次のコードを使用してDessertReleaseApplication
のインスタンスを取得します。
...
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
...
}
}
}
}
DessertReleaseViewModel
のインスタンスを作成し、userPreferencesRepository
を渡します。
...
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
DessertReleaseViewModel(application.userPreferencesRepository)
}
}
}
}
UserPreferencesRepository
に ViewModel からアクセスできるようになりました。次のステップでは、先ほど実装した UserPreferencesRepository
の読み取りと書き込みの機能を使用します。
レイアウト設定を保存する
DessertReleaseViewModel
のselectLayout()
関数を編集して設定リポジトリにアクセスし、レイアウト設定を更新します。DataStore
への書き込みは、suspend
関数を使用して非同期に行われます。設定リポジトリのsaveLayoutPreference()
関数を呼び出す新しいコルーチンを開始します。
fun selectLayout(isLinearLayout: Boolean) {
viewModelScope.launch {
userPreferencesRepository.saveLayoutPreference(isLinearLayout)
}
}
レイアウト設定を読み取る
このセクションでは、ViewModel
の既存の uiState: StateFlow
をリファクタリングして、リポジトリの isLinearLayout: Flow
を反映させます。
uiState
プロパティをMutableStateFlow(DessertReleaseUiState)
に初期化するコードを削除します。
val uiState: StateFlow<DessertReleaseUiState> =
リポジトリからの線形レイアウト設定には、true または false という 2 つの値があり、形式は Flow<Boolean>
です。この値は、UI の状態にマッピングする必要があります。
StateFlow
を、isLinearLayout Flow
で呼び出されるmap()
コレクション変換の結果に設定します。
val uiState: StateFlow<DessertReleaseUiState> =
userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
}
DessertReleaseUiState
データクラスのインスタンスを返し、isLinearLayout Boolean
を渡します。画面はこの UI 状態を使用して、表示する正しい文字列とアイコンを決定します。
val uiState: StateFlow<DessertReleaseUiState> =
userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
DessertReleaseUiState(isLinearLayout)
}
UserPreferencesRepository.isLinearLayout
は Flow
で、これは cold です。ただし、UI に状態を提供するには、StateFlow
などのホットフローを使用して、UI で状態が常にすぐに使用できるようにすることをおすすめします。
stateIn()
関数を使用して、Flow
をStateFlow
に変換します。stateIn()
関数は、scope
、started
、initialValue
の 3 つのパラメータを受け入れます。これらのパラメータには、それぞれviewModelScope
、SharingStarted.WhileSubscribed(5_000)
、DessertReleaseUiState()
を渡します。
val uiState: StateFlow<DessertReleaseUiState> =
userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
DessertReleaseUiState(isLinearLayout)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = DessertReleaseUiState()
)
- アプリを起動します。切り替えアイコンをクリックすると、グリッド レイアウトと線形レイアウトを切り替えることができます。
お疲れさまでした。ユーザーのレイアウト設定を保存するために、アプリに Preferences DataStore
を追加しました。
7. 解答コードを取得する
この Codelab の完成したコードをダウンロードするには、以下の git コマンドを使用します。
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-dessert-release.git $ cd basic-android-kotlin-compose-training-dessert-release $ git checkout main
または、リポジトリを ZIP ファイルとしてダウンロードし、Android Studio で開くこともできます。
解答コードを確認する場合は、GitHub で表示します。