DataStore を使用して設定をローカルに保存する

1. 始める前に

はじめに

このユニットで、SQL と Room を使用してデバイスにデータをローカルで保存する方法を学びました。SQL と Room は強力なツールです。ただし、リレーショナル データを保存する必要がない場合には、DataStore のシンプルなソリューションを利用できます。DataStore Jetpack コンポーネントは、少ないオーバーヘッドで小さくシンプルなデータセットを保存できる優れた方法です。DataStore には、Preferences DataStoreProto DataStore の 2 種類の実装があります。

  • Preferences DataStore は Key-Value ペアを格納します。値は、StringBooleanInteger などの Kotlin の基本データ型にできます。複雑なデータセットは保存されません。定義済みのスキーマは必要ありません。Preferences Datastore の主なユースケースは、ユーザー設定をデバイスに保存することです。
  • Proto DataStore はカスタムデータ型を格納します。proto 定義をオブジェクト構造にマッピングする事前定義スキーマが必要です。

この Codelab では Preferences DataStore についてのみ説明します。Proto DataStore の詳細については、DataStore のドキュメントをご覧ください。

Preferences DataStore は、ユーザー管理の設定を保存する優れた方法です。この Codelab では、DataStore を実装してこれを行う方法について学びます。

前提条件:

必要なもの

  • Android Studio がインストールされた、インターネットに接続できるパソコン。
  • デバイスまたはエミュレータ
  • Dessert Release アプリのスターター コード

作成するアプリの概要

Dessert Release アプリには、Android リリースのリストが表示されます。アプリバーのアイコンで、グリッドビュー レイアウトとリストビュー レイアウトを切り替えます。

b6e4bd0e50915b81.png 24a261db4cf2c6b8.png

現在の状態では、アプリはレイアウトの選択を保持しません。アプリを閉じてもレイアウトの選択は保存されず、設定はデフォルトの選択に戻ります。この 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
  1. Android Studio で basic-android-kotlin-compose-training-dessert-release フォルダを開きます。
  2. Android Studio で Dessert Release アプリのコードを開きます。

3.依存関係を設定する

app/build.gradle.kts ファイルの dependencies に以下を追加します。

implementation("androidx.datastore:datastore-preferences:1.0.0")

4. ユーザー設定リポジトリを実装する

  1. data パッケージで、UserPreferencesRepository という名前の新しいクラスを作成します。

c4c2e90902898001.png

  1. UserPreferencesRepository コンストラクタで、Preferences 型の DataStore オブジェクト インスタンスを表すプライベート値プロパティを定義します。
class UserPreferencesRepository(
    private val dataStore: DataStore<Preferences>
){
}

DataStore は Key-Value ペアを格納します。値にアクセスするにはキーを定義する必要があります。

  1. UserPreferencesRepository クラス内に companion object を作成します。
  2. 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 の値を更新できます。このラムダ内のすべての更新は、単一のトランザクションとして実行されます。つまり、更新はアトミックで、一度に行われます。この更新により、一部の値が更新されても他の値が更新されない状況が回避されます。

  1. suspend 関数を作成し、saveLayoutPreference() という名前を付けます。
  2. saveLayoutPreference() 関数で、dataStore オブジェクトに対して edit() メソッドを呼び出します。
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
    dataStore.edit {

    }
}
  1. コードを読みやすくするために、ラムダ本体で提供される MutablePreferences の名前を定義します。このプロパティを使用して、定義したキーと、saveLayoutPreference() 関数に渡されたブール値を使用して値を設定します。
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
    dataStore.edit { preferences ->
        preferences[IS_LINEAR_LAYOUT] = isLinearLayout
    }
}

DataStore から読み取る

isLinearLayoutdataStore に書き込む方法を構築しました。これを読み取る手順は次のとおりです。

  1. UserPreferencesRepository に、isLinearLayout という Flow<Boolean> 型のプロパティを作成します。
val isLinearLayout: Flow<Boolean> =
  1. DataStore.data プロパティを使用して DataStore 値を公開できます。isLinearLayoutDataStore オブジェクトの data プロパティに設定します。
val isLinearLayout: Flow<Boolean> = dataStore.data

data プロパティは、Preferences オブジェクトの Flow です。Preferences オブジェクトは、DataStore 内のすべての Key-Value ペアを格納します。DataStore のデータが更新されるたびに、新しい Preferences オブジェクトが Flow に出力されます。

  1. map 関数を使用して、Flow<Preferences>Flow<Boolean> に変換します。

この関数は、現在の Preferences オブジェクトをパラメータとして持つラムダを受け入れます。以前に定義したキーを指定して、レイアウト設定を取得できます。saveLayoutPreference がまだ呼び出されていない場合、値が存在しない可能性があるため、デフォルト値も指定する必要があります。

  1. 線形レイアウト ビューをデフォルトにするには、true を指定します。
val isLinearLayout: Flow<Boolean> = dataStore.data.map { preferences ->
    preferences[IS_LINEAR_LAYOUT] ?: true
}

例外処理

デバイスでファイル システムを操作する際には、なんらかの失敗が発生する可能性があります。たとえば、ファイルが存在しない、ディスクに空きがない、ディスクがマウントされていない、といったことが起こります。DataStore でファイルのデータの読み書きを行う際、DataStore へのアクセス時に IOExceptions が発生する場合があります。例外をキャッチしてこれらの失敗を処理するには、catch{} 演算子を使用します。

  1. コンパニオン オブジェクトに、ロギングに使用する不変の TAG 文字列プロパティを実装します。
private companion object {
    val IS_LINEAR_LAYOUT = booleanPreferencesKey("is_linear_layout")
    const val TAG = "UserPreferencesRepo"
}
  1. Preferences DataStore は、データの読み取り中にエラーが発生した場合、IOException をスローします。isLinearLayout の初期化ブロックで、map() の前に catch{} 演算子を使用して、IOException をキャッチできるようにします。
val isLinearLayout: Flow<Boolean> = dataStore.data
    .catch {}
    .map { preferences ->
        preferences[IS_LINEAR_LAYOUT] ?: true
    }
  1. 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 を手動で指定する必要があります。DataStoreUserPreferencesRepository に挿入する手順は次のとおりです。

  1. dessertrelease パッケージを見つけます。
  2. このディレクトリ内に DessertReleaseApplication という新しいクラスを作成し、Application クラスを実装します。これは DataStore のコンテナです。
class DessertReleaseApplication: Application() {
}
  1. DessertReleaseApplication.kt ファイル内、ただし DessertReleaseApplication クラスの外部で、LAYOUT_PREFERENCE_NAME という private const val を宣言します。
  2. LAYOUT_PREFERENCE_NAME 変数に文字列値 layout_preferences を割り当てます。これは、次のステップでインスタンス化する Preferences Datastore の名前として使用できます。
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
  1. 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
)
  1. DessertReleaseApplication クラス本文内で、UserPreferencesRepositorylateinit 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
}
  1. 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()
    }
}
  1. 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)
    }
}
  1. AndroidManifest.xml ファイルの <application> タグ内に次の行を追加します。
<application
    android:name=".DessertReleaseApplication"
    ...
</application>

このアプローチでは、DessertReleaseApplication クラスをアプリのエントリ ポイントとして定義します。このコードの目的は、MainActivity を起動する前に、DessertReleaseApplication クラスで定義された依存関係を初期化することです。

6. UserPreferencesRepository を使用する

リポジトリを ViewModel に提供する

UserPreferencesRepository が依存関係の挿入により利用可能となったので、DessertReleaseViewModel でこれを使用できます。

  1. DessertReleaseViewModel で、コンストラクタ パラメータとして UserPreferencesRepository のプロパティを作成します。
class DessertReleaseViewModel(
    private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
    ...
}
  1. ViewModel のコンパニオン オブジェクトの viewModelFactory initializer ブロックで、次のコードを使用して DessertReleaseApplication のインスタンスを取得します。
    ...
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
                ...
            }
        }
    }
}
  1. DessertReleaseViewModel のインスタンスを作成し、userPreferencesRepository を渡します。
    ...
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
                DessertReleaseViewModel(application.userPreferencesRepository)
            }
        }
    }
}

UserPreferencesRepository に ViewModel からアクセスできるようになりました。次のステップでは、先ほど実装した UserPreferencesRepository の読み取りと書き込みの機能を使用します。

レイアウト設定を保存する

  1. DessertReleaseViewModelselectLayout() 関数を編集して設定リポジトリにアクセスし、レイアウト設定を更新します。
  2. DataStore への書き込みは、suspend 関数を使用して非同期に行われます。設定リポジトリの saveLayoutPreference() 関数を呼び出す新しいコルーチンを開始します。
fun selectLayout(isLinearLayout: Boolean) {
    viewModelScope.launch {
        userPreferencesRepository.saveLayoutPreference(isLinearLayout)
    }
}

レイアウト設定を読み取る

このセクションでは、ViewModel の既存の uiState: StateFlow をリファクタリングして、リポジトリの isLinearLayout: Flow を反映させます。

  1. uiState プロパティを MutableStateFlow(DessertReleaseUiState) に初期化するコードを削除します。
val uiState: StateFlow<DessertReleaseUiState> =

リポジトリからの線形レイアウト設定には、true または false という 2 つの値があり、形式は Flow<Boolean> です。この値は、UI の状態にマッピングする必要があります。

  1. StateFlow を、isLinearLayout Flow で呼び出される map() コレクション変換の結果に設定します。
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
}
  1. DessertReleaseUiState データクラスのインスタンスを返し、isLinearLayout Boolean を渡します。画面はこの UI 状態を使用して、表示する正しい文字列とアイコンを決定します。
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
        DessertReleaseUiState(isLinearLayout)
    }

UserPreferencesRepository.isLinearLayoutFlow で、これは cold です。ただし、UI に状態を提供するには、StateFlow などのホットフローを使用して、UI で状態が常にすぐに使用できるようにすることをおすすめします。

  1. stateIn() 関数を使用して、FlowStateFlow に変換します。
  2. stateIn() 関数は、scopestartedinitialValue の 3 つのパラメータを受け入れます。これらのパラメータには、それぞれ viewModelScopeSharingStarted.WhileSubscribed(5_000)DessertReleaseUiState() を渡します。
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
        DessertReleaseUiState(isLinearLayout)
    }
.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = DessertReleaseUiState()
    )
  1. アプリを起動します。切り替えアイコンをクリックすると、グリッド レイアウトと線形レイアウトを切り替えることができます。

b6e4bd0e50915b81.png 24a261db4cf2c6b8.png

お疲れさまでした。ユーザーのレイアウト設定を保存するために、アプリに 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 で表示します