DataStoreのProtoBufの定義をdata classに書けると嬉しい

# はじめに
[CodeLabs](https://codelabs.developers.google.com/?cat=android) にDataStoreの[プラクティス](https://developer.android.com/codelabs/android-proto-datastore)があります。

それによると、ProtoBufの定義は[Proto language](https://developers.google.com/protocol-buffers/docs/overview)で書く必要があり、
またその.protoファイルは `app/src/main/proto` に配置する必要がありました。
更にenumを使う場合はその定義自体も.protoに書いてJava / Kotlinのコードから消すことで2重管理を回避していました。

ここで気になったのは以下の点です。

- DataStoreに関する一部のコードだけProto language で書く必要がありなんか美しくない
- Android Studioのプロジェクトツリーで表示タイプをAndroidにすると`app/src/main/proto`が見えない
- Enumを.protoで定義し直すのが面倒。Enumに定数やメソッドを追加している場合どうするの？

[kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) ではProtoBufを扱うことができ、定義はdata classを使っています。
DataStoreのデータ定義もこれに対応できれば上記の懸念が解決できると思い試してみました。


# プラクティスの改造
前述のCodeLabsのプラクティスに沿って変更してみます。
結論だけ見たい方はまとめへ

##5. Proto DataStore - overview

```gradle:変更前
plugins {
    ...
    id "com.google.protobuf" version "0.8.12"
}

dependencies {
    implementation  "androidx.datastore:datastore-core:1.0.0-alpha04"
    implementation  "com.google.protobuf:protobuf-javalite:3.10.0"
    ...
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.10.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'
                }
            }
        }
    }
}
```
変更前との共通点はdatastore-coreだけです。

```gradle:変更後
plugins{
    ...
    id 'org.jetbrains.kotlin.plugin.serialization' version '1.4.10' //追加
}
android{
    kotlinOptions {
        freeCompilerArgs += [
                "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
                "-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi" // 追加
        ]
    }
}
dependencies {
    implementation  "androidx.datastore:datastore-core:1.0.0-alpha05"      //最新化(2020/12/22時点)
    implementation "org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.0.1"  //追加
    ...
}

// protobuf部分は不要
```

##6 Defining and using protobuf objects
### Create the proto file
protoファイルは必要ありません。

```protobuf:app/src/main/proto/user_prefs.proto(不要)
syntax = "proto3";

option java_package = "com.codelab.android.datastore";
option java_multiple_files = true;

message UserPreferences {
  // filter for showing / hiding completed tasks
  bool show_completed = 1;
}
```

代わりにdata classを作って＠Serializableをつけましょう。
protoBufは順番が重要なので@ProtoNumberも書いておいた方がいいです。

```kotlin:UserPreferences.kt(変更後-新規作成)
package com.codelab.android.datastore.data
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber

@Serializable
data class UserPreferences(
//初期値も設定しておく
    @ProtoNumber(1) val showCompleted : Boolean = false
)
```

### Create the serializer
``` kotlin:UserPreferencesSerializer.kt(変更前)
object UserPreferencesSerializer : Serializer<UserPreferences> {
    override fun readFrom(input: InputStream): UserPreferences {
        try {
            return UserPreferences.parseFrom(input)
        } catch (exception: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", exception)
        }
    }

    override fun writeTo(t: UserPreferences, output: OutputStream) = t.writeTo(output)
}
```

Serializationの書き方に変えます。読み込み失敗時に発生するのはSerializationExceptionです。

```kotlin:UserPreferencesSerializer.kt(変更後)
package com.codelab.android.datastore.data

import kotlinx.serialization.SerializationException
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray
import kotlinx.serialization.protobuf.ProtoBuf
import java.io.InputStream
import java.io.OutputStream

object UserPreferencesSerializer : androidx.datastore.core.Serializer<UserPreferences> {
    override fun readFrom(input: InputStream): UserPreferences {
        try {
            return ProtoBuf.decodeFromByteArray(input.readBytes())    //変更
        } catch (exception: SerializationException){                  //変更
            throw CorruptionException("Cannot read proto.", exception)
        }
    }
    override fun writeTo(t: UserPreferences, output: OutputStream) = 
        output.write(ProtoBuf.encodeToByteArray(t))                   //変更
    override val defaultValue: UserPreferences    //alpha05では defaultValueも必要なので追加
        get() = UserPreferences()
}

```
## 7. Persisting data in Proto DataStore
### Creating the DataStore
変更ありません。

```kotlin:UserPreferencesRepository(変更なし)
private val dataStore: DataStore<UserPreferences> =
    context.createDataStore(
        fileName = "user_prefs.pb",
        serializer = UserPreferencesSerializer)
```
### Reading data from Proto DataStore
```kotlin:UserPreferencesRepository(変更前)
private val TAG: String = "UserPreferencesRepo"

val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
    .catch { exception ->
        // dataStore.data throws an IOException when an error is encountered when reading data
        if (exception is IOException) {
            Log.e(TAG, "Error reading sort order preferences.", exception)
            emit(UserPreferences.getDefaultInstance())
        } else {
            throw exception
        }
    }
```

変更後、初期値はdata class定義時に設定しているので引数なしでインスタンス化するだけ

```kotlin:UserPreferencesRepository(変更後)
private val TAG: String = "UserPreferencesRepo"

val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
    .catch { exception ->
        // dataStore.data throws an IOException when an error is encountered when reading data
        if (exception is IOException) {
            Log.e(TAG, "Error reading sort order preferences.", exception)
            emit(UserPreferences())      // <-変更
        } else {
            throw exception
        }
    }
```
### Writing data to Proto DataStore
```kotlin:UserPreferencesRepository(変更前)
suspend fun updateShowCompleted(completed: Boolean) {
    dataStore.updateData { preferences ->
        preferences.toBuilder().setShowCompleted(completed).build()
    }
}
```
変更後、data classに toBuilderはないけど元の値をコピーして必要な箇所を変更すれば同じ。はず。
(data classの変数はvalで宣言しているためcopy時の変更しか受け付けないつもりです。)

```kotlin:UserPreferencesRepository(変更後)
suspend fun updateShowCompleted(completed: Boolean) {
    dataStore.updateData { preferences ->
        preferences.copy(showCompleted = completed) // <-変更
    }
}
```
## 8. SharedPreferences to Proto DataStore
### Defining the data to be saved in proto
SortOrderの対応ですがEnumに変更はなくそのまま使えます。
初期値を設定しておけばUNSPECIFIEDを生やす必要はないでしょう。

```kotlin:UserPreferences.kt
package com.codelab.android.datastore.data

import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber

@Serializable
data class UserPreferences(
//初期値も設定しておく
    @ProtoNumber(1) val showCompleted: Boolean = false,
    @ProtoNumber(2) val sortOrder: SortOrder = SortOrder.NONE  //追加
)
```
### Migrating from SharedPreferences
ここも対応不要です。data class 作成時に初期値を設定しているのが効いてます。
### Saving the sort order to DataStore
```kotlin:UserPreferencesRepository(変更前)
suspend fun enableSortByDeadline(enable: Boolean) {
    // updateData handles data transactionally, ensuring that if the sort is updated at the same
    // time from another thread, we won't have conflicts
    dataStore.updateData { preferences ->
        val currentOrder = preferences.sortOrder
        val newSortOrder =
            if (enable) {
                if (currentOrder == SortOrder.BY_PRIORITY) {
                    SortOrder.BY_DEADLINE_AND_PRIORITY
                } else {
                    SortOrder.BY_DEADLINE
                }
            } else {
                if (currentOrder == SortOrder.BY_DEADLINE_AND_PRIORITY) {
                    SortOrder.BY_PRIORITY
                } else {
                    SortOrder.NONE
                }
            }
        preferences.toBuilder().setSortOrder(newSortOrder).build()
    }
}
```
更新行だけcopyに変えます。

```kotlin:UserPreferencesRepository(変更後)
suspend fun enableSortByDeadline(enable: Boolean) {
    dataStore.updateData { preferences ->
        val currentOrder = preferences.sortOrder
        val newSortOrder =
            if (enable) {
                if (currentOrder == SortOrder.BY_PRIORITY) {
                    SortOrder.BY_DEADLINE_AND_PRIORITY
                } else {
                    SortOrder.BY_DEADLINE
                }
            } else {
                if (currentOrder == SortOrder.BY_DEADLINE_AND_PRIORITY) {
                    SortOrder.BY_PRIORITY
                } else {
                    SortOrder.NONE
                }
            }
        preferences.copy(sortOrder = newSortOrder)    //  <-変更
    }
}
```
## 9. Update TasksViewModel to use UserPreferencesRepository
ViewMode側には変更ありません。

## 10. 追加作業(pro-guard対応)
Androidのリリース版で動作させるにはproguardの難読化を回避する必要があります。
https://github.com/Kotlin/kotlinx.serialization#android
今回の場合だと以下の記載を追加することになります。

```pro:proguard-rules.pro
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations

# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
#noinspection ShrinkerUnresolvedReference
-keepclassmembers class kotlinx.serialization.protobuf.** {
    *** Companion;
}
-keepclasseswithmembers class kotlinx.serialization.protobuf.** {
    kotlinx.serialization.KSerializer serializer(...);
}

# Change here com.yourcompany.yourpackage
-keep,includedescriptorclasses class com.codelab.android.datastore.data.**$$serializer { *; } # <-- change package name to your app's
-keepclassmembers class com.codelab.android.datastore.data.** { # <-- change package name to your app's
    *** Companion;
}
-keepclasseswithmembers class com.codelab.android.datastore.data.** { # <-- change package name to your app's
    kotlinx.serialization.KSerializer serializer(...);
}
```
`#noinspection ShrinkerUnresolvedReference` はバグ対応です。(参考: https://issuetracker.google.com/issues/153616200)

# まとめ
kotlinx.serializationを使ってもDataStoreのprotoBuf化に成功しました。
.protoを触ることなく、enumも変更しなくていいのでかなり実装が楽になります。
SharedPreferencesからDataStoreへの乗り換えが捗りますね。

変更内容は以下の通り
1. dataStore用のdata classに@Serializableを設定。 @ProtoNumber設定も推奨
2. Serializerはserializationの書き方に変更
3. 読み取り時は変更なし/ 更新時はbuilderの代わりにcopyを使う

ポイントはSerializerが`kotlinx.serialization`にも`androidx.datastore`にもあるのでインポート元を間違えないことでしょうか。
SerializableはRoomの@Entityなdata classにも適用できるので、DataStoreとRoomの中身をまとめてprotoBufでバイナリにしてバックアップするなどが簡単にできますね。