1. 始める前に
これまでの Codelab では、データベース抽象化レイヤである Room を使用して SQLite データベースにデータを保存する方法を学びました。この Codelab では、Jetpack DataStore について説明します。Kotlin のコルーチンと Flow に基づいて構築された DataStore には、次の 2 種類の実装があります。型付きオブジェクトを格納する Proto DataStore と、Key-Value ペアを格納する Preferences DataStore です。
この Codelab のハンズオンでは、Preferences DataStore の使用方法を学びます。Proto DataStore は、この Codelab の対象外です。
前提条件
- Android アーキテクチャ コンポーネントの
ViewModel
、LiveData
、Flow
に精通しており、ViewModelProvider.Factory
を使用してViewModel
をインスタンス化する方法を理解している。 - 同時実行の基本に精通している。
- 長時間実行タスクにコルーチンを使用する方法を理解している。
学習内容
- DataStore の概要、使用すべき理由とタイミング
- アプリに Preference DataStore を追加する方法
必要なもの
- Words アプリのスターター コード(前の Codelab で作成した Words アプリの解答コードと同じ)。
- Android Studio がインストールされているパソコン。
この Codelab のスターター コードをダウンロードする
この Codelab では、前の解答コードから Words アプリの機能を拡張します。スターター コードには、これまでの Codelab でよく見たコードも含まれています。
この Codelab のコードを GitHub から取得して Android Studio で開く手順は次のとおりです。
- Android Studio を起動します。
- [Welcome to Android Studio] ウィンドウで、[Get from VCS] をクリックします。
- [Get from Version Control] ダイアログの [Version control] で、[Git] が選択されていることを確認します。
- 提供されたコードの URL を [URL] ボックスに貼り付けます。
- 必要に応じて、[Directory] を推奨されるデフォルトとは異なるものに変更します。
- [Clone] をクリックします。Android Studio がコードの取得を開始します。
- Android Studio が開くまで待ちます。
- Codelab のスターター コード、アプリコード、または解答コードに適したモジュールを選択します。
- 実行ボタン をクリックし、コードをビルドして実行します。
2. スターター アプリの概要
Words アプリは、2 つの画面で構成されます。1 つ目の画面にはユーザーが選択できる文字が表示され、2 つ目の画面には選択した文字で始まる単語のリストが表示されます。
このアプリには、ユーザが文字のリスト レイアウトとグリッド レイアウトを切り替えるためのメニュー オプションがあります。
- スターター コードをダウンロードし、Android Studio で開いてアプリを実行します。文字が線形レイアウトで表示されます。
- 右上にあるメニュー オプションをタップします。レイアウトがグリッド レイアウトに切り替わります。
- アプリを終了して再起動します。そのためには、Android Studio の [Stop ‘app']()と [Run ‘app']()を使用します。なお、アプリを再起動すると文字はグリッド レイアウトではなく線形レイアウトで表示されます。
ユーザーの選択は保持されない点にご注意ください。この Codelab では、この問題の解決方法について説明します。
作成するアプリの概要
- この Codelab では、Preferences DataStore を使用して DataStore でレイアウト設定を保持する方法を学びます。
3.Preferences DataStore の概要
Preferences DataStore は、ログイン情報、ダークモード設定、フォントサイズなどを保存するような、小規模でシンプルなデータセットに最適です。DataStore は、オンライン食料品店の在庫リストや学生データベースのような、複雑なデータセットには適していません。大規模なデータセットや複雑なデータセットを保存する必要がある場合は、DataStore ではなく Room を使用することを検討してください。
Jetpack DataStore ライブラリを使用すると、データを保存するためのシンプルで安全な非同期 API を作成できます。これには Preferences DataStore と Proto DataStore という 2 種類の実装があります。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 をアプリと統合するには、まず依存関係として追加します。
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 を作成する
data
というパッケージを追加し、その中にSettingsDataStore
という Kotlin クラスを作成します。Context
型のSettingsDataStore
クラスにコンストラクタ パラメータを追加します。
class SettingsDataStore(context: Context) {}
SettingsDataStore
クラスの外で、LAYOUT_PREFERENCES_NAME
というprivate const val
を宣言し、文字列値layout_preferences
を割り当てます。これは、次のステップでインスタンス化する Preferences Datastore の名前です。
private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"
- 引き続きクラスの外で、
preferencesDataStore
デリゲートを使用してDataStore
インスタンスを作成します。Preferences Datastore を使用しているため、データストア型としてPreferences
を渡す必要があります。また、データストアのname
をLAYOUT_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
クラスに以下を実装します。
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
にしてください。
- レイアウト設定の Boolean と
Context
という 2 つのパラメータを取る、saveLayoutToPreferencesStore()
というsuspend
関数を作成します。
suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {
}
- 上記の関数を実装して
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
値を取得します。
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
}
- 以下の import が自動的にインポートされない場合は追加します。
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
例外処理
DataStore がファイルに対してデータを読み書きする際、データへのアクセス時に IOExceptions
が発生することがあります。その場合、catch()
演算子を使用して例外を捕捉し、処理します。
- 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
で次の手順を行います。
SettingsDataStore
型のSettingsDataStore
というprivate
クラス変数を宣言します。この変数は後で初期化するため、lateinit
にします。
private lateinit var SettingsDataStore: SettingsDataStore
onViewCreated()
関数の最後で、新しい変数を初期化し、requireContext()
をSettingsDataStore
コンストラクタに渡します。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
// Initialize SettingsDataStore
SettingsDataStore = SettingsDataStore(requireContext())
}
データの読み取りと監視
LetterListFragment
で、onViewCreated()
メソッド内のSettingsDataStore
の初期化の部分にあるasLiveData
()
を使用してpreferenceFlow
をLivedata
に変換します。オブザーバーをアタッチし、オーナーとしてviewLifecycleOwner
を渡します。
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { })
- オブザーバー内で、新しいレイアウト設定を
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
オーナーが破棄されたときにキャンセルされます。
LetterListFragment
で、onOptionsItemSelected()
関数内のR.id.
action_switch_layout
のケースの最後にあるlifecycleScope
を使用してコルーチンを起動します。launch
ブロック内で、saveLayoutToPreferencesStore()
を呼び出してisLinearLayoutManager
とcontext
を渡します。
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
}
- アプリを実行します。メニュー オプションをクリックしてアプリのレイアウトを変更します。
- Preferences DataStore の永続性をテストします。アプリ レイアウトをグリッド レイアウトに変更します。アプリを終了して再起動します。これを行うには、Android Studio の [Stop ‘app'] オプション()と [Run ‘app'] オプション()を使用します。
アプリを再起動すると文字は線形レイアウトではなくグリッド レイアウトで表示されます。アプリは、ユーザーが選択したレイアウト設定を正常に保存しています。
文字がグリッド レイアウトで表示されるようになりましたが、メニュー アイコンは正しく更新されません。次は、この問題の解決方法について説明します。
7. メニュー アイコンのバグを修正する
onViewCreated()
では、RecyclerView レイアウトはレイアウト設定に従って更新されますが、メニュー アイコンは更新されません。これがメニュー アイコンのバグの原因となっています。この問題は、RecyclerView レイアウトの更新と同時にメニューを再描画することで解決できます。
オプション メニューの再描画
一度作成したメニューは、フレームごとに再描画されるわけではありません。これは、フレームごとに同じメニューを再描画すると冗長になるためです。invalidateOptionsMenu()
関数は、オプション メニューを再描画するよう Android に指示します。
メニュー アイテムの追加、アイテムの削除、メニューのテキストまたはアイコンの変更など、オプション メニューでなんらかの変更を加えたとき、この関数を呼び出すことができます。今回は、メニュー アイコンが変更されました。このメソッドを呼び出すと、オプション メニューが変更されたため再作成する必要があることが宣言されます。onCreateOptionsMenu(android.view.Menu)
メソッドは、次回表示する必要があるときに呼び出されます。
LetterListFragment
で、onViewCreated()
内のpreferenceFlow
オブザーバーの最後にあるchooseLayout()
の呼び出しで、activity
のinvalidateOptionsMenu()
を呼び出してメニューを再描画します。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
...
// Redraw the menu
activity?.invalidateOptionsMenu()
})
}
- アプリを再度実行し、レイアウトを変更します。
- アプリを終了して再起動します。メニュー アイコンが正しく更新されます。
これで、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. 関連リンク
DataStore
guide
- DataStore リファレンス
- 設定
- android.datastore.preferences.core