Preferences DataStore を使用する

Datastore とは

DataStore は、SharedPreferences に代わるものとして改善された新しいデータ ストレージ ソリューションです。Kotlin のコルーチンと Flow に基づいて構築された DataStore には、次の 2 種類の実装があります。プロトコル バッファに基づく型付きオブジェクトを保存する Proto DataStore と、Key-Value ペアを保存する Preferences DataStore です。データは、非同期で、一貫性を持ち、トランザクションとして保存されるため、SharedPreferences の欠点の一部が解消されます。

学習内容

  • DataStore の概要と使用すべき理由。
  • DataStore をプロジェクトに追加する方法。
  • Preferences DataStore と Proto DataStore の違い、およびそれぞれの利点。
  • Preferences DataStore の使用方法。
  • SharedPreferences から Preferences DataStore に移行する方法。

作業内容

この Codelab で使用するサンプルアプリでは、完了したステータスでフィルタリングでき、優先度と期限で並べ替えができるタスクのリストを表示します。

fcb2ffa4e6b77f33.gif

[Show completed tasks] フィルタのブール値フラグはメモリに保存されます。並べ替え順序は、SharedPreferences オブジェクトを使用してディスクに引き継がれます。

この Codelab では、次のタスクを完了して、Preferences DataStore の使用方法を確認します。

  • 完了ステータスのフィルタを DataStore に引き継ぐ。
  • 並べ替え順序を SharedPreferences から DataStore に移行する。

両者の違いを理解するために、Proto DataStore Codelab も使用してみることをおすすめします。

必要なもの

アーキテクチャ コンポーネントの概要については、View Codelab を使用した Room をご覧ください。フローの概要については、Kotlin Flow と LiveData を使用した高度なコルーチンをご覧ください。

このステップでは、Codelab 全体のコードをダウンロードし、簡単なサンプルアプリを実行します。

できるだけ迅速にご利用を開始していただけるよう、Google では構築のスターター プロジェクトをご用意しました。

git がインストールされている場合は、以下のコマンドを実行します。git がインストールされているかどうかを確認するには、ターミナルまたはコマンドラインで「git --version」と入力し、正しく実行されることを確認します。

 git clone https://github.com/googlecodelabs/android-datastore

初期状態は master ブランチにあります。ソリューションのコードは preferences_datastore ブランチにあります。

git がない場合は、次のボタンをクリックして、この Codelab のすべてのコードをダウンロードできます。

ソースコードをダウンロード

  1. コードを解凍し、Android Studio バージョン 3.6 以降でプロジェクトを開きます。
  2. デバイスまたはエミュレータで [app] 実行構成を実行します。

b3c0dfdb92dfed77.png

アプリが実行され、タスクのリストが表示されます。

16eb4ceb800bf131.png

このアプリにより、タスクのリストが表示されます。各タスクには、名前、完了ステータス、優先度、期限というプロパティがあります。

作業に必要なコードを簡素化するため、アプリでは次の 2 つの操作のみを行うことができます。

  • 完了したタスクの公開設定を切り替える - デフォルトでは、タスクは非表示になります
  • タスクを優先度、期限、期限と優先度の順に並べ替える

アプリは、「アプリ アーキテクチャ ガイド」で推奨されているアーキテクチャに沿って実行されます。次に、各パッケージの内容を示します。

data

  • Task モデルクラス
  • TasksRepository クラス - タスクを指定します。簡素化のために、ハードコードされたデータを返し、それを Flow を介して公開することで、より現実的なシナリオを表します。
  • UserPreferencesRepository クラス - enum として定義された SortOrder を保持します。列挙値名に基づいて、現在の並べ替え順は String として SharedPreferences に保存されます。並べ替え順を保存および取得する同期メソッドが公開されます。

ui

  • RecyclerView を使用した Activity の表示に関するクラス。
  • TasksViewModel クラスは、UI ロジックに関与しています。

TasksViewModel - UI に表示される必要があるデータの構築に必要なすべての要素(タスクのリスト、完了の表示、並べ替え順のフラグ)を保持し、TasksUiModel オブジェクトにラップします。これらの値が変更されるたびに、新しい TasksUiModel を構築し直す必要があります。そのために、次の 3 つの要素を組み合わせます。

  • Flow<List<Task>>TasksRepository から取得されます。
  • 最新の完了の表示フラグを保持する MutableStateFlow<Boolean>。これは、メモリにのみ保持されます。
  • 最新の SortOrder 値を保持する MutableStateFlow<SortOrder>

UI が正しく更新されることを確実にするために、アクティビティの開始時にのみ LiveData<TasksUiModel> を公開します。

コードにはいくつかの問題があります。

  • UserPreferencesRepository.sortOrder を初期化すると、ディスク IO の UI スレッドをブロックします。その結果、UI ジャンクが発生することがあります。
  • 完了の表示フラグはメモリにのみ保持されるため、ユーザーがアプリを開くたびにリセットされます。SortOrder と同様に、これはアプリを閉じても成り立つように維持される必要があります。
  • 現在はデータの引き継ぎに SharedPreferences を使用していますが、ここでは MutableStateFlow をメモリに保持することで、手動で変更し、変更の通知ができるようにしています。これは、アプリケーション内の他の場所で値が変更されると、簡単に壊れてしまいます。
  • UserPreferencesRepository には、並べ替え順序を更新する 2 つのメソッド、enableSortByDeadline()enableSortByPriority() があります。どちらの方法も現在の並べ替え順の値に依存していますが、一方がもう片方のメソッドの終了前に呼び出されると、誤った最終値を使用することになります。さらに、UI スレッドで呼び出されると、これらのメソッドは UI ジャンクと厳格なモード違反を引き起こす可能性があります。

完了の表示フラグと並べ替え順フラグはどちらもユーザー設定ですが、現時点では 2 つの異なるオブジェクトとして表現されています。今回の目標の 1 つは、これら 2 つのフラグを UserPreferences クラスの下に統合することです。

では、DataStore を使ってこうした問題に対処する方法を確認しましょう。

小規模またはシンプルなデータセットの保存が必要な場合が頻繁にあります。その場合、以前は SharedPreferences を使用したかもしれませんが、この API にはいくつかの欠点があります。Jetpack DataStore ライブラリは、このような問題を解決し、データを保存するシンプルで安全かつ非同期の API を作成します。次の 2 つの実装を提供します。

  • Preferences DataStore
  • Proto DataStore

機能

SharedPreferences

PreferencesDataStore

ProtoDataStore

非同期 API

✅(リスナーを介して変更された値を読み取る場合のみ)

✅(Flow を介して)

✅(Flow を介して)

同期 API

✅(ただし、UI スレッドでの呼び出しは安全ではない)

UI スレッドでの呼び出しは安全

❌*

✅(処理は内部の Dispatchers.IO に移動)

✅(処理は内部の Dispatchers.IO に移動)

エラーをシグナルできる

ランタイム例外からの保護

❌**

強整合性保証を備えたトランザクション API がある

データの移行を処理

✅(SharedPreferences から)

✅(SharedPreferences から)

型の安全性

✅(プロトコル バッファを使用して)

  • SharedPreferences には、UI スレッドで呼び出しても安全に見える同期 API がありますが、これは実際にはディスク I/O オペレーションを行います。さらに、apply()fsync() で UI スレッドをブロックします。fsync() 呼び出しの保留は、サービスの開始と停止、およびアプリケーションの任意の場所でアクティビティが開始または停止するたびにトリガーされます。UI スレッドは、apply() によってスケジュールされた保留中の fsync() 呼び出しによってブロックされます。多くの場合、ANR の発生元になります。

** SharedPreferences は、ランタイムの例外として解析エラーをスローします。

Preferences DataStore と Proto DataStore

Preference DataStore と Proto DataStore のどちらでもデータを保存できますが、両者は異なる方法を使用します。

  • Preference DataStore(SharedPreferences など)は、スキーマを事前に定義することなく、キーに基づいてデータにアクセスします。
  • Proto DataStore は、プロトコル バッファを使用してスキーマを定義します。Protobufs を使用すると、強力に型付けされたデータを引き継ぎできます。XML やその他類似のデータ形式よりも、小さくてシンプルかつ具体的です。Proto DataStore では新しいシリアル化メカニズムを学ぶことが求められますが、Proto DataStore がもたらす強力な型付けという利点には充分な価値があると考えられます。

Room と DataStore

部分的な更新、参照整合性、大規模または複雑なデータセットが必要な場合は、DataStore ではなく Room を使用することを検討してください。DataStore は、小規模で単純なデータセットに適しており、部分更新や参照整合性をサポートしていません。

Preferences DataStore API は SharedPreferences と類似していますが、次のようないくつかの違いがあります。

  • データ更新をトランザクションとして処理する
  • データの現在の状態を表す Flow を公開する
  • データの永続化メソッドがない(apply()commit()
  • 内部状態への変更可能な参照が返されない
  • 型付けされたキーを使用して MapMutableMap のような API を公開する

Preferences DataStore API をプロジェクトに追加し、SharedPreferences を DataStore に移行してみましょう。

依存関係を追加する

build.gradle ファイルを更新して、以下の Preference DataStore の依存関係を追加します。

implementation "androidx.datastore:datastore-preferences:1.0.0-alpha06"

完了の表示フラグと並べ替え順フラグはどちらもユーザー設定ですが、現時点では 2 つの異なるオブジェクトとして表現されています。今回の目標の 1 つは、これら 2 つのフラグを UserPreferences クラスの下で統合し、DataStore を使用して UserPreferencesRepository に保存することです。現在、完了の表示フラグはメモリに TasksViewModel で保持されています。

まず、UserPreferencesRepositoryUserPreferences データクラスを作成することから始めましょう。現時点では、showCompleted フィールドが 1 つだけ必要です。並べ替え順は後で追加します。

data class UserPreferences(val showCompleted: Boolean)

DataStore の作成

context.createDataStoreFactory() メソッドを使用して、UserPreferencesRepositoryDataStore<Preferences> プライベート フィールドを作成しましょう。必須のパラメータは、Preferences DataStore の名前です。

private val dataStore: DataStore<Preferences> =
        context.createDataStore(name = "user")

Preferences DataStore からのデータの読み取り

Preferences DataStore は、設定が変更されるたびに出力される Flow<Preferences> に保存されたデータを公開します。Preferences オブジェクト全体ではなく、UserPreferences オブジェクトを公開します。そうするには、Flow<Preferences> をマッピングし、目的のブール値を取得し、キーに基づいて UserPreferences オブジェクトを作成する必要があります。

したがって、最初に必要なのは show completed キーを定義することです。これは、プライベート PreferencesKeys オブジェクト内のメンバーとして宣言できる booleanPreferencesKey 値です。

private object PreferencesKeys {
  val SHOW_COMPLETED = booleanPreferencesKey("show_completed")
}

dataStore.data: Flow<Preferences> に基づいて作成された userPreferencesFlow: Flow<UserPreferences> を公開します。これはマッピングされ、適切な設定が取得できるようになります。

val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
    .map { preferences ->
        // Get our show completed value, defaulting to false if not set:
        val showCompleted = preferences[PreferencesKeys.SHOW_COMPLETED]?: false
        UserPreferences(showCompleted)
    }

データ読み取り中の例外の処理

DataStore がファイルからデータを読み取る際、データの読み取り中にエラーが発生すると IOExceptions がスローされます。これに対処するには、map() の前に catch() Flow 演算子を使用し、スローされた例外が IOException であった場合に emptyPreferences() を出力します。別の種類の例外がスローされた場合は、もう一度スローします。

val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
    .catch { exception ->
        // dataStore.data throws an IOException when an error is encountered when reading data
        if (exception is IOException) {
            emit(emptyPreferences())
        } else {
            throw exception
        }
    }.map { preferences ->
        // Get our show completed value, defaulting to false if not set:
        val showCompleted = preferences[PreferencesKeys.SHOW_COMPLETED]?: false
        UserPreferences(showCompleted)
    }

Preferences DataStore へのデータの書き込み

データを書き込むために、DataStore には suspend 関数 DataStore.edit(transform: suspend (MutablePreferences) -> Unit) が用意されています。この関数は、DataStore 内の状態をトランザクション的に更新できるようにする transform ブロックを受け入れます。

変換ブロックに渡された MutablePreferences は、以前に実行した編集によって最新の状態になります。transform ブロックの MutablePreferences に対するすべての変更は、transform の完了後と edit の完了前にディスクに適用されます。MutablePreferences で 1 つの値を設定すると、その他のすべての設定は変更されないままとなります。

注: 変換ブロックの外部で MutablePreferences を変更しようとしないでください。

updateShowCompleted() という UserPreferencesshowCompleted プロパティを更新できるようにする suspend 関数を作成してみましょう。この関数は、dataStore.edit() を呼び出して新しい値を設定します。

suspend fun updateShowCompleted(showCompleted: Boolean) {
    dataStore.edit { preferences ->
        preferences[PreferencesKeys.SHOW_COMPLETED] = showCompleted
    }
}

edit() は、ディスクの読み取りまたは書き込み中にエラーが発生した場合、IOException をスローできます。変換ブロックでその他のエラーが発生した場合、edit() によってスローされます。

この時点で、アプリはコンパイルされているはずですが、UserPreferencesRepository でたった今作成した機能は使用されません。

並べ替え順は SharedPreferences に保存されます。それを DataStore に移行しましょう。実行するには、まず UserPreferences の更新からはじめます。これも並べ替え順に保存します。

data class UserPreferences(
    val showCompleted: Boolean,
    val sortOrder: SortOrder
)

SharedPreferences からの移行

DataStore に移行できるようにするには、DataStore ビルダーを更新して、SharedPreferencesMigration で移行のリストに渡すことができるようにする必要があります。DataStore は自動的に SharedPreferences から DataStore に移行できるようになります。移行は、DataStore 内でデータアクセスが行われる前に実行されます。つまり、DataStore.data が値を出力する前と DataStore.edit() でデータが更新される前に、移行が成功している必要があります。

注: キーが SharedPreferences から移行されるのは 1 回のみです。そのため、コードが DataStore に移行されたら、古い SharedPreferences の使用を中止する必要があります。

private val dataStore: DataStore<Preferences> =
    context.createDataStore(
        name = USER_PREFERENCES_NAME,
        migrations = listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME))
    )

private object PreferencesKeys {
    ...
    // Note: this has the the same name that we used with SharedPreferences.
    val SORT_ORDER = stringPreferencesKey("sort_order")
}

すべてのキーが DataStore に移行され、ユーザー設定 SharedPreferences から削除されます。これで、Preferences から、SORT_ORDER キーに基づいて SortOrder を取得して更新できるようになります。

DataStore からの並べ替え順の読み取り

userPreferencesFlow を更新して、map() 変換でも並べ替え順序を取得できるようにしましょう。

val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
    .catch { exception ->
        if (exception is IOException) {
            emit(emptyPreferences())
        } else {
            throw exception
        }
    }.map { preferences ->
        // Get the sort order from preferences and convert it to a [SortOrder] object
        val sortOrder =
            SortOrder.valueOf(
                preferences[PreferencesKeys.SORT_ORDER] ?: SortOrder.NONE.name)

        // Get our show completed value, defaulting to false if not set:
        val showCompleted = preferences[PreferencesKeys.SHOW_COMPLETED] ?: false
        UserPreferences(showCompleted, sortOrder)
    }

DataStore に並べ替え順を保存する

現在、UserPreferencesRepository では並べ替え順フラグの設定には同期的な方法しか公開されていないため、同時実行の問題があります。Google は、並べ替え順序を更新する 2 つのメソッドとして enableSortByDeadline()enableSortByPriority() を公開しています。どちらのメソッドも現在の並べ替え順の値に依存しますが、一方のメソッドがもう片方のメソッドが終了する前に呼び出された場合、最終的に誤った値になります。

DataStore ではデータの更新がトランザクションとして行われることを保証しているため、この問題は発生しなくなります。次のように変更します。

  • enableSortByDeadline()enableSortByPriority() を、dataStore.edit() を使用する suspend 関数になるように更新します。
  • edit() の変換ブロックでは、_sortOrderFlow フィールドからではなく Preference パラメータから currentOrder を取得します。
  • updateSortOrder(newSortOrder) を呼び出す代わりに、設定内で並べ替え順を直接更新できます。

実装は次のようになります。

suspend fun enableSortByDeadline(enable: Boolean) {
    // edit handles data transactionally, ensuring that if the sort is updated at the same
    // time from another thread, we won't have conflicts
    dataStore.edit { preferences ->
        // Get the current SortOrder as an enum
        val currentOrder = SortOrder.valueOf(
            preferences[PreferencesKeys.SORT_ORDER] ?: SortOrder.NONE.name
        )

        val newSortOrder =
            if (enable) {
                if (currentOrder == SortOrder.BY_PRIORITY) {
                    SortOrder.BY_DEADLINE_AND_PRIORITY
                } else {
                    SortOrder.BY_DEADLINE
                }
            } else {
                if (currentOrder == SortOrder.BY_DEADLINE_AND_PRIORITY) {
                    SortOrder.BY_PRIORITY
                } else {
                    SortOrder.NONE
                }
            }
        preferences[PreferencesKeys.SORT_ORDER] = newSortOrder.name
    }
}

suspend fun enableSortByPriority(enable: Boolean) {
    // edit handles data transactionally, ensuring that if the sort is updated at the same
    // time from another thread, we won't have conflicts
    dataStore.edit { preferences ->
        // Get the current SortOrder as an enum
        val currentOrder = SortOrder.valueOf(
            preferences[PreferencesKeys.SORT_ORDER] ?: SortOrder.NONE.name
        )

        val newSortOrder =
            if (enable) {
                if (currentOrder == SortOrder.BY_DEADLINE) {
                    SortOrder.BY_DEADLINE_AND_PRIORITY
                } else {
                    SortOrder.BY_PRIORITY
                }
            } else {
                if (currentOrder == SortOrder.BY_DEADLINE_AND_PRIORITY) {
                    SortOrder.BY_DEADLINE
                } else {
                    SortOrder.NONE
                }
            }
        preferences[PreferencesKeys.SORT_ORDER] = newSortOrder.name
    }
}

UserPreferencesRepository は、完了の表示フラグと並べ替え順フラグ両方を DataStore に保存し、Flow<UserPreferences> を公開します。TasksViewModel を更新して、これらを使用しましょう。

代わりに、showCompletedFlowsortOrderFlow を削除し、userPreferencesRepository.userPreferencesFlow で初期化される userPreferencesFlow という値を作成します。

private val userPreferencesFlow = userPreferencesRepository.userPreferencesFlow

tasksUiModelFlow を作成するときに、showCompletedFlowsortOrderFlowuserPreferencesFlow に置き換えます。必要に応じてパラメータを変更してください。

filterSortTasks を呼び出すときは、userPreferencesshowCompletedsortOrder を渡します。コードの内容は次のようになります。

private val tasksUiModelFlow = combine(
        repository.tasks,
        userPreferencesFlow
    ) { tasks: List<Task>, userPreferences: UserPreferences ->
        return@combine TasksUiModel(
            tasks = filterSortTasks(
                tasks,
                userPreferences.showCompleted,
                userPreferences.sortOrder
            ),
            showCompleted = userPreferences.showCompleted,
            sortOrder = userPreferences.sortOrder
        )
    }

showCompletedTasks() 関数は更新され、userPreferencesRepository.updateShowCompleted() を呼び出すようになりました。これは suspend 関数であるため、viewModelScope に新しいコルーチンを作成します。

fun showCompletedTasks(show: Boolean) {
    viewModelScope.launch {
        userPreferencesRepository.updateShowCompleted(show)
    }
}

userPreferencesRepository 関数の enableSortByDeadline()enableSortByPriority() は suspend 関数になったため、viewModelScope で開始された新しいコルーチンでも呼び出す必要があります。

fun enableSortByDeadline(enable: Boolean) {
    viewModelScope.launch {
       userPreferencesRepository.enableSortByDeadline(enable)
    }
}

fun enableSortByPriority(enable: Boolean) {
    viewModelScope.launch {
        userPreferencesRepository.enableSortByPriority(enable)
    }
}

UserPreferencesRepository のクリーンアップ

不要になったフィールドとメソッドを削除しましょう。以下を削除できます。

  • _sortOrderFlow
  • sortOrderFlow
  • updateSortOrder()
  • private val sortOrder: SortOrder

これでアプリが正常にコンパイルされるようになりました。実行して、完了の表示フラグと並べ替え順フラグが正しく保存されているかを確認してみましょう。

Codelab repo の設定ブランチを確認して、変更を比較します。

これで、Preferences DataStore への移行が完了しました。学習内容をおさらいしましょう。

  • SharedPreferences には、UI スレッドで呼び出しても安全に見える同期 API がある、エラーをシグナルさせるメカニズムがない、トランザクション API の不足といった一連のデメリットがあります。
  • DataStore は、その API の欠点のほとんどに対応した、SharedPreferences に代わるものです。
  • DataStore には、Kotlin のコルーチンと Flow を使用する完全に非同期の API があり、データの移行を実行し、データの整合性を保証し、データ破損を処理します。