【Kotlin/Android Studio】DataStoreの使い方!データの保存と取得方法

この記事からわかること
- Android Studio/KotlinでDataStoreを使った実装方法
- ローカルにデータを永続的に保存する方法
- アプリを停止してもデータを保持するには?
- 保存できるデータ型の種類
- Preferences DataStoreとProto DataStoreの違い
- No type arguments expected for class Flowの解決法
- Property delegate must have a 'getValue(Context, KProperty<*>)' method. None of the following functions is suitable:...の解決法
index
[open]
\ アプリをリリースしました /
参考文献:公式リファレンス:DataStore
環境
- Android Studio:Flamingo
- Kotlin:1.8.20
DataStoreとは?
AndroidのDataStoreはプロトコルバッファ(※)を使用してKey-Value形式や型付きオブジェクトをローカルに保存することができるJetpackのライブラリです。DataStoreを使用することでアプリを停止させても永続的にデータを保存し再度起動した時にもデータを再度取得することが可能になります。
※プロトコルバッファとはGoogleが開発した、データをバイト列などの形式に変換しネットワークやファイルに保存・転送する仕組みのこと
DataStoreではKotlin CoroutinesとFlowオブジェクトを活用することでデータを非同期的に一貫した形でトランザクションとして保存することができるようになっています。そのためKotlin Coroutinesの扱いには慣れておく必要があります。
また保存できる型は以下の通りです。
- int
- long
- float
- boolean
- String
- Set<String>
SharedPreferencesは非推奨?
似たようなものにSharedPreferencesがありますが、公式よりSharedPreferencesではなくDataStoreを使うことが推奨されています。現在SharedPreferencesを使用してデータを保存している場合は、DataStoreに移行することを検討するよう公式がアナウンスしているようです。
またローカルにデータを永続的に保存する別の方法としてRoomDataBase
もあります。
Preferences DataStoreとProto DataStore
DataStoreの中でも大きく分けて2種類に分かれます。
Preferences DataStore
Key-Value形式でデータの保存およびアクセスを行う。定義済みのスキーマは必要ないがタイプセーフではない
Proto DataStore
カスタムデータ型のインスタンスとしてデータを保存。プロトコルバッファを使用してスキーマを定義する必要があるがタイプセーフ
Preferences DataStoreの使い方
ではここからはDataStoreを使用する方法をまとめていきます。今回はPreferences DataStoreを使用していきます。
流れ
- 依存関係の追加
- Datastore<Preferences>のインスタンスを作成
- インスタンスに保存する必要がある各値のキーを定義
依存関係の追加
DataStoreを実装するためには依存関係を追加する必要があります。使用する種類によって追加するコードが変わります。
Preferences DataStore
dependencies {
// 〜〜〜〜〜〜〜〜〜〜〜
implementation("androidx.datastore:datastore-preferences:1.0.0")
}
Proto DataStore
dependencies {
// 〜〜〜〜〜〜〜〜〜〜〜
implementation("androidx.datastore:datastore:1.0.0")
}
Datastore<Preferences>のインスタンスを作成
Datastoreを使用するにはまずDatastore<Preferences>インスタンスを作成する必要があります。これはkotlinファイルの最上位でインスタンス生成を1回呼び出し、定義したプロパティを介して他の下層のファイルからアクセスします。なので名前はなんでも良いですが例えば「DataStoreExtensions.kt」というファイル名を作り、以下のimport
文も含めてペーストしてください。name
にはアプリ内で使用するDatastoreの用途がわかるようなDB名をつけてください。またこのインスタンスはシングルトンになります。
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.preferencesDataStore
import androidx.datastore.preferences.core.Preferences
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
ここで手打ちで入力するとAndroid Studioのバージョンによっては自動補完で異なるimport
文が記述され以下のようなエラーが発生(後半で紹介してます)してしまいます。
Property delegate must have a 'getValue(Context, KProperty<*>)' method. None of the following functions is suitable:
public abstract operator fun getValue(thisRef: Context, property: KProperty<*>): DataStore<Preferences> defined in kotlin.properties.ReadOnlyProperty
インスタンスに保存する必要がある各値のキーを定義
Preference DataStoreでは定義済みのスキーマを使用しないため、任意のキーを定義する関数を使用してDataStore<Preferences>インスタンスに保存する値のキーを定義する必要があります。キーを定義する関数はデータ型によって異なりInt
型ならintPreferencesKey
、String
型ならstringPreferenceKey
を使用します。
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
これでDataStoreを使う準備が整ったので実際にデータを保存、取得してみたいと思います。
データを保存する
ではこのまま「カウンターアプリ」を作っていきます。ここからは「MainActivity.kt」内に記述していきます。
データを保存するには生成したdataStore
インスタンスからedit
メソッドを呼び出します。引数からMutablePreference
型が取得できるので定義した名前の変数settings
を用意して受け取ります。あとはMutablePreference[キー]
形式で参照できるので新しい値を格納します。
suspend fun incrementCounter() {
applicationContext.dataStore.edit { settings ->
val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
settings[EXAMPLE_COUNTER] = currentCounterValue + 1
}
}
また非同期処理で実行されることを想定してsuspend
関数(Coroutinesのコード)にしています。あとはボタンのクリックイベントなどから呼び出せばOKです。
val addButton:Button = findViewById(R.id.add_button)
val getButton:Button = findViewById(R.id.get_button)
addButton.setOnClickListener{
GlobalScope.launch (Dispatchers.IO) {
incrementCounter()
}
}
保存されたデータは/data/app/パッケージ名/files/datastore
内に保管されます。
データを取得する
データを取得するためにはまずFlow
オブジェクトを取得します。dataStore.data
からDataStoreに保存されているデータにアクセスできます。その際に例外が発生する可能性があるのでcatch
で例外を捕捉しています。最後にmap
を使用して実際のデータFlow<Int>
を取得しています。
val exampleCounterFlow: Flow<Int> = dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
preferences[EXAMPLE_COUNTER] ?: 0
}
またこの際にNo type arguments expected for class Flow
というエラーが発生することがありますが詳細はこちらをご覧ください。
あとはこれを元にデータを取得しUIに反映させる処理を実装してみます。
suspend fun getCounter() {
val text:TextView = findViewById(R.id.activity_text)
val exampleCounterFlow: Flow<Int> = dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
preferences[EXAMPLE_COUNTER] ?: 0
}
text.text = exampleCounterFlow.first().toString()
}
呼び出し時は以下のような感じです。
val getButton:Button = findViewById(R.id.get_button)
getButton.setOnClickListener{
runBlocking {
getCounter()
}
}
Flowでなくデータをそのまま取得する
データを1回だけ取得したい場合はfirst
を使用します。
public fun getCounter(): Int {
return runBlocking {
try {
val preferences = context.dataStore.data.first()
preferences[EXAMPLE_COUNTER] ?: 0
} catch (e: IOException) {
Log.d("DataStore", "Failed to read preferences", e)
0
} catch (e: Exception) {
Log.d("DataStore", "Unexpected error", e)
0
}
}
}
全体のコード
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.IOException
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val addButton:Button = findViewById(R.id.add_button)
val getButton:Button = findViewById(R.id.get_button)
addButton.setOnClickListener{
GlobalScope.launch (Dispatchers.IO) {
incrementCounter()
}
}
getButton.setOnClickListener{
runBlocking {
getCounter()
}
}
}
suspend fun incrementCounter() {
applicationContext.dataStore.edit { settings ->
val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
settings[EXAMPLE_COUNTER] = currentCounterValue + 1
}
}
suspend fun getCounter() {
val text:TextView = findViewById(R.id.activity_text)
val exampleCounterFlow: Flow<Int> = dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
preferences[EXAMPLE_COUNTER] ?: 0
}
text.text = exampleCounterFlow.first().toString()
}
}
DataStoreを管理するクラスを作成してみる
ここまでの紹介では実際のアプリに組み込んだ際の汎用性が拡張性が全くないのでもう少し使いやすくしていきます。次は「カウンター」ではなくユーザー名を保存できるようにしていきます。dataStore
の定義は変わりませんが、キーの定義と保存・取得のロジックを保持するクラスに切り出してみました。
class DataStoreManager(private val context: Context) {
companion object {
val CURRENT_USER = stringPreferencesKey("current_user")
}
suspend fun saveCurrentUser(currentUser: String) {
try {
context.dataStore.edit { preferences ->
preferences[CURRENT_USER] = currentUser
}
} catch (e: IOException) {
print("例外が発生したよ")
}
}
public fun observeCurrentUser(): Flow<String?> {
return context.dataStore.data.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}.map { preferences ->
preferences[CURRENT_USER]
}
}
}
Activity側では以下のように書けるようになりました。
val dataStoreManager = DataStoreManager(this)
button.setOnClickListener {
runBlocking(Dispatchers.IO) {
dataStoreManager.saveCurrentUser(editText.text.toString())
}
}
lifecycleScope.launch{
// CURRENT_USERを観察し、変更があれば自動更新されるようにする
dataStoreManager.observeCurrentUser().collect {
text.text = it.toString()
}
}
observeCurrentUser
でFlow<String?>
オブジェクトを受け取りcollect
メソッドで受け取った値に対しての処理を定義しています。
Property delegate must have a 'getValue(Context, KProperty<*>)' method. None of the following functions is suitable:public abstract operator fun getValue(thisRef: Context, property: KProperty<*>): DataStore<Preferences> defined in kotlin.properties.ReadOnlyProperty
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
上記のDataStoreのDatastore<Preferences>のインスタンスを作成するコードをファイルに記述して実行すると以下のようなエラーが発生することがあります。
Property delegate must have a 'getValue(Context, KProperty<*>)' method. None of the following functions is suitable:
public abstract operator fun getValue(thisRef: Context, property: KProperty<*>): DataStore<Preferences> defined in kotlin.properties.ReadOnlyProperty
これはimport
文が正しく読み込まれていないために発生しています。本来ならandroidx.datastore.preferences.core.Preferences
が正しいのですが自動補完でに任せるとjava.util.prefs.Preferences
が記述されてしまいます。
import androidx.datastore.preferences.core.Preferences // ○
import java.util.prefs.Preferences // ×
なのでjava.util.prefs.Preferences
をandroidx.datastore.preferences.core.Preferences
に入れ替えることでこのエラーは解決します。
No type arguments expected for class Flow
Flow<Int>
を記述した際に以下のようなエラーが発生することがあります。
No type arguments expected for class Flow
これもimport
文が正しく読み込まれていないために発生するエラーです。なのでjava.util.concurrent.Flow
をkotlinx.coroutines.flow.Flow
に入れ替えることでこのエラーは解決します。
//import kotlinx.coroutines.flow.Flow // ○
import java.util.concurrent.Flow // ×
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。