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 で表示します。
