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

この記事からわかること

  • Android Studio/KotlinDataStoreを使った実装方法
  • ローカルデータ永続的保存する方法
  • アプリ停止してもデータを保持するには?
  • 保存できるデータ型種類
  • Preferences DataStoreProto 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]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

参考文献:公式リファレンス:DataStore

環境

DataStoreとは?

AndroidのDataStoreプロトコルバッファ(※)を使用してKey-Value形式や型付きオブジェクトをローカルに保存することができるJetpackのライブラリです。DataStoreを使用することでアプリを停止させても永続的にデータを保存し再度起動した時にもデータを再度取得することが可能になります。

※プロトコルバッファとはGoogleが開発した、データをバイト列などの形式に変換しネットワークやファイルに保存・転送する仕組みのこと

DataStoreではKotlin CoroutinesとFlowオブジェクトを活用することでデータを非同期的に一貫した形でトランザクションとして保存することができるようになっています。そのためKotlin Coroutinesの扱いには慣れておく必要があります。

また保存できる型は以下の通りです。

SharedPreferencesは非推奨?

似たようなものにSharedPreferencesがありますが、公式よりSharedPreferencesではなくDataStoreを使うことが推奨されています。現在SharedPreferencesを使用してデータを保存している場合は、DataStoreに移行することを検討するよう公式がアナウンスしているようです。

またローカルにデータを永続的に保存する別の方法としてRoomDataBaseもあります。

Preferences DataStoreとProto DataStore

DataStoreの中でも大きく分けて2種類に分かれます。

Preferences DataStore

Key-Value形式でデータの保存およびアクセスを行う。定義済みのスキーマは必要ないがタイプセーフではない

Proto DataStore

カスタムデータ型のインスタンスとしてデータを保存。プロトコルバッファを使用してスキーマを定義する必要があるがタイプセーフ

Preferences DataStoreの使い方

ではここからはDataStoreを使用する方法をまとめていきます。今回はPreferences DataStoreを使用していきます。

流れ

  1. 依存関係の追加
  2. Datastore<Preferences>のインスタンスを作成
  3. インスタンスに保存する必要がある各値のキーを定義

依存関係の追加

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型ならintPreferencesKeyString型なら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()
    }
}

全体のコード

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()
    }
}

observeCurrentUserFlow<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.Preferencesandroidx.datastore.preferences.core.Preferencesに入れ替えることでこのエラーは解決します。

No type arguments expected for class Flow

Flow<Int>を記述した際に以下のようなエラーが発生することがあります。

No type arguments expected for class Flow

これもimport文が正しく読み込まれていないために発生するエラーです。なのでjava.util.concurrent.Flowkotlinx.coroutines.flow.Flowに入れ替えることでこのエラーは解決します。

//import kotlinx.coroutines.flow.Flow // ○
  
import java.util.concurrent.Flow // ×

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index