Skip to content

Commit 59cfd5b

Browse files
committed
Merge branch 'proto-datastore' into main
2 parents ec94606 + d04f9ae commit 59cfd5b

File tree

9 files changed

+247
-1
lines changed

9 files changed

+247
-1
lines changed

app/build.gradle.kts

+23
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ plugins {
66
id("com.google.dagger.hilt.android")
77
id("kotlin-kapt")
88
id("com.google.devtools.ksp")
9+
id("com.google.protobuf")
10+
kotlin("plugin.serialization")
911
}
1012

1113
android {
@@ -111,6 +113,11 @@ dependencies {
111113
implementation("androidx.room:room-ktx:2.5.2")
112114
ksp("androidx.room:room-compiler:2.5.2")
113115

116+
// DataStore
117+
implementation("androidx.datastore:datastore:1.0.0")
118+
implementation("com.google.protobuf:protobuf-javalite:3.22.2")
119+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
120+
114121
// Coil
115122
implementation("io.coil-kt:coil-compose:2.4.0")
116123
implementation("io.coil-kt:coil-svg:2.4.0")
@@ -148,3 +155,19 @@ fun getApiKey(): String {
148155

149156
return apikeyProperties.getProperty("API_KEY")
150157
}
158+
159+
protobuf {
160+
protoc {
161+
artifact = "com.google.protobuf:protoc:3.24.4"
162+
}
163+
164+
generateProtoTasks {
165+
all().forEach { task ->
166+
task.builtins {
167+
create("java") {
168+
option("lite")
169+
}
170+
}
171+
}
172+
}
173+
}

app/proguard-rules.pro

+5
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,8 @@
1010
-keepclasseswithmembers,allowobfuscation class * {
1111
@com.google.gson.annotations.SerializedName <fields>;
1212
}
13+
14+
# Proto DataStore
15+
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite {
16+
<fields>;
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package dev.shorthouse.coinwatch.data
2+
3+
import android.content.Context
4+
import androidx.datastore.core.DataStoreFactory
5+
import androidx.datastore.dataStoreFile
6+
import androidx.test.core.app.ApplicationProvider
7+
import com.google.common.truth.Truth.assertThat
8+
import dev.shorthouse.coinwatch.data.datastore.Currency
9+
import dev.shorthouse.coinwatch.data.datastore.UserPreferences
10+
import dev.shorthouse.coinwatch.data.datastore.UserPreferencesRepository
11+
import dev.shorthouse.coinwatch.data.datastore.UserPreferencesSerializer
12+
import kotlinx.coroutines.Dispatchers
13+
import kotlinx.coroutines.ExperimentalCoroutinesApi
14+
import kotlinx.coroutines.Job
15+
import kotlinx.coroutines.cancel
16+
import kotlinx.coroutines.flow.first
17+
import kotlinx.coroutines.test.StandardTestDispatcher
18+
import kotlinx.coroutines.test.TestScope
19+
import kotlinx.coroutines.test.resetMain
20+
import kotlinx.coroutines.test.runTest
21+
import kotlinx.coroutines.test.setMain
22+
import org.junit.After
23+
import org.junit.Before
24+
import org.junit.Test
25+
26+
@OptIn(ExperimentalCoroutinesApi::class)
27+
class UserPreferencesRepositoryTest {
28+
29+
private val testContext: Context = ApplicationProvider.getApplicationContext()
30+
private val testCoroutineDispatcher = StandardTestDispatcher()
31+
private val testCoroutineScope = TestScope(testCoroutineDispatcher + Job())
32+
33+
private val testDataStore = DataStoreFactory.create(
34+
serializer = UserPreferencesSerializer,
35+
scope = testCoroutineScope,
36+
produceFile = { testContext.dataStoreFile("test_datastore") }
37+
)
38+
39+
// Class under test
40+
private val userPreferencesRepository = UserPreferencesRepository(testDataStore)
41+
42+
@Before
43+
fun setup() {
44+
Dispatchers.setMain(testCoroutineDispatcher)
45+
}
46+
47+
@After
48+
fun cleanup() {
49+
Dispatchers.resetMain()
50+
testCoroutineScope.cancel()
51+
}
52+
53+
@Test
54+
fun when_setCurrencyToUSD_should_updateUserPreferencesCurrencyToUSD() =
55+
testCoroutineScope.runTest {
56+
val expectedUserPreferences = UserPreferences(
57+
currency = Currency.USD
58+
)
59+
60+
userPreferencesRepository.updateCurrency(
61+
currency = Currency.USD
62+
)
63+
64+
val userPreferences = userPreferencesRepository
65+
.userPreferencesFlow
66+
.first()
67+
68+
assertThat(userPreferences).isEqualTo(expectedUserPreferences)
69+
}
70+
71+
@Test
72+
fun when_setCurrencyToGBP_should_updateUserPreferencesCurrencyToGBP() =
73+
testCoroutineScope.runTest {
74+
val expectedUserPreferences = UserPreferences(
75+
currency = Currency.GBP
76+
)
77+
78+
userPreferencesRepository.updateCurrency(
79+
currency = Currency.GBP
80+
)
81+
82+
val userPreferences = userPreferencesRepository
83+
.userPreferencesFlow
84+
.first()
85+
86+
assertThat(userPreferences).isEqualTo(expectedUserPreferences)
87+
}
88+
89+
@Test
90+
fun when_setCurrencyToEUR_should_updateUserPreferencesCurrencyToEUR() =
91+
testCoroutineScope.runTest {
92+
val expectedUserPreferences = UserPreferences(
93+
currency = Currency.EUR
94+
)
95+
96+
userPreferencesRepository.updateCurrency(
97+
currency = Currency.EUR
98+
)
99+
100+
val userPreferences = userPreferencesRepository
101+
.userPreferencesFlow
102+
.first()
103+
104+
assertThat(userPreferences).isEqualTo(expectedUserPreferences)
105+
}
106+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package dev.shorthouse.coinwatch.data.datastore
2+
3+
import androidx.annotation.StringRes
4+
import dev.shorthouse.coinwatch.R
5+
import kotlinx.serialization.Serializable
6+
7+
@Serializable
8+
data class UserPreferences(
9+
val currency: Currency = Currency.USD
10+
)
11+
12+
enum class Currency(@StringRes val nameStringId: Int) {
13+
USD(R.string.currency_usd),
14+
GBP(R.string.currency_gbp),
15+
EUR(R.string.currency_eur)
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package dev.shorthouse.coinwatch.data.datastore
2+
3+
import androidx.datastore.core.DataStore
4+
import java.io.IOException
5+
import javax.inject.Inject
6+
import kotlinx.coroutines.flow.Flow
7+
import kotlinx.coroutines.flow.catch
8+
import timber.log.Timber
9+
10+
class UserPreferencesRepository @Inject constructor(
11+
private val userPreferencesDataStore: DataStore<UserPreferences>
12+
) {
13+
val userPreferencesFlow: Flow<UserPreferences> = userPreferencesDataStore.data
14+
.catch { exception ->
15+
if (exception is IOException) {
16+
Timber.e("Error reading user preferences", exception)
17+
emit(UserPreferences())
18+
} else {
19+
throw exception
20+
}
21+
}
22+
23+
suspend fun updateCurrency(currency: Currency) {
24+
userPreferencesDataStore.updateData { currentPreferences ->
25+
currentPreferences.copy(currency = currency)
26+
}
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package dev.shorthouse.coinwatch.data.datastore
2+
3+
import androidx.datastore.core.Serializer
4+
import java.io.InputStream
5+
import java.io.OutputStream
6+
import kotlinx.serialization.SerializationException
7+
import kotlinx.serialization.json.Json
8+
import timber.log.Timber
9+
10+
object UserPreferencesSerializer : Serializer<UserPreferences> {
11+
override val defaultValue = UserPreferences()
12+
13+
override suspend fun readFrom(input: InputStream): UserPreferences {
14+
return try {
15+
Json.decodeFromString(
16+
deserializer = UserPreferences.serializer(),
17+
string = input.readBytes().decodeToString()
18+
)
19+
} catch (exception: SerializationException) {
20+
Timber.e("Error serializing user preferences", exception)
21+
defaultValue
22+
}
23+
}
24+
25+
override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
26+
output.write(
27+
Json.encodeToString(serializer = UserPreferences.serializer(), value = t)
28+
.encodeToByteArray()
29+
)
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package dev.shorthouse.coinwatch.di
2+
3+
import android.content.Context
4+
import androidx.datastore.core.DataStore
5+
import androidx.datastore.core.DataStoreFactory
6+
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
7+
import androidx.datastore.dataStoreFile
8+
import dagger.Module
9+
import dagger.Provides
10+
import dagger.hilt.InstallIn
11+
import dagger.hilt.android.qualifiers.ApplicationContext
12+
import dagger.hilt.components.SingletonComponent
13+
import dev.shorthouse.coinwatch.data.datastore.UserPreferences
14+
import dev.shorthouse.coinwatch.data.datastore.UserPreferencesSerializer
15+
import javax.inject.Singleton
16+
17+
@InstallIn(SingletonComponent::class)
18+
@Module
19+
class DataStoreModule {
20+
21+
@Provides
22+
@Singleton
23+
fun provideProtoDataStore(@ApplicationContext appContext: Context): DataStore<UserPreferences> {
24+
return DataStoreFactory.create(
25+
serializer = UserPreferencesSerializer,
26+
produceFile = { appContext.dataStoreFile("user_preferences.pb") },
27+
corruptionHandler = ReplaceFileCorruptionHandler(
28+
produceNewData = { UserPreferences() }
29+
)
30+
)
31+
}
32+
}

app/src/main/res/values/strings.xml

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<resources>
2-
<string name="app_name">CoinWatch</string>
2+
<string name="app_name" translatable="false">CoinWatch</string>
33
<string name="cd_top_bar_back">Back</string>
44
<string name="cd_top_bar_favourite">Favourite</string>
55
<string name="good_morning">Good morning</string>
@@ -55,4 +55,7 @@
5555
<string name="search_screen">Search</string>
5656
<string name="favourites_screen">Favourites</string>
5757
<string name="error_state_favourite_coins">Unable to fetch favourite coins</string>
58+
<string name="currency_usd" translatable="false">USD</string>
59+
<string name="currency_gbp" translatable="false">GBP</string>
60+
<string name="currency_eur" translatable="false">EUR</string>
5861
</resources>

build.gradle.kts

+2
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ plugins {
44
id("org.jetbrains.kotlin.android") version "1.8.20" apply false
55
id("com.google.dagger.hilt.android") version "2.48.1" apply false
66
id("com.google.devtools.ksp") version "1.8.20-1.0.11" apply false
7+
id("com.google.protobuf") version "0.9.4" apply false
8+
kotlin("plugin.serialization") version "1.8.20" apply false
79
}

0 commit comments

Comments
 (0)