【Android】 DataStoreを試してみる

# [DataStore](https://developer.android.com/topic/libraries/architecture/datastore?hl=ja)とは?
> プロトコル バッファを使用して Key-Value ペアや型付きオブジェクトを格納できるSharedPreferencesの進化版

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

## 試した環境
サンプル用に[こちら](https://github.com/Slowhand0309/DataStoreSample)にリポジトリ作成しています。

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

## Preferences DataStore

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

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

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

```kotlin
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")
```

### 値を保存して読み込む処理を書いてみる
シンプルに保存 -> 読み込みを行ってログ出力を行います。

```kotlin
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` を修正します。

```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.gradle` の `dependencies` に以下を追加します。

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

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

```proto
syntax = "proto3";

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

message Settings {
  int32 example_counter = 1;
}
```

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

```kotlin
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
)
```

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

```kotlin
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
- [Jetpack DataStore入門〜Preferences DataStore実装編〜 - Sansan Builders Blog](https://buildersbox.corp-sansan.com/entry/2021/02/24/110000)
- [CoroutinesのlaunchとasyncとChannelとFlowの使い分け - Qiita](https://qiita.com/naoi/items/9892db4cec2e9c0f6114)
- [Jetpack Compose の例 - たくさんの自由帳](https://takusan.negitoro.dev/posts/android_jc_recipe/)
- [コンポーザブルのライフサイクル  |  Jetpack Compose  |  Android Developers](https://developer.android.com/jetpack/compose/lifecycle)
- [Android Jetpack Proto DataStore. A Jetpack recommended solution for… | by Satya Pavan Kantamani | ProAndroidDev](https://proandroiddev.com/android-jetpack-proto-datastore-a11ff8edcda8)
- [公式ページを眺めているだけではわからないJetpack DataStoreのあれこれ - Qiita](https://qiita.com/que9/items/54c60d9cbca1dfaffdbf)