Skip to content

Commit 8fc346a

Browse files
committed
Add documentation for data store
1 parent dd998c1 commit 8fc346a

File tree

13 files changed

+169
-28
lines changed

13 files changed

+169
-28
lines changed

android/engine/build.gradle.kts

+3-2
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,6 @@ dependencies {
140140
implementation(libs.dagger.hilt.android)
141141
implementation(libs.hilt.work)
142142
implementation(libs.slf4j.nop)
143-
implementation(libs.datastore)
144-
implementation(libs.datastore.preferences)
145143
implementation(libs.cqf.cql.evaluator) {
146144
exclude(group = "com.github.ben-manes.caffeine")
147145
exclude(group = "ca.uhn.hapi.fhir")
@@ -200,6 +198,9 @@ dependencies {
200198
api(libs.retrofit2.kotlinx.serialization.converter)
201199
api(libs.okhttp)
202200
api(libs.okhttp.logging.interceptor)
201+
api(libs.datastore)
202+
api(libs.datastore.preferences)
203+
203204
api(libs.commons.jexl3) { exclude(group = "commons-logging", module = "commons-logging") }
204205
api(libs.easy.rules.jexl) {
205206
exclude(group = "commons-logging", module = "commons-logging")

android/engine/src/main/java/org/smartregister/fhircore/engine/datastore/PreferenceDataStore.kt

+10-7
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
package org.smartregister.fhircore.engine.datastore
22

33
import android.content.Context
4+
import androidx.datastore.core.DataStore
45
import androidx.datastore.preferences.core.Preferences
56
import androidx.datastore.preferences.core.edit
67
import androidx.datastore.preferences.core.emptyPreferences
78
import androidx.datastore.preferences.core.stringPreferencesKey
9+
import androidx.datastore.preferences.preferencesDataStore
810
import dagger.hilt.android.qualifiers.ApplicationContext
911
import kotlinx.coroutines.flow.catch
1012
import kotlinx.coroutines.flow.map
11-
import org.smartregister.fhircore.engine.datastore.mockdata.SerializablePractitionerDetails
12-
import org.smartregister.fhircore.engine.datastore.mockdata.SerializableUserInfo
13-
import org.smartregister.fhircore.engine.ui.bottomsheet.RegisterBottomSheetFragment.Companion.TAG
14-
import timber.log.Timber
1513
import java.io.IOException
1614
import javax.inject.Inject
1715
import javax.inject.Singleton
1816

17+
const val DATASTORE_NAME = "preferences_datastore"
18+
val Context.dataStore : DataStore<Preferences> by preferencesDataStore(name = DATASTORE_NAME)
19+
1920
@Singleton
20-
class DataStoresPreference @Inject constructor(@ApplicationContext val context: Context ) {
21+
class PreferenceDataStore @Inject constructor(@ApplicationContext val context: Context ) {
2122
fun <T> read(key: Preferences.Key<T>) = context.dataStore.data
2223
.catch { exception ->
2324
if (exception is IOException) {
@@ -36,7 +37,9 @@ class DataStoresPreference @Inject constructor(@ApplicationContext val context:
3637
}
3738

3839
companion object Keys {
39-
val APP_ID = stringPreferencesKey("appId")
40-
val LANG = stringPreferencesKey("lang")
40+
val appIdKeyName = "appId"
41+
val langKeyName = "lang"
42+
val APP_ID by lazy { stringPreferencesKey(appIdKeyName) }
43+
val LANG by lazy { stringPreferencesKey(langKeyName) }
4144
}
4245
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package org.smartregister.fhircore.engine.datastore
2+
3+
import android.content.Context
4+
import androidx.datastore.core.DataStore
5+
import androidx.datastore.dataStore
6+
import dagger.hilt.android.qualifiers.ApplicationContext
7+
import kotlinx.coroutines.flow.catch
8+
import javax.inject.Inject
9+
import org.smartregister.fhircore.engine.datastore.mockdata.SerializablePractitionerDetails
10+
import org.smartregister.fhircore.engine.datastore.mockdata.SerializableUserInfo
11+
import org.smartregister.fhircore.engine.datastore.serializers.PractitionerDetailsDataStoreSerializer
12+
import org.smartregister.fhircore.engine.datastore.serializers.UserInfoDataStoreSerializer
13+
import timber.log.Timber
14+
import java.io.IOException
15+
import javax.inject.Singleton
16+
17+
private const val PRACTITIONER_DETAILS_DATASTORE_JSON = "practitioner_details.json"
18+
private const val USER_INFO_DATASTORE_JSON = "user_info.json"
19+
private const val TAG = "Proto DataStore"
20+
21+
val Context.practitionerProtoStore: DataStore<SerializablePractitionerDetails> by dataStore(
22+
fileName = PRACTITIONER_DETAILS_DATASTORE_JSON,
23+
serializer = PractitionerDetailsDataStoreSerializer
24+
)
25+
26+
val Context.userInfoProtoStore: DataStore<SerializableUserInfo> by dataStore(
27+
fileName = USER_INFO_DATASTORE_JSON,
28+
serializer = UserInfoDataStoreSerializer
29+
)
30+
@Singleton
31+
class ProtoDataStore @Inject constructor(@ApplicationContext val context: Context) {
32+
33+
val practitioner = context.practitionerProtoStore.data
34+
.catch { exception ->
35+
if (exception is IOException) {
36+
Timber.tag(TAG).e(exception, "Error reading practitioner details preferences.")
37+
emit(SerializablePractitionerDetails())
38+
} else {
39+
throw exception
40+
}
41+
}
42+
43+
suspend fun writePractitioner(serializablePractitionerDetails: SerializablePractitionerDetails) {
44+
context.practitionerProtoStore.updateData { practitionerData ->
45+
practitionerData.copy(
46+
name = serializablePractitionerDetails.name,
47+
id = serializablePractitionerDetails.id
48+
)
49+
}
50+
}
51+
52+
val userInfo = context.userInfoProtoStore.data
53+
.catch { exception ->
54+
if (exception is IOException) {
55+
Timber.tag(TAG).e(exception, "Error reading practitioner details preferences.")
56+
emit(SerializableUserInfo())
57+
} else {
58+
throw exception
59+
}
60+
}
61+
62+
suspend fun writeUserInfo(serializableUserInfo: SerializableUserInfo) {
63+
context.userInfoProtoStore.updateData { userInfo ->
64+
userInfo.copy(
65+
name = serializableUserInfo.name
66+
)
67+
}
68+
}
69+
70+
}

android/engine/src/main/java/org/smartregister/fhircore/engine/datastore/serializers/PractitionerDetailsDataStoreSerializer.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import androidx.datastore.core.Serializer
44
import kotlinx.serialization.json.Json
55
import org.apache.commons.lang3.SerializationException
66
import org.smartregister.fhircore.engine.datastore.mockdata.SerializablePractitionerDetails
7+
import timber.log.Timber
78
import java.io.InputStream
89
import java.io.OutputStream
910

@@ -18,7 +19,7 @@ object PractitionerDetailsDataStoreSerializer: Serializer<SerializablePractition
1819
string = input.readBytes().decodeToString()
1920
)
2021
} catch (e: SerializationException) {
21-
e.printStackTrace()
22+
Timber.tag(SerializerConstants.PROTOSTORE_SERIALIZER_TAG).d(e)
2223
defaultValue
2324
}
2425
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
package org.smartregister.fhircore.engine.datastore.serializers
22

3-
class SerializerConstants {
3+
object SerializerConstants {
4+
const val PROTOSTORE_SERIALIZER_TAG = "Proto DataStore"
45
}

android/engine/src/main/java/org/smartregister/fhircore/engine/datastore/serializers/UserInfoDataStoreSerializer.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import androidx.datastore.core.Serializer
44
import kotlinx.serialization.json.Json
55
import org.apache.commons.lang3.SerializationException
66
import org.smartregister.fhircore.engine.datastore.mockdata.SerializableUserInfo
7+
import org.smartregister.fhircore.engine.ui.bottomsheet.RegisterBottomSheetFragment
8+
import timber.log.Timber
79
import java.io.InputStream
810
import java.io.OutputStream
911

@@ -18,7 +20,7 @@ object UserInfoDataStoreSerializer: Serializer<SerializableUserInfo> {
1820
string = input.readBytes().decodeToString()
1921
)
2022
} catch (e: SerializationException) {
21-
e.printStackTrace()
23+
Timber.tag(SerializerConstants.PROTOSTORE_SERIALIZER_TAG).d(e)
2224
defaultValue
2325
}
2426
}

android/engine/src/main/java/org/smartregister/fhircore/engine/di/CoreModule.kt

-12
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ import javax.inject.Singleton
3232
import org.hl7.fhir.r4.context.SimpleWorkerContext
3333
import org.hl7.fhir.r4.model.Parameters
3434
import org.hl7.fhir.r4.utils.FHIRPathEngine
35-
import org.smartregister.fhircore.engine.datastore.DataStoreHelper
36-
import org.smartregister.fhircore.engine.datastore.DataStoresRepository
3735
import org.smartregister.fhircore.engine.util.helper.TransformSupportServices
3836

3937
@InstallIn(SingletonComponent::class)
@@ -72,14 +70,4 @@ class CoreModule {
7270
fun provideFhirOperator(fhirEngine: FhirEngine): FhirOperator =
7371
FhirOperator(fhirContext = FhirContext.forCached(FhirVersionEnum.R4), fhirEngine = fhirEngine)
7472

75-
@Singleton
76-
@Provides
77-
fun provideDataStoresRepository(@ApplicationContext context: Context): DataStoresRepository =
78-
DataStoresRepository(context)
79-
80-
@Singleton
81-
@Provides
82-
fun provideDataStoreHelper(repository: DataStoresRepository) =
83-
DataStoreHelper(repository)
84-
8573
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,54 @@
11
package org.smartregister.fhircore.engine.datastore
22

3-
class DataStoreHelperTest {
3+
import android.content.Context
4+
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
5+
import androidx.test.core.app.ApplicationProvider
6+
import dagger.hilt.android.testing.HiltAndroidRule
7+
import dagger.hilt.android.testing.HiltAndroidTest
8+
import kotlinx.coroutines.flow.first
9+
import kotlinx.coroutines.flow.map
10+
import kotlinx.coroutines.test.runTest
11+
import org.junit.Before
12+
import org.junit.Rule
13+
import org.junit.Test
14+
import org.smartregister.fhircore.engine.robolectric.RobolectricTest
15+
16+
@HiltAndroidTest
17+
internal class PreferenceDataStoreTest : RobolectricTest() {
18+
private val testContext: Context = ApplicationProvider.getApplicationContext()
19+
20+
@get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
21+
@get:Rule(order = 1) val instantTaskExecutorRule = InstantTaskExecutorRule()
22+
23+
private lateinit var preferenceDataStore: PreferenceDataStore
24+
25+
private val keys = PreferenceDataStore.Keys
26+
27+
@Before
28+
fun setUp() {
29+
hiltRule.inject()
30+
preferenceDataStore = PreferenceDataStore(testContext)
31+
}
32+
33+
@Test
34+
fun testReadAppId() {
35+
val expectedValue = ""
36+
runTest {
37+
val valueFlow = preferenceDataStore.read(keys.APP_ID)
38+
valueFlow.map { value ->
39+
assert(value == expectedValue)
40+
}
41+
}
42+
}
43+
44+
@Test
45+
fun testWriteAppId() {
46+
val newAppId = "new_app_id"
47+
val key = keys.APP_ID
48+
49+
runTest {
50+
preferenceDataStore.write(key, newAppId)
51+
assert(preferenceDataStore.read(keys.APP_ID).first() == newAppId)
52+
}
53+
}
454
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/home/kelvin/fhircore/android/hisp-wdf-fhir-resources/app

android/gradle/libs.versions.toml

+4-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ cql-to-elm = "2.4.0"
2222
dagger-hilt-compiler = "2.45"
2323
data-capture = "1.0.0-preview19-SNAPSHOT"
2424
datastore = "1.0.0"
25-
datastore-preferences = "1.0.0"
2625
desugar-jdk-libs = "1.1.5"
2726
easy-rules-jexl = "4.1.0"
2827
espresso-core = "3.5.1"
@@ -39,6 +38,7 @@ hilt-android-testing = "2.45"
3938
hilt-compiler = "1.0.0"
4039
hilt-navigation-compose = "1.0.0"
4140
hilt-work = "1.0.0"
41+
hiltAndroid = "2.44"
4242
jjwt = "0.9.1"
4343
joda-time = "2.10.14"
4444
json = "20230618"
@@ -132,8 +132,8 @@ cql-to-elm = { group = "info.cqframework", name = "cql-to-elm", version.ref = "c
132132
dagger-hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt-android" }
133133
dagger-hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "dagger-hilt-compiler" }
134134
data-capture = { group = "org.smartregister", name = "data-capture", version.ref = "data-capture" }
135-
datastore = { group = "androidx.datastore", name = "datastore", version.ref = "datastore" }
136-
datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore-preferences"}
135+
datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
136+
datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
137137
easy-rules-jexl = { group = "org.jeasy", name = "easy-rules-jexl", version.ref = "easy-rules-jexl" }
138138
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
139139
fhir-common-utils = { group = "org.smartregister", name = "fhir-common-utils", version.ref = "fhir-common-utils" }
@@ -145,6 +145,7 @@ glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "gl
145145
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
146146
hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt-android-compiler" }
147147
hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt-android-testing" }
148+
hilt-android-v244 = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
148149
hilt-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hilt-compiler" }
149150
hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hilt-navigation-compose" }
150151
hilt-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "hilt-work" }

android/quest/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ dependencies {
369369
implementation(libs.dagger.hilt.android)
370370
implementation(libs.hilt.work)
371371
implementation(libs.cql.measure.evaluator)
372+
implementation(libs.hilt.android.v244)
372373

373374
// Annotation processors
374375
kapt(libs.hilt.compiler)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
label: DataStore
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
**STORING PREFERENCES DATA**
2+
3+
Fhircore uses Preferences [DataStore](https://developer.android.com/topic/libraries/architecture/datastore) and Proto DataStore to store preferences data. Primitive types are stored in the Preferences DataStore and structured types are stored in the Proto DataStore.
4+
5+
The interfaces exposing access to these two storage options are located in ```engine/datastore/PreferenceDataStore``` and ```engine/datastore/ProtoDataStore```.
6+
7+
***PreferenceDataStore.kt***<br/>
8+
To register a key-value pair that you intend to store in the preferences datastore, add a key that enforces the type of data being stored and the name of the key in the Keys companion object.
9+
10+
The read() method returns a flow. To observe the flow, call the function in the view model and store the resulting flow. You may then convert it to a StateFlow and expose it to the UI.
11+
12+
The write() method can be used in the UI directly but it is preferable to expose it through a view model to follow the MVVM pattern.
13+
14+
Both read() and write() are Generic methods.
15+
16+
***ProtoDataStore.kt***<br/>
17+
Proto DataStore allows us to store objects in a type-safe way. For ease of adaptation, we use [Kotlinx serialization](https://kotlinlang.org/docs/serialization.html#serialize-and-deserialize-json) to allow us to use Kotlin data classes instead of Protobuf files to define the schema of the data being stored.
18+
19+
Preferably, any objects made within Fhircore with the intent of being persisted in the Proto DataStore should be annotated with ```@Serializable``` (kotlinx.serialization.Serializable).
20+
21+
You also need to register a serializer that will serialize and deserialize the object stored within a JSON file on the device. The serializers are found within ```engine/datastore/serializers```

0 commit comments

Comments
 (0)