Preferences DataStore

1. 始める前に

これまでの Codelab では、データベース抽象化レイヤである Room を使用して SQLite データベースにデータを保存する方法を学びました。この Codelab では、Jetpack DataStore について説明します。Kotlin のコルーチンと Flow に基づいて構築された DataStore には、次の 2 種類の実装があります。型付きオブジェクトを格納する Proto DataStore と、Key-Value ペアを格納する Preferences DataStore です。

この Codelab のハンズオンでは、Preferences DataStore の使用方法を学びます。Proto DataStore は、この Codelab の対象外です。

前提条件

  • Android アーキテクチャ コンポーネントの ViewModelLiveDataFlow に精通しており、ViewModelProvider.Factory を使用して ViewModel をインスタンス化する方法を理解している。
  • 同時実行の基本に精通している。
  • 長時間実行タスクにコルーチンを使用する方法を理解している。

学習内容

  • DataStore の概要、使用すべき理由とタイミング
  • アプリに Preference DataStore を追加する方法

必要なもの

  • Words アプリのスターター コード(前の Codelab で作成した Words アプリの解答コードと同じ)。
  • Android Studio がインストールされているパソコン

この Codelab のスターター コードをダウンロードする

この Codelab では、前の解答コードから Words アプリの機能を拡張します。スターター コードには、これまでの Codelab でよく見たコードも含まれています。

この Codelab のコードを GitHub から取得して Android Studio で開く手順は次のとおりです。

  1. Android Studio を起動します。
  2. [Welcome to Android Studio] ウィンドウで [Check out project from version control] をクリックします。
  3. [Git] を選択します。

b89a22e2d8cf3b4e.png

  1. [Clone Repository] ダイアログで、提供されたコードの URL を [URL] ボックスに貼り付けます。
  2. [Test] ボタンをクリックし、[Connection successful] という緑色のポップアップ バブルが表示されることを確認します。
  3. 必要に応じて、[Directory] を推奨されるデフォルトとは異なるものに変更します。

e4fb01c402e47bb3.png

  1. [Clone] をクリックします。Android Studio がコードの取得を開始します。
  2. [Checkout from Version Control] ポップアップで [Yes] をクリックします。

1902d34f29119530.png

  1. Android Studio が開くまで待ちます。
  2. Codelab のスターター コードまたは解答コードに適したモジュールを選択します。

2371589274bce21c.png

  1. 実行ボタン 11c34fc5e516fb1c.png をクリックし、コードをビルドして実行します。

2. スターター アプリの概要

Words アプリは、2 つの画面で構成されます。1 つ目の画面にはユーザーが選択できる文字が表示され、2 つ目の画面には選択した文字で始まる単語のリストが表示されます。

このアプリには、ユーザが文字のリスト レイアウトとグリッド レイアウトを切り替えるためのメニュー オプションがあります。

  1. スターター コードをダウンロードし、Android Studio で開いてアプリを実行します。文字が線形レイアウトで表示されます。
  2. 右上にあるメニュー オプションをタップします。レイアウトがグリッド レイアウトに切り替わります。
  3. アプリを終了して再起動します。これを行うには、Android Studio の [Stop ‘app'] オプション(1c2b7a60ebd9a46e.png)と [Run ‘app'] オプション(3b4c2b852ca05ab9.png)を使用します。なお、アプリを再起動すると文字はグリッド レイアウトではなく線形レイアウトで表示されます。

ユーザーの選択は保持されない点にご注意ください。この Codelab では、この問題の解決方法について説明します。

作成するアプリの概要

  • この Codelab では、Preferences DataStore を使用して DataStore でレイアウト設定を保持する方法を学びます。

3.Preferences DataStore の概要

Preferences DataStore は、ログイン情報、ダークモード設定、フォントサイズなどを保存するような、小規模でシンプルなデータセットに最適です。DataStore は、オンライン食料品店の在庫リストや学生データベースのような、複雑なデータセットには適していません。大規模なデータセットや複雑なデータセットを保存する必要がある場合は、DataStore ではなく Room を使用することを検討してください。

Jetpack DataStore ライブラリを使用すると、データを保存するためのシンプルで安全な非同期 API を作成できます。これには次の 2 種類の実装があります。Preferences DataStore と

Proto DataStore です。Preferences DataStore と Proto DataStore はどちらもデータを保存できますが、その方法はそれぞれ異なります。

  • Preferences DataStore は、スキーマ(データベース モデル)を事前に定義することなく、キーに基づいてデータにアクセスし、保存します。
  • Proto DataStore は、プロトコル バッファを使用してスキーマを定義します。プロトコル バッファ(Protobuf)を使用すると、厳密に型付けされたデータを保持できます。XML や他の類似のデータ形式よりも高速で小さく、シンプルかつ具体的です。

Room と Datastore: 使用するタイミング

アプリで SQL などの構造化された大規模または複雑なデータを保存する必要がある場合は、Room を使用することを検討してください。ただし、保存する必要のあるのが Key-Value ペアで保存できるシンプルまたは小規模なデータのみである場合は、DataStore が最適です。

Proto DataStore と Preferences DataStore: 使用するタイミング

Proto DataStore は型安全性と効率性に優れていますが、構成と設定が必要です。アプリデータが Key-Value ペアで保存できるほどシンプルな場合は、設定が簡単であるため、Preferences DataStore の方が適しています。

Preferences DataStore を依存関係として追加する

DataStore をアプリに統合する最初のステップは、依存関係として追加することです。

  1. build.gradle(Module: Words.app) に次の依存関係を追加します。
implementation "androidx.datastore:datastore-preferences:1.0.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"

4. Preferences DataStore を作成する

  1. data というパッケージを追加し、その中に SettingsDataStore という Kotlin クラスを作成します。
  2. Context 型の SettingsDataStore クラスにコンストラクタ パラメータを追加します。
class SettingsDataStore(context: Context) {}
  1. SettingsDataStore クラスの外で、LAYOUT_PREFERENCES_NAME, という private const val を宣言し、文字列値 layout_preferences を割り当てます。これは、次のステップでインスタンス化する Preferences Datastore の名前です。
private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"
  1. 引き続きクラスの外で、preferencesDataStore デリゲートを使用して DataStore インスタンスを作成します。Preferences Datastore を使用しているため、データストア型として Preferences を渡す必要があります。また、データストアの nameLAYOUT_PREFERENCES_NAME に設定します。

完成したコードは次のようになります。

private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"

// Create a DataStore instance using the preferencesDataStore delegate, with the Context as
// receiver.
private val Context.dataStore : DataStore<Preferences> by preferencesDataStore(
   name = LAYOUT_PREFERENCES_NAME
)

5. SettingsDataStore クラスを実装する

前述のとおり、Preferences DataStore はデータを Key-Value ペアで保存します。このステップでは、レイアウト設定の保存に必要なキーを定義し、Preferences DataStore との間で読み書きする関数を定義します。

キー型の関数

Preferences DataStore は、Room のような定義済みのスキーマを使用せず、対応するキー型の関数を使用して、DataStore<Preferences> インスタンスに保存する各値のキーを定義します。たとえば、int 値のキーを定義するには intPreferencesKey() を使用し、string 値のキーを定義するには stringPreferencesKey() を使用します。通常、これらの関数名には、キーに対して保存するデータの型が接頭辞として付加されます。

data\SettingsDataStore クラスに以下を実装します。

  1. SettingsDataStore クラスを実装するために、最初のステップとして、ユーザー設定が線形レイアウトであるかどうかを指定するブール値を格納するキーを作成します。IS_LINEAR_LAYOUT_MANAGER, という private クラス プロパティを作成し、関数パラメータとして is_linear_layout_manager キー名を渡す booleanPreferencesKey() を使用して初期化します。
private val IS_LINEAR_LAYOUT_MANAGER = booleanPreferencesKey("is_linear_layout_manager")

Preferences DataStore に書き込む

それでは、キーを使用して、ブール値のレイアウト設定を DataStore に保存します。Preferences DataStore には、DataStore 内のデータをトランザクションとして更新する edit() suspend 関数が用意されています。この関数の transform パラメータは、必要に応じて値を更新できるコードブロックを受け入れます。変換ブロック内のすべてのコードは単一のトランザクションとして扱われます。トランザクション処理は内部で Dispacter.IO に移されるため、edit() 関数を呼び出すときは必ず関数を suspend にしてください。

  1. レイアウト設定の Boolean と Context という 2 つのパラメータを取る、saveLayoutToPreferencesStore() という suspend 関数を作成します。
suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {

}
  1. 上記の関数を実装して dataStore.edit(), を呼び出し、新しい値を保存するコードブロックを渡します。
suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {
   context.dataStore.edit { preferences ->
       preferences[IS_LINEAR_LAYOUT_MANAGER] = isLinearLayoutManager
   }
}

Preferences DataStore から読み取る

Preferences DataStore は、設定が変更されるたびに出力される Flow<Preferences> に保存されたデータを公開します。Preferences オブジェクト全体ではなく、Boolean の値だけを公開します。そのために、Flow<Preferences> をマッピングし、目的の Boolean 値を取得します。

  1. dataStore.data: Flow<Preferences> に基づいて作成された preferenceFlow: Flow<UserPreferences> を公開し、マッピングして Boolean 設定を取得します。初回実行時は Datastore が空であるため、デフォルトで true を返します。
val preferenceFlow: Flow<Boolean> = context.dataStore.data
   .map { preferences ->
       // On the first run of the app, we will use LinearLayoutManager by default
       preferences[IS_LINEAR_LAYOUT_MANAGER] ?: true
   }
  1. 以下の import が自動的にインポートされない場合は追加します。
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map

例外処理

DataStore がファイルに対してデータを読み書きする際、データへのアクセス時に IOExceptions が発生することがあります。その場合、catch() 演算子を使用して例外を捕捉し、処理します。

  1. SharedPreference DataStore は、データの読み取り中にエラーが発生した場合、IOException をスローします。preferenceFlow の宣言で、map() の前に catch() 演算子を使用して IOException を捕捉し、emptyPreferences() を出力します。説明を簡単にするために、ここでは他の種類の例外は想定していないので、別の種類の例外がスローされた場合は、もう一度スローします。
val preferenceFlow: Flow<Boolean> = context.dataStore.data
   .catch {
       if (it is IOException) {
           it.printStackTrace()
           emit(emptyPreferences())
       } else {
           throw it
       }
   }
   .map { preferences ->
       // On the first run of the app, we will use LinearLayoutManager by default
       preferences[IS_LINEAR_LAYOUT_MANAGER] ?: true
   }

data\SettingsDataStore クラスを使用できるようになりました。

6. SettingsDataStore クラスを使用する

次のタスクでは、LetterListFragment クラスで SettingsDataStore を使用します。オブザーバーをレイアウト設定にアタッチし、それに応じて UI を更新します。

LetterListFragment に次のステップを実装します。

  1. SettingsDataStore 型の SettingsDataStore という private クラス変数を宣言します。この変数は後で初期化するため、lateinit にします。
private lateinit var SettingsDataStore: SettingsDataStore
  1. onViewCreated() 関数の最後で、新しい変数を初期化し、requireContext()SettingsDataStore コンストラクタに渡します。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   ...
   // Initialize SettingsDataStore
   SettingsDataStore = SettingsDataStore(requireContext())
}

データの読み取りと監視

  1. LetterListFragment で、onViewCreated() メソッド内の SettingsDataStore の初期化の部分にある asLiveData() を使用して preferenceFlowLivedata に変換します。オブザーバーをアタッチし、オーナーとして viewLifecycleOwner を渡します。
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { })
  1. オブザーバー内で、新しいレイアウト設定を isLinearLayoutManager 変数に割り当てます。chooseLayout() 関数を呼び出して RecyclerView レイアウトを更新します。
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
           isLinearLayoutManager = value
           chooseLayout()
   })

完成した onViewCreated() 関数は次のようになります。

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   recyclerView = binding.recyclerView
   // Initialize SettingsDataStore
   SettingsDataStore = SettingsDataStore(requireContext())
   SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
           isLinearLayoutManager = value
           chooseLayout()
   })
}

レイアウト設定を DataStore に書き込む

最後のステップとして、ユーザーがメニュー オプションをタップしたときに、レイアウト設定を Preferences DataStore に書き込みます。Preferences DataStore へのデータの書き込みは、コルーチン内で非同期に行う必要があります。フラグメント内でこれを行うには、LifecycleScope という CoroutineScope を使用します。

LifecycleScope

フラグメントなどのライフサイクル対応コンポーネントは、アプリ内の論理スコープに関する優れたコルーチン サポートと、LiveData との相互運用レイヤを提供します。LifecycleScope は、Lifecycle オブジェクトごとに定義されます。このスコープ内で起動されたすべてのコルーチンは、Lifecycle オーナーが破棄されたときにキャンセルされます。

  1. LetterListFragment で、onOptionsItemSelected() 関数内の R.id.action_switch_layout のケースの最後にある lifecycleScope を使用してコルーチンを起動します。launch ブロック内で、saveLayoutToPreferencesStore() を呼び出して isLinearLayoutManagercontext を渡します。
override fun onOptionsItemSelected(item: MenuItem): Boolean {
   return when (item.itemId) {
       R.id.action_switch_layout -> {
           ...
           // Launch a coroutine and write the layout setting in the preference Datastore
           lifecycleScope.launch {             SettingsDataStore.saveLayoutToPreferencesStore(isLinearLayoutManager, requireContext())
           }
           ...

           return true
       }
  1. アプリを実行します。メニュー オプションをクリックしてアプリのレイアウトを変更します。

  1. Preferences DataStore の永続性をテストします。あアプリ レイアウトをグリッド レイアウトに変更します。アプリを終了して再起動します。これを行うには、Android Studio の [Stop ‘app'] オプション(1c2b7a60ebd9a46e.png)と [Run ‘app'] オプション(3b4c2b852ca05ab9.png)を使用します。

cd2c31f27dfb5157.png

アプリを再起動すると文字は線形レイアウトではなくグリッド レイアウトで表示されます。アプリは、ユーザーが選択したレイアウト設定を正常に保存しています。

文字がグリッド レイアウトで表示されるようになりましたが、メニュー アイコンは正しく更新されません。次は、この問題の解決方法について説明します。

7. メニュー アイコンのバグを修正する

onViewCreated() では、RecyclerView レイアウトはレイアウト設定に従って更新されますが、メニュー アイコンは更新されません。これがメニュー アイコンのバグの原因となっています。この問題は、RecyclerView レイアウトの更新と同時にメニューを再描画することで解決できます。

オプション メニューの再描画

一度作成したメニューは、フレームごとに再描画されるわけではありません。これは、フレームごとに同じメニューを再描画すると冗長になるためです。invalidateOptionsMenu() 関数は、オプション メニューを再描画するよう Android に指示します。

メニュー アイテムの追加、アイテムの削除、メニューのテキストまたはアイコンの変更など、オプション メニューでなんらかの変更を加えたとき、この関数を呼び出すことができます。今回は、メニュー アイコンが変更されました。このメソッドを呼び出すと、オプション メニューが変更されたため再作成する必要があることが宣言されます。onCreateOptionsMenu(android.view.Menu) メソッドは、次回表示する必要があるときに呼び出されます。

  1. LetterListFragment で、onViewCreated() 内の preferenceFlow オブザーバーの最後にある chooseLayout() の呼び出しで、activityinvalidateOptionsMenu() を呼び出してメニューを再描画します。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   ...
   SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
           ...
           // Redraw the menu
           activity?.invalidateOptionsMenu()
   })
}
  1. アプリを再度実行し、レイアウトを変更します。
  2. アプリを終了して再起動します。メニュー アイコンが正しく更新されます。

ce3474dba2a9c1c8.png

これで、Preferences DataStore をアプリに追加して、ユーザーの選択を保存できるようになりました。

8. 解答コード

この Codelab の解答コードは、以下に示すプロジェクトとモジュールにあります。

9. まとめ

  • DataStore は、Kotlin のコルーチンと Flow を使用する完全非同期の API を備えており、データの整合性が保証されます。
  • Jetpack DataStore は、プロトコル バッファを使用して Key-Value ペアや型付きオブジェクトを格納できるデータ ストレージ ソリューションです。
  • DataStore には、Preferences DataStore と Proto DataStore という 2 種類の実装があります。
  • Preferences DataStore は定義済みのスキーマを使用しません。
  • Preferences DataStore は、対応するキー型の関数を使用して、DataStore<Preferences> インスタンスに保存する必要がある各値のキーを定義します。たとえば、int 値のキーを定義するには、intPreferencesKey() を使用します。
  • Preferences DataStore には、DataStore 内のデータをトランザクションとして更新する edit() 関数が用意されています。

10. 関連リンク

ブログ

Jetpack DataStore を使用してデータを保存する