🙌

DataStoreを試してみる

2021/10/22に公開

DataStoreとは?

プロトコル バッファを使用して Key-Value ペアや型付きオブジェクトを格納できるSharedPreferencesの進化版

DataStore には、以下2 種類がある

  • Preferences DataStore
    • キーを使用してデータの保存およびアクセスを行います。この実装では、
      定義済みのスキーマは必要ありませんが、タイプセーフではありません。
  • Proto DataStore
    • カスタムデータ型のインスタンスとしてデータを保存します。この実装では、
      プロトコル バッファを使用してスキーマを定義する必要がありますが、タイプセーフです。

試した環境

サンプル用にこちらにリポジトリ作成しています。

macOS Catalina ver 10.15.7 (intel版)
Android Studio Arctic Fox | 2020.3.1 Pat ch 2

Preferences DataStore

まずは Preferences DataStore を試してみます。早速プロジェクトを作成し、以下を app/build.gradle に追加します。

implementation "androidx.datastore:datastore-preferences:1.0.0"

DataStore クラスと Preferences クラスを使用して、単純な Key-Value ペアをディスクに保持してみます。
Kotlinファイルのトップで以下を追加し、DataStoreをシングルトンとして扱えるようにします。

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

値を保存して読み込む処理を書いてみる

シンプルに保存 -> 読み込みを行ってログ出力を行います。

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
val TEXT_KEY = stringPreferencesKey("example_text")

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...

        GlobalScope.launch {
            saveText(this@MainActivity, "sample")

            val textFlow: Flow<String> = dataStore.data.map { p -> p[TEXT_KEY] ?: "" }
            textFlow.collect { Log.d("DataStore", "text = $it") }
        }
    }
}

suspend fun saveText(context: Context, text: String) {
    context.dataStore.edit { settings ->
        settings[TEXT_KEY] = text
    }
}

結果 DataStore: text = sample が出力されればOK

Proto DataStore

次は Proto DataStore を試してみます。

環境構築

まずは以下内容でモジュール内の build.gradle を修正します。

plugins {
    id "com.google.protobuf"
}

dependencies {
    implementation "androidx.datastore:datastore:1.0.0"
    implementation  "com.google.protobuf:protobuf-javalite:3.14.0"
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.14.0"
    }

    // Generates the java Protobuf-lite code for the Protobufs in this project. See
    // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
    // for more information.
    generateProtoTasks {
        all().each { task ->
            task.builtins {
                java {
                    option 'lite'
                }
            }
        }
    }
}

プロジェクトルートの build.gradledependencies に以下を追加します。

dependencies {
        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.17'
}

次に app/src/main/protosettings.proto を作成します。

syntax = "proto3";

option java_package = "com.slowhand.datastoresample.model";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

Settings.kt を以下内容で作成します。

package com.slowhand.datastoresample.model

import android.content.Context
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.DataStore
import androidx.datastore.core.Serializer
import androidx.datastore.dataStore
import androidx.datastore.preferences.protobuf.InvalidProtocolBufferException
import java.io.InputStream
import java.io.OutputStream

object SettingsSerializer : Serializer<Settings> {
    override val defaultValue: Settings = Settings.getDefaultInstance()

    override suspend fun readFrom(input: InputStream): Settings {
        try {
            return Settings.parseFrom(input)
        } catch (exception: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", exception)
        }
    }

    override suspend fun writeTo(
        t: Settings,
        output: OutputStream
    ) = t.writeTo(output)
}

val Context.settingsDataStore: DataStore<Settings> by dataStore(
    fileName = "settings.proto",
    serializer = SettingsSerializer
)

保存して読み込んでみます。

GlobalScope.launch {
    incrementCounter(this@MainActivity)
    val exampleCounterFlow: Flow<Int> = settingsDataStore.data
        .map { settings ->
            settings.exampleCounter
        }
    exampleCounterFlow.collect { Log.d("DataStore", "counter = $it")}
}

suspend fun incrementCounter(context: Context) {
    context.settingsDataStore.updateData { currentSettings ->
        currentSettings.toBuilder()
            .setExampleCounter(currentSettings.exampleCounter + 1)
            .build()
    }
}

counter = のログが出力されればOKです。

参考URL

Discussion