Skip to content

Commit cb8548e

Browse files
Setup datastore interface (#2917)
* Add datastore and preferences datastore dependencies * Create datastores provider - Create delegate for providing practitionerPreferences to demonstrate proto datastore - Create primitives Preference Store to store all primitives on the same datastore and also demonstrate preferences datastore Create a serializer and wrapper data class for the Practitioner details to enable them to be stored to disk * Redesign StoresProvider and add Repository - Combined different object into one to prevent repitition of functions and allow readability. Concerns on write performance to be discussed - Added DataStoresRespository to provide an interface for the DataStore Helper - Added extra primitive data and extra structured data to experiment with multiple objects being added to the data stores * Redesign architecture - Put all datastore delegates in one file. They will be provided to the repository through Hilt - Remove data store initialization Singletons - Move Data store keys to repository - Begin writing explicit write functions for the dataStore to prevent exposing keys and write errors. Needs discussion * Proto DataStore - Add write functions and read flows to datastore repository - Add DataStoreHelper class as the interface for all data store functions and flows * Add providers for the datastore repository and datastore helper into the core module * Add sample tests for the dataStore repository * Add tests for protoStore read and write * Add initial refactors - Separate Proto and Preferences data stores - Move Context datastore extensions from singular file to top of respective classes - Remove @provides in Core module. @singleton is sufficient - Fix dependency insertion in gradle - Remove data class for storing all primitives. All primitives now inserted individually - Make preferences read function to accept delegate to prevent writing a different read for each preference stored * Add tests for Proto DataStore and Preference DataStore * Revert lost changes * Revert lost code * Re-add lost documentation * Code cleanup * Code cleanup * Update docs/engineering/android-app/datastore/datastore.mdx Co-authored-by: Elly Kitoto <[email protected]> * Update docs/engineering/android-app/datastore/datastore.mdx Co-authored-by: Elly Kitoto <[email protected]> * Update docs/engineering/android-app/datastore/datastore.mdx Co-authored-by: Elly Kitoto <[email protected]> * Rename data classes --------- Co-authored-by: Elly Kitoto <[email protected]>
1 parent c54c75a commit cb8548e

File tree

13 files changed

+504
-0
lines changed

13 files changed

+504
-0
lines changed

android/engine/build.gradle.kts

+2
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ 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)
143145
implementation(libs.cqf.cql.evaluator) {
144146
exclude(group = "com.github.ben-manes.caffeine")
145147
exclude(group = "ca.uhn.hapi.fhir")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2021-2023 Ona Systems, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.smartregister.fhircore.engine.datastore
18+
19+
import android.content.Context
20+
import androidx.datastore.core.DataStore
21+
import androidx.datastore.preferences.core.Preferences
22+
import androidx.datastore.preferences.core.edit
23+
import androidx.datastore.preferences.core.emptyPreferences
24+
import androidx.datastore.preferences.core.stringPreferencesKey
25+
import androidx.datastore.preferences.preferencesDataStore
26+
import dagger.hilt.android.qualifiers.ApplicationContext
27+
import java.io.IOException
28+
import javax.inject.Inject
29+
import javax.inject.Singleton
30+
import kotlinx.coroutines.flow.catch
31+
import kotlinx.coroutines.flow.map
32+
33+
const val DATASTORE_NAME = "preferences_datastore"
34+
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = DATASTORE_NAME)
35+
36+
@Singleton
37+
class PreferenceDataStore @Inject constructor(@ApplicationContext val context: Context) {
38+
fun <T> read(key: Preferences.Key<T>) =
39+
context.dataStore.data
40+
.catch { exception ->
41+
if (exception is IOException) {
42+
emit(emptyPreferences())
43+
} else {
44+
throw exception
45+
}
46+
}
47+
.map { preferences -> preferences[key] as T }
48+
49+
suspend fun <T> write(key: Preferences.Key<T>, data: T) {
50+
context.dataStore.edit { preferences -> preferences[key] = data }
51+
}
52+
53+
companion object Keys {
54+
val appIdKeyName = "appId"
55+
val langKeyName = "lang"
56+
val APP_ID by lazy { stringPreferencesKey(appIdKeyName) }
57+
val LANG by lazy { stringPreferencesKey(langKeyName) }
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2021-2023 Ona Systems, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.smartregister.fhircore.engine.datastore
18+
19+
import android.content.Context
20+
import androidx.datastore.core.DataStore
21+
import androidx.datastore.dataStore
22+
import dagger.hilt.android.qualifiers.ApplicationContext
23+
import java.io.IOException
24+
import javax.inject.Inject
25+
import javax.inject.Singleton
26+
import kotlinx.coroutines.flow.catch
27+
import org.smartregister.fhircore.engine.datastore.mockdata.PractitionerDetails
28+
import org.smartregister.fhircore.engine.datastore.mockdata.UserInfo
29+
import org.smartregister.fhircore.engine.datastore.serializers.PractitionerDetailsDataStoreSerializer
30+
import org.smartregister.fhircore.engine.datastore.serializers.UserInfoDataStoreSerializer
31+
import timber.log.Timber
32+
33+
private const val PRACTITIONER_DETAILS_DATASTORE_JSON = "practitioner_details.json"
34+
private const val USER_INFO_DATASTORE_JSON = "user_info.json"
35+
private const val TAG = "Proto DataStore"
36+
37+
val Context.practitionerProtoStore: DataStore<PractitionerDetails> by
38+
dataStore(
39+
fileName = PRACTITIONER_DETAILS_DATASTORE_JSON,
40+
serializer = PractitionerDetailsDataStoreSerializer,
41+
)
42+
43+
val Context.userInfoProtoStore: DataStore<UserInfo> by
44+
dataStore(
45+
fileName = USER_INFO_DATASTORE_JSON,
46+
serializer = UserInfoDataStoreSerializer,
47+
)
48+
49+
@Singleton
50+
class ProtoDataStore @Inject constructor(@ApplicationContext val context: Context) {
51+
52+
val practitioner =
53+
context.practitionerProtoStore.data.catch { exception ->
54+
if (exception is IOException) {
55+
Timber.tag(TAG).e(exception, "Error reading practitioner details preferences.")
56+
emit(PractitionerDetails())
57+
} else {
58+
throw exception
59+
}
60+
}
61+
62+
suspend fun writePractitioner(practitionerDetails: PractitionerDetails) {
63+
context.practitionerProtoStore.updateData { practitionerData ->
64+
practitionerData.copy(
65+
name = practitionerDetails.name,
66+
id = practitionerDetails.id,
67+
)
68+
}
69+
}
70+
71+
val userInfo =
72+
context.userInfoProtoStore.data.catch { exception ->
73+
if (exception is IOException) {
74+
Timber.tag(TAG).e(exception, "Error reading practitioner details preferences.")
75+
emit(UserInfo())
76+
} else {
77+
throw exception
78+
}
79+
}
80+
81+
suspend fun writeUserInfo(userInfo: UserInfo) {
82+
context.userInfoProtoStore.updateData { userInfo ->
83+
userInfo.copy(
84+
name = userInfo.name,
85+
)
86+
}
87+
}
88+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2021-2023 Ona Systems, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.smartregister.fhircore.engine.datastore.mockdata
18+
19+
import kotlinx.serialization.Serializable
20+
21+
@Serializable
22+
data class PractitionerDetails(
23+
val name: String = "sample_name",
24+
val id: Int = 1,
25+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2021-2023 Ona Systems, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.smartregister.fhircore.engine.datastore.mockdata
18+
19+
import kotlinx.serialization.Serializable
20+
21+
@Serializable
22+
data class UserInfo(
23+
val name: String = "sample name",
24+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2021-2023 Ona Systems, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.smartregister.fhircore.engine.datastore.serializers
18+
19+
import androidx.datastore.core.Serializer
20+
import java.io.InputStream
21+
import java.io.OutputStream
22+
import kotlinx.serialization.json.Json
23+
import org.apache.commons.lang3.SerializationException
24+
import org.smartregister.fhircore.engine.datastore.mockdata.PractitionerDetails
25+
import timber.log.Timber
26+
27+
object PractitionerDetailsDataStoreSerializer : Serializer<PractitionerDetails> {
28+
override val defaultValue: PractitionerDetails
29+
get() = PractitionerDetails()
30+
31+
override suspend fun readFrom(input: InputStream): PractitionerDetails {
32+
return try {
33+
Json.decodeFromString(
34+
deserializer = PractitionerDetails.serializer(),
35+
string = input.readBytes().decodeToString(),
36+
)
37+
} catch (e: SerializationException) {
38+
Timber.tag(SerializerConstants.PROTOSTORE_SERIALIZER_TAG).d(e)
39+
defaultValue
40+
}
41+
}
42+
43+
override suspend fun writeTo(t: PractitionerDetails, output: OutputStream) {
44+
output.write(
45+
Json.encodeToString(
46+
serializer = PractitionerDetails.serializer(),
47+
value = t,
48+
)
49+
.encodeToByteArray(),
50+
)
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright 2021-2023 Ona Systems, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.smartregister.fhircore.engine.datastore.serializers
18+
19+
object SerializerConstants {
20+
const val PROTOSTORE_SERIALIZER_TAG = "Proto DataStore"
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2021-2023 Ona Systems, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.smartregister.fhircore.engine.datastore.serializers
18+
19+
import androidx.datastore.core.Serializer
20+
import java.io.InputStream
21+
import java.io.OutputStream
22+
import kotlinx.serialization.json.Json
23+
import org.apache.commons.lang3.SerializationException
24+
import org.smartregister.fhircore.engine.datastore.mockdata.UserInfo
25+
import timber.log.Timber
26+
27+
object UserInfoDataStoreSerializer : Serializer<UserInfo> {
28+
override val defaultValue: UserInfo
29+
get() = UserInfo()
30+
31+
override suspend fun readFrom(input: InputStream): UserInfo {
32+
return try {
33+
Json.decodeFromString(
34+
deserializer = UserInfo.serializer(),
35+
string = input.readBytes().decodeToString(),
36+
)
37+
} catch (e: SerializationException) {
38+
Timber.tag(SerializerConstants.PROTOSTORE_SERIALIZER_TAG).d(e)
39+
defaultValue
40+
}
41+
}
42+
43+
override suspend fun writeTo(t: UserInfo, output: OutputStream) {
44+
output.write(
45+
Json.encodeToString(
46+
serializer = UserInfo.serializer(),
47+
value = t,
48+
)
49+
.encodeToByteArray(),
50+
)
51+
}
52+
}

0 commit comments

Comments
 (0)