[Android] 003. DataStore

だいぶ前に書いたSharedPreferences は扱いやすいけど、移行を検討しろって言われているので試してみました
https://developer.android.com/topic/libraries/architecture/datastore

Preferences DataStore

実装は↑通りにすればokなのでHilt適応版
とりあえずサクッとベースのコピペコード

[versions]
datastore = "1.1.1"
hilt = "2.52"
hiltNavigationCompose = "1.2.0"
com-google-devtools-ksp = "2.0.0-1.0.24"

[libraries]
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" }

[plugins]
com-google-devtools-ksp = {id = "com.google.devtools.ksp", version.ref = "com-google-devtools-ksp" }
com-google-dagger-hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
plugins {
    alias(libs.plugins.com.google.devtools.ksp) apply false
    alias(libs.plugins.com.google.dagger.hilt.android) apply false
}
plugins {
    alias(libs.plugins.com.google.devtools.ksp)
    alias(libs.plugins.com.google.dagger.hilt.android)
}

dependencies {
    implementation(libs.androidx.datastore.preferences)
    implementation(libs.hilt.android)
    ksp(libs.hilt.android.compiler)
    implementation(libs.androidx.hilt.navigation.compose)
}
/**
 * ```AndroidManifest.xml
 * <application
 *     android:name=".Application"
 * ```
 */
@HiltAndroidApp
class Application : android.app.Application() {
    override fun onCreate() {
        super.onCreate()
    }
}
@AndroidEntryPoint
class MainActivity : ComponentActivity() { 
    ...
    setContent {
        ...
        Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
            Screen(Modifier.padding(innerPadding))
        }
...

Preferences DataStore 定義

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

class AppDataPreferences @Inject constructor(
    private val dataStore: DataStore<Preferences>
) {
    val count = dataStore.data
        .catch {
            emit(emptyPreferences())
        }
        .map { preferences ->
            preferences[KEY_COUNT] ?: 0
        }

    suspend fun getCount(): Result<Int> = Result.runCatching {
        withContext(Dispatchers.IO) {
            count.first()
        }
    }
    suspend fun setCount(count: Int) = Result.runCatching {
        withContext(Dispatchers.IO) {
            dataStore.edit { preferences ->
                preferences[KEY_COUNT] = count
            }
        }
    }

    private companion object {
        val KEY_COUNT = intPreferencesKey(
            name = "count"
        )
    }
}

@Module
@InstallIn(SingletonComponent::class)
class DateStoreModule {
    @Singleton
    @Provides
    fun provideAppPreferences(
        @ApplicationContext context: Context,
    ) = AppDataPreferences(context.dataStore)
}

確認用UI

ボタン押したらカウント増えていきます
アプリを落としても値が保持されていたらok

@HiltViewModel
class MainViewModel @Inject constructor(
    private val appDataPreferences: AppDataPreferences,
) : ViewModel() {
    val count = appDataPreferences.count.stateIn(viewModelScope, SharingStarted.Eagerly, 0)

    fun incrementCount() = viewModelScope.launch {
        val count = appDataPreferences.getCount().getOrDefault(0)
        appDataPreferences.setCount(count + 1)
    }
}

@Composable
fun Screen(
    modifier: Modifier = Modifier,
    vm: MainViewModel = hiltViewModel()
) {
    val count by vm.count.collectAsState()
    Column(modifier) {
        Button(onClick = {
            vm.incrementCount()
        }) {
            Text(count.toString())
        }
    }
}

Proto DataStore (protobuf)

調査中…相変わらず情報が分散していてサクッと実装できないw
kotlinにすると自動生成したファイルでエラーが出る…
protobuf-bomも機能してない?

[versions]
datastore = "1.1.1"

[libraries]
androidx-datastore = { group = "androidx.datastore", name = "datastore", version.ref = "datastore" }
dependencies {
    implementation(libs.androidx.datastore)
}

android devの説明に合わせて.protoを追加

syntax = "proto3";
package com.xxx;

option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

.protoのpluginはJetBrins製のでいいか

.protoの扱い方はcodelabにあります
https://developer.android.com/codelabs/android-proto-datastore#0
ここではjavaで出力だけどkotlinもサポートしてます
https://github.com/protocolbuffers/protobuf/blob/main/java/README.md

pluginのバージョンなどはこちら
https://mvnrepository.com/artifact/com.google.protobuf/protoc

[versions]
protobufKotlinLite = "4.28.3"
protobuf = "0.9.4"

[libraries]
protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobufKotlinLite" }

[plugins]
com-google-protobuf = { id = "com.google.protobuf", version.ref = "protobuf" }
plugins {
    alias(libs.plugins.com.google.protobuf)
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:4.28.3"
    }
    generateProtoTasks {
        all().forEach {
            it.builtins {
                create("kotlin") {
                    option("lite")
                }
            }
        }
    }
}

Proto DataStore (kotlin serialization)

こっちの方が断然使いやすいような…devloperでなぜ直接紹介しない…
https://android-developers-jp.googleblog.com/2021/04/using-datastore-with-kotlin-serialization.html

main/protoに.protoの代わりにjsonを置いたりする必要もないです

@Serializable
data class AppData(
    val count: Int = 0
)

// 🙅‍♂️import kotlinx.serialization.Serializer
// 🙆‍♂️import androidx.datastore.core.Serializer
object AppDataSerializer : Serializer<AppData> {
    override val defaultValue = AppData()
    override suspend fun readFrom(input: InputStream): AppData {
        try {
            return Json.decodeFromString(AppData.serializer(), input.readBytes().decodeToString())
        } catch (serialization: SerializationException) {
            throw CorruptionException("Unable to read UserPrefs", serialization)
        }
    }

    override suspend fun writeTo(t: AppData, output: OutputStream) {
        withContext(Dispatchers.IO) {
            output.write(Json.encodeToString(AppData.serializer(), t).encodeToByteArray())
        }
    }
}

val Context.dataStore by dataStore("appData.json", serializer = AppDataSerializer)

@Composable
fun Screen(modifier: Modifier = Modifier) {
    val context = LocalContext.current
    val appData by context.dataStore.data.collectAsState(AppData())
    suspend fun setCount(count: Int) = context.dataStore.updateData { it.copy(count) }

    val scope = rememberCoroutineScope()
    Column(modifier) {
        Button(onClick = {
            scope.launch {
                setCount(appData.count + 1)
            }
        }) {
            Text("${appData.count}")
        }
    }
}

Android Studio Ladybug 2024.2.1 Patch 2 built on October 25, 2024