実装前の注意点
DataStore(Preferences) API は、キー&バリュー形式でデータを保存・管理できる仕組みになっており、それ自体はそこまで複雑ではないのですが、実装にあたっては以下のような予備知識を必要とします。
- ViewModelを使ったUI状態管理の基本
- Kotlinの非同期処理(Flow)の基本
- DI(Dependency Injection:依存性注入)の基本的な理解
これらの事項に対する理解が曖昧な場合、これから紹介するサンプルコードがそれぞれどんな役割を果たしているかが分かりづらくなることが予想されます。
コードの説明に関してはできる限り記事内でフォローしますが、全ての詳細をカバーすることはできないので、ある程度の事前知識を持っていることを前提とさせて頂きます。
サンプルコードを見て、『何をしているのか全く意味がわからない』という状態になったら、Jetpack Compose や Kotlin の基本に立ち返ってみると良いかもしれません。
下に参考リンク(GoogleおよびKotlin公式ドキュメント)を貼っておきます。
ただ、筆者の考えとしては、最初はコードの意味がまったく分からなくても良いと思っています。
とりあえずコードを丸っとコピペして、『詳しいことは分からないけど、とにかく実装できた!』…というステップを踏むことも大事だと思います。
もちろん基本は大事なのですが、理解は後からでも良いというのが筆者の持論なので、コードの意味がわからなくてもとにかく前に進んでみるのも良いかもしれません。
POINT!
- ・DataStore(Preferences)の実装にあたっては、Kotlinの非同期処理やDIなどの理解を必要とする!
- ・とは言え、最初から全てを完璧にマスターしておく必要はない!
- ・まずは、『とにかくやってみる』というスタンスも全然アリ!
完成形のUIを確認
今回は、『ユーザー名を入力して保存(SAVE)ボタンをタップすると、そのユーザー名が端末に永続的に保存され、保存されたユーザー名が表示されるアプリ』を作ってみたいと思います。
完成のイメージとしては下の画像のようになります。
完成時には、『SAVE』ボタンをタップすることでアプリを終了してもユーザー名が保存されるようにする必要がありますが、DataStore(Preferences)の導入までは、とりあえずSAVEボタンを押しても何も起こらない状態にしておきましょう。
ユーザー名を表示する部分も、とりあえず『Hi, Name?』とハードコードしておきます。
下はアプリのメイン画面(MainScreen.kt)の暫定コードです:
@Composable
fun MainScreen(
modifier: Modifier = Modifier
) {
var userInput by remember {
mutableStateOf("")
}
Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Hi, Name?",
style = MaterialTheme.typography.displaySmall,
modifier = Modifier
.padding(top = 32.dp)
)
TextField(
value = userInput,
onValueChange = { userInput = it },
modifier = Modifier
.padding(vertical = 32.dp)
)
Button(
onClick = { /*TODO*/ }
) {
Text(text = "SAVE")
}
}
}
当然ですが、現時点ではSAVEボタンをタップしても何も起こらず、入力したユーザー名情報はアプリを閉じると失われます。
1) 依存関係の設定
完成のイメージを確認できたところで、さっそく DataStore(Preferences) の導入を進めていきましょう。
まずは依存関係を設定する必要があります。モジュールレベルの build.gradle.kts ファイルに以下の依存関係を追加します。(※ViewModelも導入する必要があるので、DataStoreと同時に記述しています。)
// add ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
// add DataStore preferences
implementation("androidx.datastore:datastore-preferences:1.0.0")
上のコードでは記事執筆時点での最新の安定バージョンを指定していますが、各自、その時点での最新の安定バージョンを調べて指定することをおすすめします。
コードを追加したら Sync Now をクリックして同期させておきましょう。
この時点では特に難しいポイントはないかと思います
2) UserRepository.ktの追加
さて、ここからが本番です。ユーザーの設定情報(ここではユーザー名)を DataStore(Preferences) を利用して端末に永続保存するためのロジックを記述していきます。
まず、UIではなくデータを管理するためのパッケージ・ファイルであるということを明確にするために、『data』という名前のパッケージを作成します。
com.example.projectname パッケージ上で右クリックし、 New > Package を選択し、data と入力すればOKです。
dataパッケージを作成したら、そのパッケージ内に UserRepository と名前をつけたKotlinファイルを作成します。この時、Classを選択してください。
この後も特定のパッケージにファイルを追加する手順が含まれますが、やり方は同じです。
では、作成された UserRepository.kt ファイルの UserRepository クラスに、DataStore(Preferences) を利用するロジックを記述していきます。
まず、DataStore を利用する際、アプリのトップレベルにおけるコンテキストを利用する必要があるので、 DataStore 型である dataStore をコンストラクタとして指定しておきます。
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
class UserRepository(private val dataStore: DataStore<Preferences>) {
}
このようにすることで、UserRepository のクラス内でコンテキストを定義する必要がなくなり、UserRepository クラスでは DataStore のロジックのみを担うことができます。
さて、DataStore(Preferences)では、データをキーとバリュー(key & value)で管理することができますが、今回の値(value)はユーザー名=文字列です。
なので、stringPreferencesKey()
を使って文字列型の value を持つ key を次のように定義します。
import androidx.datastore.preferences.core.stringPreferencesKey
class UserRepository(private val dataStore: DataStore<Preferences>) {
private companion object {
val USER_NAME = stringPreferencesKey("user_name")
}
}
private companion object
として定義することで、 Preferences の各 key をクラスに一つしか存在できない静的なオブジェクト(シングルトン)として管理できるようになります。
今回はユーザー名だけなので管理は楽ですが、他の設定項目のオン/オフの切り替えなど、多くの key を管理しなければならないケースでは、companion object で管理するメリットがさらに大きくなります
そして次に、端末に保存されている文字列型の value を読み取るためのプロパティを定義します。
これは、コンストラクタで受け取った dataStore の data を map()
で展開し、先ほど companion object で定義した key を指定することによって実現できます。
class UserRepository(private val dataStore: DataStore<Preferences>) {
private companion object {
val USER_NAME = stringPreferencesKey("user_name")
}
val currentUserName: Flow<String> =
dataStore.data.map { preferences ->
preferences[USER_NAME] ?: "Unknown"
}
}
本番ではデータの読み取りに失敗した場合に備えて、エラーを捕捉し、エラーに対応するためのコードも記載すべきですが、今回はコードを簡略化するために省略します。
そして、このままではデータの読み取りだけしかできないので、データを新しく保存したり更新したりするためのメソッドを追加します。
edit()
を使って、文字列を引数として受け取り、その文字列を保存するメソッドを追加します。
class UserRepository(private val dataStore: DataStore<Preferences>) {
private companion object {
val USER_NAME = stringPreferencesKey("user_name")
}
val currentUserName: Flow<String> =
dataStore.data.map { preferences ->
preferences[USER_NAME] ?: "Unknown"
}
suspend fun saveUserName(userName: String) {
dataStore.edit { preferences ->
preferences[USER_NAME] = userName
}
}
}
これで、DataStore (Preferences)の利用に必要な以下の3つが揃いました。
- 文字列を値に持つキーの設定
- 値を読み取るプロパティ
- 値を保存するメソッド
UserRepository.kt ファイルのコードはこれで完成です。
しかし、コンストラクタとして設定している dataStore を受け取るための準備ができていないので、まだ機能しません。
次の章で、アプリケーションコンテキストを利用する準備を整えていきましょう。
3) MyApplicationクラスの追加と設定
このステップでは、アプリケーションコンテキストを UserRepository で利用するための設定を行っていきます。
まず、com.example.projectname パッケージ内に、MyApplication.kt ファイルを作成します(Classを指定)。
そして、次のように MyApplication.kt 内の MyApplicationクラスの外側で、Context拡張プロパティとしてdataStoreを定義し、それを特定のDataStoreインスタンスに委譲します。
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = "setting"
)
class MyApplication {
}
これにより、アプリケーションのどの部分からでも、同じContextを通じてDataStoreにアクセスすることができ、その結果としてデータの一貫性が保たれ、コードの再利用性が向上します。
MyApplication クラスは Application を継承するように指定し、次のようにコードを記載します。
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = "setting"
)
class MyApplication: Application() {
lateinit var userRepository: UserRepository
override fun onCreate() {
super.onCreate()
userRepository = UserRepository(dataStore)
}
}
userRepository は、前のステップで作成した UserRepository クラスを継承していますが、当然ながらここでは初期化できません(コンストラクタである dataStore を指定できないため)
そのため、アプリケーションが初期化されるのに合わせて、先ほど定義した Context.dataStore が UserRepository クラスに渡されるようにします。
このプロセスにより、アプリケーション全体で一貫したデータストレージの仕組みを提供し、UserRepositoryクラスがこのデータストアを使ってユーザーの設定や情報を管理できるようになります。
これにより、データの保存、取得、更新がアプリケーションのどこからでも簡単に行えるようになります。
また、このステップの最後に AndroidManifest.xml ファイルの application タグに次のように android:name を加えるのを忘れないようにしましょう。
<application
android:name=".MyApplication">
この指定により、MainActivityが起動される前に、MyApplicationクラスで定義された依存関係が初期化されるようになります。
これで MyApplication クラスに関わるコードの記述と設定は完了ですが、コード量が少ない割にコードの役割や意味が分かりづらいところかと思います。
このステップは、『アプリケーションコンテキスト(アプリ全体におけるコンテキスト)を、DataStore(Preferences)で利用できるようにしている』…と、最初はざっくりと理解しておくと良いかと思います。
4) ViewModelの追加と設定
これまでのステップで、DataStore(Preferences)を利用する準備は整いましたが、このままではUIに反映されません。
このステップでは、UIの役割を担う ViewModel を利用してアプリを完成に近づけていきたいと思います。
最初に、sample.com.projectname > ui パッケージ内に、MyAppViewModel ファイルを作成しておきましょう。
MyAppViewModelクラスでは、当然ながら UserRepositoryクラスのプロパティやメソッドを利用しますが、その UserRepository はアプリケーションコンテキストに依存関係を有しています。
なので、そのまま UserRepository を MyAppViewMode 内で利用することはできません。ということで、次のようにコンストラクタとして userRepository を指定します。
import androidx.lifecycle.ViewModel
import com.example.datastoresample.data.UserRepository
class MyAppViewModel(
private val userRepository: UserRepository
): ViewModel() {
}
これにより、UserRepository クラスはアプリケーションコンテキストに依存し、MyAppViewModel クラスは UserRepository クラスに依存しているという関係になりました。
このままでは MyAppViewModel を初期化する際に UserRepository の初期化が必要になってしまい、ViewModelを利用できなくなる気がしてしまいますが、この依存関係の解決はこのステップの最後で行います。
依存関係の解決はとりあえず置いておき、まずは保存されたユーザーネームを得るためのプロパティから定義していきましょう。
その前に、ViewModel で管理する UiState として data class を MyAppViewModel クラスの外側で定義しておきます。
data class UiState (
val userName: String
)
そして、UiState データクラスを利用し、UserRepository クラスで定義した currentUserName プロパティから次のようにデータを非同期的に読み込みます。
class MyAppViewModel(
private val userRepository: UserRepository
): ViewModel() {
val uiState: StateFlow<UiState> =
userRepository.currentUserName.map { userName ->
UiState(userName)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = UiState("Unknown")
)
}
ViewModelにおけるUI状態(UiState)は StateFlow 型で管理する必要があるので、 .stateIn()
で Flow 型から StateFlow 型に変換すると同時に、値の購読の設定や初期値の指定などを行っています。
SharingStarted.WhileSubscribed(5000)
は、フローの購読が終了してから5秒後まで値の放出を継続することを意味します。これにより、UIコンポーネントが購読をやめた後も少しの間、データの更新を受け取れるようになります。
このあたりの細かな挙動や仕組みはやや複雑なので、『値の購読を最適化するための設定』というふうに捉えておくと良いかもしれません。
さて、何はともあれこれで 保存されているユーザーネーム(文字列)情報をUI状態を管理するための値として読み取る準備が整ったので、次は値を保存・更新するためのメソッドを追加しましょう。
こちらに関しては、UserRepository クラスに定義したものとほぼ同じような感じで実装することができます。
fun saveUserName(userName: String) {
viewModelScope.launch {
userRepository.saveUserName(userName)
}
}
UserRepository クラスに定義された saveUserName()
メソッドは suspend fun なので、コルーチンスコープ内で呼び出す必要があります。
これで値の読み取りと保存(更新)ができるようになりましたが、MyViewModel は UserRepository に依存しており、UserRepository はアプリケーションコンテキストに依存しているという問題が残っているので、それを解決します。
MyViewModel クラス内に、次のように companion object を定義します。
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as MyApplication)
MyAppViewModel(application.userRepository)
}
}
}
これまでのコードと比べるとより一層、何を意味しているのかがぱっと見では分かりづらいかと思いますが、このコードの目的は、MyAppViewModel のインスタンスを作成する際に、アプリケーションコンテキストから必要な依存関係(この場合はuserRepository)を注入することです。
これにより、ViewModelのテストや再利用が容易になり、依存関係の明示的な管理が可能になります。
ざっくり言えば、ViewModel(MyAppViewModel)が初期化される際のカスタマイズを行っており、それにより、MyAppViewModel が UserRepository に依存している問題を解決している…ということになります。
あとは最初に提示した MainScreen.kt を修正して、ボタンタップでデータ(ユーザー名)が永続的に保存されるようにし、保存されているデータを文字列として表示させるようにすれば完成です。
5) ビューの更新
前回までのステップで DataStore (Preferences) を使ったデータの永続的な保存と読み出し、ViewModel を使ったUI管理の準備が整ったので、あとは ViewModel 介して 保存されたデータがUIに反映されるよう、MainScreen.kt を更新していきます。
まずは、MyAppViewModel を MainScreen()
のパラメータとして次のように指定します。
@Composable
fun MainScreen(
modifier: Modifier = Modifier,
myAppViewModel: MyAppViewModel = viewModel(factory = MyAppViewModel.Factory)
) { … }
factory パラメータに MyAppViewModel で定義した Factory を渡しているため、この MyAppViewModel が初期化される際は、前章でカスタマイズした初期化設定が適用されることとなります。
次に、UI状態(ここでは保存されたユーザーネーム)を定義するため、MainScreen関数内で次のように savedUserName を定義します。
val savedUserName by myAppViewModel.uiState.collectAsState()
また、Textコンポーザブルのテキスト表示部分も合わせて変更しておきまあしょう。
Text(
text = "Hi, ${savedUserName.userName}",
style = MaterialTheme.typography.displaySmall,
modifier = Modifier
.padding(top = 32.dp)
)
最後に、ボタンクリックで TextField に入力された文字列が保存されるように onclick パラメータに渡す関数を設定すれば完成です。
Button(
onClick = { myAppViewModel.saveUserName(userInput) }
) {
Text(text = "SAVE")
}
アプリをビルドして、ユーザー名を入力してからSAVEボタンをタップした後、一度アプリを完全に終了させてから再度アプリを開いてみましょう。
すると、アプリを終了しても保存されたデータが失われず、維持されていることが確認できます。
お疲れ様でした!これで、基本的な DataStore (Preferences) の実装の完了です。
開発初心者の方は、とにかく必要なコードを覚えようとする傾向があるように思いますが、重要なのはコードを丸暗記することではありません。
テストではないのですから、コードの書き方を忘れてしまったとしても、過去に自分が書いたコードをそのままコピーしても良いのです。
むしろ、実装のたびにいちいち1からコードを記述するよりも、一度記述したコードをテンプレートとしてGitやクラウド等で管理しておき、いつでも再利用できるようにしておいた方が効率的で良いと思います。
ただし、コードをコピペするにしても、そのコードがどんな意味を持っていて、何の役割を果たすものなのかを理解しておくことは重要です。
最初は『よくわからないけれど、とりあえずやってみる』でも良いのですが、訳もわからずにコードをコピーして貼り付けるクセをつけてしまうとよくないので、少しずつでも良いので、できるだけコードを理解するように心がけると良いと思います。