Skip to content

Commit 6040ec7

Browse files
Replaced previous Settings and ObservableSettings implementations with SuspendSettings and FlowSettings
1 parent 49d730c commit 6040ec7

File tree

8 files changed

+202
-172
lines changed

8 files changed

+202
-172
lines changed

cmp-android/dependencies/prodReleaseRuntimeClasspath.tree.txt

+2
Original file line numberDiff line numberDiff line change
@@ -1229,6 +1229,7 @@
12291229
| | | | \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0 -> 1.10.1 (*)
12301230
| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1 (*)
12311231
| | | +--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3 (*)
1232+
| | | +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3 (*)
12321233
| | | +--- project :core:model (*)
12331234
| | | \--- project :core:common (*)
12341235
| | +--- project :core:model (*)
@@ -1431,6 +1432,7 @@
14311432
| | +--- org.jetbrains.androidx.navigation:navigation-compose:2.8.0-alpha10 (*)
14321433
| | +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.8 (*)
14331434
| | +--- project :core:common (*)
1435+
| | +--- project :core:datastore (*)
14341436
| | +--- project :feature:home
14351437
| | | +--- io.insert-koin:koin-bom:4.0.1-RC1 (*)
14361438
| | | +--- io.insert-koin:koin-android:4.0.1-RC1 (*)

cmp-navigation/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ kotlin {
2020
// Core Modules
2121
implementation(projects.core.data)
2222
implementation(projects.core.common)
23+
implementation(projects.core.datastore)
2324

2425
implementation(projects.feature.home)
2526
implementation(projects.feature.profile)

core/datastore/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ kotlin {
3232
implementation(libs.multiplatform.settings.coroutines)
3333
implementation(libs.kotlinx.coroutines.core)
3434
implementation(libs.kotlinx.serialization.core)
35+
implementation(libs.kotlinx.serialization.json)
3536
implementation(projects.core.model)
3637
implementation(projects.core.common)
3738
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2025 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
9+
*/
10+
package org.mifos.core.datastore
11+
12+
import com.russhwolf.settings.ExperimentalSettingsApi
13+
import com.russhwolf.settings.ObservableSettings
14+
import com.russhwolf.settings.Settings
15+
import com.russhwolf.settings.coroutines.FlowSettings
16+
import com.russhwolf.settings.coroutines.SuspendSettings
17+
import com.russhwolf.settings.coroutines.toFlowSettings
18+
import com.russhwolf.settings.coroutines.toSuspendSettings
19+
import kotlinx.coroutines.CoroutineDispatcher
20+
21+
@OptIn(ExperimentalSettingsApi::class)
22+
object SettingsFactory {
23+
fun createSuspendSettings(
24+
settings: Settings,
25+
dispatcher: CoroutineDispatcher,
26+
): SuspendSettings {
27+
return settings.toSuspendSettings(dispatcher = dispatcher)
28+
}
29+
30+
fun createFlowSettings(
31+
settings: Settings,
32+
dispatcher: CoroutineDispatcher,
33+
): FlowSettings {
34+
val observableSettings: ObservableSettings = settings as ObservableSettings
35+
return observableSettings.toFlowSettings(dispatcher = dispatcher)
36+
}
37+
}

core/datastore/src/commonMain/kotlin/org/mifos/core/datastore/UserPreferencesDataStore.kt

+141-129
Original file line numberDiff line numberDiff line change
@@ -10,176 +10,188 @@
1010
package org.mifos.core.datastore
1111

1212
import com.russhwolf.settings.ExperimentalSettingsApi
13-
import com.russhwolf.settings.ObservableSettings
14-
import com.russhwolf.settings.Settings
15-
import com.russhwolf.settings.SettingsListener
16-
import com.russhwolf.settings.coroutines.getLongFlow
17-
import com.russhwolf.settings.coroutines.getLongStateFlow
18-
import com.russhwolf.settings.long
19-
import com.russhwolf.settings.serialization.containsValue
20-
import com.russhwolf.settings.serialization.decodeValue
21-
import com.russhwolf.settings.serialization.decodeValueOrNull
22-
import com.russhwolf.settings.serialization.encodeValue
23-
import com.russhwolf.settings.serialization.removeValue
24-
import com.russhwolf.settings.serialization.serializedValue
25-
import kotlinx.coroutines.CoroutineDispatcher
26-
import kotlinx.coroutines.CoroutineScope
13+
import com.russhwolf.settings.coroutines.FlowSettings
14+
import com.russhwolf.settings.coroutines.SuspendSettings
2715
import kotlinx.coroutines.flow.Flow
28-
import kotlinx.coroutines.flow.MutableStateFlow
29-
import kotlinx.coroutines.flow.StateFlow
30-
import kotlinx.coroutines.flow.asStateFlow
31-
import kotlinx.coroutines.withContext
32-
import kotlinx.serialization.ExperimentalSerializationApi
33-
import org.mifos.core.datastore.model.SampleUser
16+
import kotlinx.coroutines.flow.map
17+
import kotlinx.serialization.KSerializer
18+
import kotlinx.serialization.json.Json
3419

35-
private const val USER_KEY = "sample_user"
36-
private const val LAST_LOGIN_KEY = "last_login"
20+
const val USER_KEY = "sample_user"
3721

22+
@Suppress("TooManyFunctions")
23+
@OptIn(ExperimentalSettingsApi::class)
3824
class UserPreferencesDataStore(
39-
private val settings: Settings,
40-
private val dispatcher: CoroutineDispatcher,
41-
scope: CoroutineScope,
25+
private val suspendSettings: SuspendSettings,
26+
val flowSettings: FlowSettings,
4227
) {
4328

4429
// --- Basic Operations ---
4530

4631
// Store primitive value
47-
fun putLastLogin(timeStamp: Long) {
48-
settings.putLong(LAST_LOGIN_KEY, timeStamp)
32+
33+
suspend fun putInt(key: String, value: Int) {
34+
suspendSettings.putInt(key, value)
35+
}
36+
37+
suspend fun getInt(key: String, default: Int): Int {
38+
return suspendSettings.getInt(key, default)
4939
}
5040

51-
// Retrieve primitive value with default
52-
fun getLastLogin(default: Long = 0): Long {
53-
return settings.getLong(LAST_LOGIN_KEY, default)
41+
suspend fun getNullableInt(key: String): Int? {
42+
return suspendSettings.getIntOrNull(key)
43+
}
44+
45+
suspend fun putLong(key: String, value: Long) {
46+
suspendSettings.putLong(key, value)
47+
}
48+
49+
suspend fun getLong(key: String, default: Long): Long {
50+
return suspendSettings.getLong(key, default)
5451
}
5552

5653
// Retrieve nullable primitive
57-
fun getLastLoginOrNull(): Long? {
58-
return settings.getLongOrNull(LAST_LOGIN_KEY)
54+
suspend fun getNullableLong(key: String): Long? {
55+
return suspendSettings.getLongOrNull(key)
56+
}
57+
58+
suspend fun putFloat(key: String, value: Float) {
59+
suspendSettings.putFloat(key, value)
60+
}
61+
62+
suspend fun getFloat(key: String, default: Float): Float {
63+
return suspendSettings.getFloat(key, default)
64+
}
65+
66+
suspend fun getNullableFloat(key: String): Float? {
67+
return suspendSettings.getFloatOrNull(key)
68+
}
69+
70+
suspend fun putDouble(key: String, value: Double) {
71+
suspendSettings.putDouble(key, value)
72+
}
73+
74+
suspend fun getDouble(key: String, default: Double): Double {
75+
return suspendSettings.getDouble(key, default)
76+
}
77+
78+
suspend fun getNullableDouble(key: String): Double? {
79+
return suspendSettings.getDoubleOrNull(key)
80+
}
81+
82+
suspend fun putString(key: String, value: String) {
83+
suspendSettings.putString(key, value)
84+
}
85+
86+
suspend fun getString(key: String, default: String): String {
87+
return suspendSettings.getString(key, default)
88+
}
89+
90+
suspend fun getNullableString(key: String): String? {
91+
return suspendSettings.getStringOrNull(key)
5992
}
6093

61-
// Property delegate for primitive
62-
val lastLogin: Long by settings.long(LAST_LOGIN_KEY, defaultValue = 0)
94+
suspend fun putBoolean(key: String, value: Boolean) {
95+
suspendSettings.putBoolean(key, value)
96+
}
97+
98+
suspend fun getBoolean(key: String, default: Boolean): Boolean {
99+
return suspendSettings.getBoolean(key, default)
100+
}
101+
102+
suspend fun getNullableBoolean(key: String): Boolean? {
103+
return suspendSettings.getBooleanOrNull(key)
104+
}
63105

64106
// Check key existence
65-
fun hasLastLogin(): Boolean {
66-
return settings.hasKey(LAST_LOGIN_KEY)
107+
suspend fun hasKey(key: String): Boolean {
108+
return suspendSettings.hasKey(key)
67109
}
68110

69-
// Remove key
70-
fun removeLastLogin() {
71-
settings.remove(LAST_LOGIN_KEY)
111+
suspend fun removeValue(key: String) {
112+
suspendSettings.remove(key)
72113
}
73114

74115
// Clear all
75116
suspend fun clearAll() {
76-
withContext(dispatcher) {
77-
settings.clear()
78-
}
117+
suspendSettings.clear()
79118
}
80119

81120
// Get all keys and size
82-
val allKeys: Set<String> get() = settings.keys
83-
val size: Int get() = settings.size
84-
85-
// --- Serialization Operations ---
86-
87-
// Store serialized object
88-
@OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)
89-
suspend fun saveUser(user: SampleUser) {
90-
withContext(dispatcher) {
91-
settings.encodeValue(
92-
serializer = SampleUser.serializer(),
93-
key = USER_KEY,
94-
value = user,
95-
)
96-
_userFlow.value = user
97-
}
121+
suspend fun getAllKeys(): Set<String> {
122+
return suspendSettings.keys()
98123
}
99124

100-
// Retrieve serialized object with default
101-
@OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)
102-
fun getUser(): SampleUser {
103-
return settings.decodeValue(
104-
serializer = SampleUser.serializer(),
105-
key = USER_KEY,
106-
defaultValue = SampleUser.DEFAULT,
107-
)
125+
suspend fun getSize(): Int {
126+
return suspendSettings.size()
108127
}
109128

110-
// Retrieve nullable serialized object
111-
@OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)
112-
fun getUserOrNull(): SampleUser? {
113-
return settings.decodeValueOrNull(
114-
serializer = SampleUser.serializer(),
115-
key = USER_KEY,
116-
)
117-
}
129+
// --- Serialization Operations ---
118130

119-
// Property delegate for serialized object
120-
@OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)
121-
val userProperty: SampleUser by settings.serializedValue(
122-
serializer = SampleUser.serializer(),
123-
key = USER_KEY,
124-
defaultValue = SampleUser.DEFAULT,
125-
)
126-
127-
// Check serialized object existence
128-
@OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)
129-
fun hasUser(): Boolean {
130-
return settings.containsValue(
131-
serializer = SampleUser.serializer(),
132-
key = USER_KEY,
131+
// Store serialized object
132+
suspend fun <T> putSerializableData(key: String, value: T, serializer: KSerializer<T>) {
133+
val json = Json.encodeToString(
134+
serializer = serializer,
135+
value = value,
133136
)
137+
suspendSettings.putString(key = key, value = json)
134138
}
135139

136-
// Remove serialized object
137-
@OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)
138-
suspend fun removeUser() {
139-
withContext(dispatcher) {
140-
settings.removeValue(
141-
serializer = SampleUser.serializer(),
142-
key = USER_KEY,
143-
)
144-
_userFlow.value = SampleUser.DEFAULT
145-
}
140+
// Get serialized object with default value
141+
suspend fun <T> getSerializedData(
142+
key: String,
143+
defaultValue: T,
144+
serializer: KSerializer<T>,
145+
): T {
146+
val json = suspendSettings.getStringOrNull(key = key) ?: return defaultValue
147+
return Json.decodeFromString(
148+
deserializer = serializer,
149+
string = json,
150+
)
146151
}
147152

148153
// --- Listener Operations ---
149-
private val observableSettings: ObservableSettings = settings as ObservableSettings
150-
fun observeAgeChange(onChange: (Int) -> Int): SettingsListener {
151-
return observableSettings.addIntListener(key = "user_age", defaultValue = 0) { value ->
152-
onChange(value)
154+
155+
inline fun <reified T> observeKeyFlow(
156+
key: String,
157+
defaultValue: T,
158+
serializer: KSerializer<T>?,
159+
): Flow<T> {
160+
return when (T::class) {
161+
Int::class -> flowSettings.getIntFlow(key, defaultValue as Int) as Flow<T>
162+
Long::class -> flowSettings.getLongFlow(key, defaultValue as Long) as Flow<T>
163+
Float::class -> flowSettings.getFloatFlow(key, defaultValue as Float) as Flow<T>
164+
Double::class -> flowSettings.getDoubleFlow(key, defaultValue as Double) as Flow<T>
165+
String::class -> flowSettings.getStringFlow(key, defaultValue as String) as Flow<T>
166+
Boolean::class -> flowSettings.getBooleanFlow(key, defaultValue as Boolean) as Flow<T>
167+
else -> {
168+
require(serializer != null) { "Unsupported type or no serializer provided for ${T::class}" }
169+
flowSettings.getStringFlow(key, Json.encodeToString(serializer, defaultValue))
170+
.map { jsonString ->
171+
Json.decodeFromString(serializer, jsonString)
172+
}
173+
}
153174
}
154175
}
155176

156-
// --- Coroutine/Flow Operations ---
157-
private val _userFlow = MutableStateFlow(getUser())
158-
159-
// Flow for serialized user
160-
val userFlow: StateFlow<SampleUser> get() = _userFlow.asStateFlow()
161-
162-
// Flow for primitive (requires ObservableSettings)
163-
@OptIn(ExperimentalSettingsApi::class)
164-
val lastLoginFlow: Flow<Long> = observableSettings.getLongFlow(
165-
key = LAST_LOGIN_KEY,
166-
defaultValue = 0L,
167-
)
168-
169-
// StateFlow for primitive
170-
@OptIn(ExperimentalSettingsApi::class)
171-
val lastLoginStateFlow: StateFlow<Long> = observableSettings.getLongStateFlow(
172-
key = LAST_LOGIN_KEY,
173-
coroutineScope = scope,
174-
defaultValue = 0L,
175-
)
176-
177-
// Update specific field (example with serialization)
178-
suspend fun updateUserName(newName: String) {
179-
withContext(dispatcher) {
180-
val currentUser = getUser()
181-
val updatedUser = currentUser.copy(name = newName)
182-
saveUser(updatedUser)
177+
inline fun <reified T> observeNullableKeyFlow(
178+
key: String,
179+
serializer: KSerializer<T>?,
180+
): Flow<T?> {
181+
return when (T::class) {
182+
Int::class -> flowSettings.getIntOrNullFlow(key) as Flow<T?>
183+
Long::class -> flowSettings.getLongOrNullFlow(key) as Flow<T?>
184+
Float::class -> flowSettings.getFloatOrNullFlow(key) as Flow<T?>
185+
Double::class -> flowSettings.getDoubleOrNullFlow(key) as Flow<T?>
186+
String::class -> flowSettings.getStringOrNullFlow(key) as Flow<T?>
187+
Boolean::class -> flowSettings.getBooleanOrNullFlow(key) as Flow<T?>
188+
else -> {
189+
require(serializer != null) { "Unsupported type or no serializer provided for ${T::class}" }
190+
flowSettings.getStringOrNullFlow(key)
191+
.map { jsonString ->
192+
jsonString?.let { Json.decodeFromString(serializer, jsonString) }
193+
}
194+
}
183195
}
184196
}
185197
}

0 commit comments

Comments
 (0)