Skip to content

Commit 494a291

Browse files
Merge pull request #45 from HekmatullahAmin/KMPPT-15
feat(core): Add core/datastore module
2 parents f812dac + 6040ec7 commit 494a291

File tree

9 files changed

+364
-0
lines changed

9 files changed

+364
-0
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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.coroutines.FlowSettings
14+
import com.russhwolf.settings.coroutines.SuspendSettings
15+
import kotlinx.coroutines.flow.Flow
16+
import kotlinx.coroutines.flow.map
17+
import kotlinx.serialization.KSerializer
18+
import kotlinx.serialization.json.Json
19+
20+
const val USER_KEY = "sample_user"
21+
22+
@Suppress("TooManyFunctions")
23+
@OptIn(ExperimentalSettingsApi::class)
24+
class UserPreferencesDataStore(
25+
private val suspendSettings: SuspendSettings,
26+
val flowSettings: FlowSettings,
27+
) {
28+
29+
// --- Basic Operations ---
30+
31+
// Store primitive value
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)
39+
}
40+
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)
51+
}
52+
53+
// Retrieve nullable primitive
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)
92+
}
93+
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+
}
105+
106+
// Check key existence
107+
suspend fun hasKey(key: String): Boolean {
108+
return suspendSettings.hasKey(key)
109+
}
110+
111+
suspend fun removeValue(key: String) {
112+
suspendSettings.remove(key)
113+
}
114+
115+
// Clear all
116+
suspend fun clearAll() {
117+
suspendSettings.clear()
118+
}
119+
120+
// Get all keys and size
121+
suspend fun getAllKeys(): Set<String> {
122+
return suspendSettings.keys()
123+
}
124+
125+
suspend fun getSize(): Int {
126+
return suspendSettings.size()
127+
}
128+
129+
// --- Serialization Operations ---
130+
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,
136+
)
137+
suspendSettings.putString(key = key, value = json)
138+
}
139+
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+
)
151+
}
152+
153+
// --- Listener Operations ---
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+
}
174+
}
175+
}
176+
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+
}
195+
}
196+
}
197+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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 kotlinx.coroutines.flow.Flow
13+
import org.mifos.core.datastore.model.SampleUser
14+
15+
interface UserPreferencesRepository {
16+
val currentUser: Flow<SampleUser>
17+
18+
suspend fun saveUser(user: SampleUser)
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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 kotlinx.coroutines.flow.Flow
13+
import org.mifos.core.datastore.model.SampleUser
14+
15+
class UserPreferencesRepositoryImpl(
16+
private val dataStore: UserPreferencesDataStore,
17+
) : UserPreferencesRepository {
18+
override val currentUser: Flow<SampleUser> =
19+
dataStore.observeKeyFlow<SampleUser>(USER_KEY, SampleUser.DEFAULT, SampleUser.serializer())
20+
21+
override suspend fun saveUser(user: SampleUser) {
22+
dataStore.putSerializableData(USER_KEY, user, SampleUser.serializer())
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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.di
11+
12+
import com.russhwolf.settings.ExperimentalSettingsApi
13+
import com.russhwolf.settings.Settings
14+
import kotlinx.coroutines.CoroutineScope
15+
import kotlinx.coroutines.Dispatchers
16+
import org.koin.core.qualifier.named
17+
import org.koin.dsl.module
18+
import org.mifos.core.common.di.AppDispatchers
19+
import org.mifos.core.datastore.SettingsFactory
20+
import org.mifos.core.datastore.UserPreferencesDataStore
21+
import org.mifos.core.datastore.UserPreferencesRepository
22+
import org.mifos.core.datastore.UserPreferencesRepositoryImpl
23+
24+
@OptIn(ExperimentalSettingsApi::class)
25+
val DatastoreModule = module {
26+
single { Settings() }
27+
single {
28+
SettingsFactory.createSuspendSettings(
29+
settings = get(),
30+
dispatcher = get(named(AppDispatchers.IO.name)),
31+
)
32+
}
33+
34+
single {
35+
SettingsFactory.createFlowSettings(
36+
settings = get(),
37+
dispatcher = get(named(AppDispatchers.IO.name)),
38+
)
39+
}
40+
single {
41+
UserPreferencesDataStore(
42+
get(),
43+
get(),
44+
)
45+
}
46+
single<UserPreferencesRepository> {
47+
UserPreferencesRepositoryImpl(
48+
dataStore = get(),
49+
)
50+
}
51+
single<CoroutineScope> { CoroutineScope(Dispatchers.Default) }
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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.model
11+
12+
import kotlinx.serialization.Serializable
13+
14+
@Serializable
15+
data class SampleUser(
16+
val id: Long,
17+
val name: String,
18+
val email: String,
19+
val isActive: Boolean,
20+
val age: Int,
21+
) {
22+
companion object {
23+
val DEFAULT = SampleUser(
24+
id = 0L,
25+
name = "Guest",
26+
email = "[email protected]",
27+
isActive = false,
28+
age = 0,
29+
)
30+
}
31+
}

0 commit comments

Comments
 (0)