feat: datastores (#392)

Co-authored-by: Florent Champigny <florent@bere.al>
This commit is contained in:
Florent CHAMPIGNY 2025-11-18 21:01:13 +01:00 committed by GitHub
parent 17fd651bae
commit ce4c8f5a0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 420 additions and 13 deletions

View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,88 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
id("com.vanniktech.maven.publish") version "0.34.0"
}
android {
namespace = "io.github.openflocon.flocon.datastores"
compileSdk = 36
defaultConfig {
minSdk = 23
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
}
dependencies {
implementation(project(":flocon-base"))
implementation(platform(libs.kotlinx.coroutines.bom))
implementation(libs.jetbrains.kotlinx.coroutines.core)
implementation(libs.androidx.datastore.preferences)
}
mavenPublishing {
publishToMavenCentral(automaticRelease = true)
if (project.hasProperty("signing.required") && project.property("signing.required") == "false") {
// Skip signing
} else {
signAllPublications()
}
coordinates(
groupId = project.property("floconGroupId") as String,
artifactId = "flocon-datastores-no-op",
version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String
)
pom {
name = "Flocon Datastores Integration No Op"
description = project.property("floconDescription") as String
inceptionYear = "2025"
url = "https://github.com/openflocon/Flocon"
licenses {
license {
name = "The Apache License, Version 2.0"
url = "https://www.apache.org/licenses/LICENSE-2.0.txt"
distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt"
}
}
developers {
developer {
id = "openflocon"
name = "Open Flocon"
url = "https://github.com/openflocon"
}
}
scm {
url = "https://github.com/openflocon/Flocon"
connection = "scm:git:git://github.com/openflocon/Flocon.git"
developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git"
}
}
}

View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View file

@ -0,0 +1,37 @@
package io.github.openflocon.flocon.preferences.datastores.model
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.doublePreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import io.github.openflocon.flocon.FloconLogger
import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconPreference
import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconPreferenceValue
import kotlinx.coroutines.flow.first
class FloconDatastorePreference(
override val name: String,
val dataStore: DataStore<Preferences>,
) : FloconPreference {
override suspend fun set(
columnName: String,
value: FloconPreferenceValue
) {
// no op
}
override suspend fun columns(): List<String> {
return emptyList() // no op
}
override suspend fun get(columnName: String): FloconPreferenceValue? {
return null // no op
}
}

1
FloconAndroid/datastores/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,89 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
id("com.vanniktech.maven.publish") version "0.34.0"
}
android {
namespace = "io.github.openflocon.flocon.datastores"
compileSdk = 36
defaultConfig {
minSdk = 23
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
}
dependencies {
implementation(project(":flocon-base"))
implementation(platform(libs.kotlinx.coroutines.bom))
implementation(libs.jetbrains.kotlinx.coroutines.core)
implementation(libs.jetbrains.kotlinx.coroutines.android)
implementation(libs.androidx.datastore.preferences)
}
mavenPublishing {
publishToMavenCentral(automaticRelease = true)
if (project.hasProperty("signing.required") && project.property("signing.required") == "false") {
// Skip signing
} else {
signAllPublications()
}
coordinates(
groupId = project.property("floconGroupId") as String,
artifactId = "flocon-datastores",
version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String
)
pom {
name = "Flocon Datastores Integration"
description = project.property("floconDescription") as String
inceptionYear = "2025"
url = "https://github.com/openflocon/Flocon"
licenses {
license {
name = "The Apache License, Version 2.0"
url = "https://www.apache.org/licenses/LICENSE-2.0.txt"
distribution = "https://www.apache.org/licenses/LICENSE-2.0.txt"
}
}
developers {
developer {
id = "openflocon"
name = "Open Flocon"
url = "https://github.com/openflocon"
}
}
scm {
url = "https://github.com/openflocon/Flocon"
connection = "scm:git:git://github.com/openflocon/Flocon.git"
developerConnection = "scm:git:ssh://git@github.com/openflocon/Flocon.git"
}
}
}

View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View file

@ -0,0 +1,80 @@
package io.github.openflocon.flocon.preferences.datastores.model
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.doublePreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import io.github.openflocon.flocon.FloconLogger
import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconPreference
import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconPreferenceValue
import kotlinx.coroutines.flow.first
class FloconDatastorePreference(
override val name: String,
private val dataStore: DataStore<Preferences>,
) : FloconPreference {
override suspend fun set(
columnName: String,
value: FloconPreferenceValue
) {
try {
val data = dataStore.data.first().asMap()
val key = data.keys.find { it.name == columnName } ?: return
dataStore.edit {
try {
when (it[key]) {
is String -> it[stringPreferencesKey(columnName)] = value.stringValue!!
is Int -> it[intPreferencesKey(columnName)] = value.intValue!!
is Boolean -> it[booleanPreferencesKey(columnName)] = value.booleanValue!!
is Float -> it[floatPreferencesKey(columnName)] = value.floatValue!!
is Long -> it[longPreferencesKey(columnName)] = value.longValue!!
is Double -> it[doublePreferencesKey(columnName)] =
value.floatValue!!.toDouble()
}
} catch (t: Throwable) {
FloconLogger.logError("cannot update datastore preference", t)
}
}
} catch (t: Throwable) {
FloconLogger.logError(t.message ?: "cannot edit datastore preference columns", t)
}
}
override suspend fun columns(): List<String> {
return try {
dataStore.data.first().asMap().map { it.key.name }
} catch (t: Throwable) {
FloconLogger.logError(t.message ?: "cannot get datastore preference columns", t)
emptyList()
}
}
override suspend fun get(columnName: String): FloconPreferenceValue? {
return try {
val data = dataStore.data.first().asMap()
val key = data.keys.find { it.name == columnName } ?: return null
val value = data[key] ?: return null
return when (value) {
is String -> FloconPreferenceValue(stringValue = value)
is Int -> FloconPreferenceValue(intValue = value)
is Float -> FloconPreferenceValue(floatValue = value)
is Double -> FloconPreferenceValue(floatValue = value.toFloat())
is Boolean -> FloconPreferenceValue(booleanValue = value)
is Long -> FloconPreferenceValue(longValue = value)
else -> null
}
} catch (t: Throwable) {
FloconLogger.logError(t.message ?: "cannot get datastore preference value", t)
null
}
}
}

View file

@ -4,7 +4,7 @@ interface FloconPreference {
val name: String
suspend fun set(
rowName: String,
columnName: String,
value: FloconPreferenceValue,
)

View file

@ -91,7 +91,7 @@ mavenPublishing {
)
pom {
name = "Flocon"
name = "Flocon No Op"
description = project.property("floconDescription") as String
inceptionYear = "2025"
url = "https://github.com/openflocon/Flocon"

View file

@ -1,4 +1,4 @@
package io.github.openflocon.flocon.sharedprefs
package io.github.openflocon.flocon.plugins.sharedprefs
import android.content.SharedPreferences
import io.github.openflocon.flocon.plugins.sharedprefs.model.FloconPreference
@ -10,7 +10,7 @@ data class FloconSharedPreference(
) : FloconPreference {
override suspend fun set(
rowName: String,
columnName: String,
value: FloconPreferenceValue
) {
// no op

View file

@ -10,22 +10,22 @@ data class FloconSharedPreference(
) : FloconPreference {
override suspend fun set(
rowName: String,
columnName: String,
value: FloconPreferenceValue
) {
val originalValue: Any? = sharedPreferences.all[rowName]
val originalValue: Any? = sharedPreferences.all[columnName]
val editor = sharedPreferences.edit()
if (originalValue is Boolean && value.booleanValue != null) {
editor.putBoolean(rowName, value.booleanValue!!)
editor.putBoolean(columnName, value.booleanValue!!)
} else if (originalValue is Long && value.longValue != null) {
editor.putLong(rowName, value.longValue!!)
editor.putLong(columnName, value.longValue!!)
} else if (originalValue is Int && value.intValue != null) {
editor.putInt(rowName, value.intValue!!)
editor.putInt(columnName, value.intValue!!)
} else if (originalValue is Float && value.floatValue != null) {
editor.putFloat(rowName, value.floatValue!!)
editor.putFloat(columnName, value.floatValue!!)
} else if (originalValue is String && value.stringValue != null) {
editor.putString(rowName, value.stringValue!!)
editor.putString(columnName, value.stringValue!!)
} else {
// do nothin
}

View file

@ -67,7 +67,7 @@ internal class FloconPreferencesPluginImpl(
scope.launch {
try {
preference.set(
rowName = toDeviceMessage.key,
columnName = toDeviceMessage.key,
value = FloconPreferenceValue(
stringValue = toDeviceMessage.stringValue,
booleanValue = toDeviceMessage.booleanValue,

View file

@ -3,6 +3,7 @@ agp = "8.11.0-rc02"
apollo = "4.0.0"
coilCompose = "3.2.0"
compose = "1.9.0"
datastorePreferences = "1.1.7"
kotlin = "2.1.0"
mavenPublish = "0.34.0"
coreKtx = "1.16.0"
@ -33,6 +34,7 @@ buildconfig = "5.6.8"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }

View file

@ -8,7 +8,9 @@
:okhttp-interceptor:assembleRelease \
:okhttp-interceptor-no-op:assembleRelease \
:ktor-interceptor:assembleRelease \
:ktor-interceptor-no-op:assembleRelease
:ktor-interceptor-no-op:assembleRelease \
:datastores:assembleRelease \
:datastores-no-op:assembleRelease
./gradlew \
:flocon-base:publishToMavenLocal \
@ -21,4 +23,6 @@
:okhttp-interceptor-no-op:publishToMavenLocal \
:ktor-interceptor:publishToMavenLocal \
:ktor-interceptor-no-op:publishToMavenLocal \
:datastores:publishToMavenLocal \
:datastores-no-op:publishToMavenLocal \
-Psigning.required=false

View file

@ -84,6 +84,8 @@ dependencies {
implementation(project(":grpc:grpc-interceptor-lite"))
debugImplementation(project(":ktor-interceptor"))
releaseImplementation(project(":ktor-interceptor-no-op"))
debugImplementation(project(":datastores"))
releaseImplementation(project(":datastores-no-op"))
}
@ -135,6 +137,10 @@ dependencies {
implementation(libs.ktor.client.okhttp)
implementation(libs.ktor.client.core)
//endregion
// region datastore
implementation(libs.androidx.datastore.preferences)
// endregion
}
apollo {

View file

@ -26,6 +26,7 @@ import io.github.openflocon.flocon.myapplication.deeplinks.initializeDeeplinks
import io.github.openflocon.flocon.myapplication.graphql.GraphQlTester
import io.github.openflocon.flocon.myapplication.grpc.GrpcController
import io.github.openflocon.flocon.myapplication.images.initializeImages
import io.github.openflocon.flocon.myapplication.sharedpreferences.initializeDatastores
import io.github.openflocon.flocon.myapplication.sharedpreferences.initializeSharedPreferences
import io.github.openflocon.flocon.myapplication.sharedpreferences.initializeSharedPreferencesAfterInit
import io.github.openflocon.flocon.myapplication.table.initializeTable
@ -68,6 +69,7 @@ class MainActivity : ComponentActivity() {
initializeDeeplinks()
initializeSharedPreferencesAfterInit(applicationContext)
initializeDatastores(applicationContext)
val dummyHttpCaller = DummyHttpCaller(client = okHttpClient)
val dummyWebsocketCaller = DummyWebsocketCaller(client = okHttpClient)

View file

@ -0,0 +1,47 @@
package io.github.openflocon.flocon.myapplication.sharedpreferences
import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import io.github.openflocon.flocon.plugins.sharedprefs.floconRegisterPreference
import io.github.openflocon.flocon.preferences.datastores.model.FloconDatastorePreference
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
private val Context.dataStore by preferencesDataStore(name = "datastore")
private val USER_NAME = stringPreferencesKey("username")
private val USER_AGE = intPreferencesKey("user.age")
fun initializeDatastores(context: Context) {
Datastores(context)
}
class Datastores(private val context: Context) {
suspend fun saveUsername(username: String) {
val value = context.dataStore.data.first()[USER_NAME]
println("Datastore Local Value : $value")
if (value == null) {
context.dataStore.edit { prefs ->
prefs[USER_NAME] = username
}
}
}
suspend fun saveUserAge(age: Int) {
context.dataStore.edit { prefs ->
prefs[USER_AGE] = age
}
}
init {
floconRegisterPreference(FloconDatastorePreference("datastore", context.dataStore))
GlobalScope.launch {
saveUsername("John Doe")
saveUserAge(30)
}
}
}

View file

@ -27,3 +27,5 @@ include(":grpc:grpc-interceptor-base")
include(":grpc:grpc-interceptor-lite")
include(":ktor-interceptor")
include(":ktor-interceptor-no-op")
include(":datastores")
include(":datastores-no-op")