From 3f31c5a1a54111add798bafa459f73ceb5c3af17 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Mon, 6 Jul 2026 02:46:21 +0500 Subject: [PATCH 01/12] Extract quick item currency title string resource --- core/presentation/src/main/res/values-ru/strings.xml | 1 + core/presentation/src/main/res/values/strings.xml | 1 + .../rate/feature/quick/presentation/main/QuickItem.kt | 5 ++++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/presentation/src/main/res/values-ru/strings.xml b/core/presentation/src/main/res/values-ru/strings.xml index cd75cdcaf..7c3bb5a8a 100644 --- a/core/presentation/src/main/res/values-ru/strings.xml +++ b/core/presentation/src/main/res/values-ru/strings.xml @@ -29,6 +29,7 @@ Расчет удален Вы удалили расчет %1$s %1$s в %2$s + %1$s к %2$s Открыть приложение Из В diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index 5c3f57606..492e766da 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -28,6 +28,7 @@ The calculation has been deleted You deleted %1$s calculation %1$s to %2$s + %1$s to %2$s Open app From To diff --git a/feature/quick/src/main/java/dev/arkbuilders/rate/feature/quick/presentation/main/QuickItem.kt b/feature/quick/src/main/java/dev/arkbuilders/rate/feature/quick/presentation/main/QuickItem.kt index b5b1f0940..1f30c98e6 100644 --- a/feature/quick/src/main/java/dev/arkbuilders/rate/feature/quick/presentation/main/QuickItem.kt +++ b/feature/quick/src/main/java/dev/arkbuilders/rate/feature/quick/presentation/main/QuickItem.kt @@ -134,8 +134,11 @@ fun QuickItem( ) { Text( text = - "${from.code} to " + + stringResource( + CoreRString.quick_item_currencies_to, + from.code, to.joinToString(", ") { it.code }, + ), fontWeight = FontWeight.Medium, color = ArkColor.TextPrimary, ) From 13c5b69e5bdb06fd0799b9a930d9a715f2cc80db Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Mon, 6 Jul 2026 07:06:12 +0500 Subject: [PATCH 02/12] Update rates in splash before showing quick screen --- .../presentation/splash/SplashViewModel.kt | 16 ++++++ .../data/repo/currency/CurrencyRepoImpl.kt | 57 +++++++++++-------- .../rate/core/domain/repo/CurrencyRepo.kt | 3 + 3 files changed, 52 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/dev/arkbuilders/rate/presentation/splash/SplashViewModel.kt b/app/src/main/java/dev/arkbuilders/rate/presentation/splash/SplashViewModel.kt index 56e6b2e09..eee458939 100644 --- a/app/src/main/java/dev/arkbuilders/rate/presentation/splash/SplashViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/rate/presentation/splash/SplashViewModel.kt @@ -2,16 +2,21 @@ package dev.arkbuilders.rate.presentation.splash import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope import dev.arkbuilders.rate.core.domain.BuildConfigFields import dev.arkbuilders.rate.core.domain.repo.CurrencyRepo import dev.arkbuilders.rate.core.domain.repo.PreferenceKey import dev.arkbuilders.rate.core.domain.repo.Prefs import dev.arkbuilders.rate.feature.portfolio.domain.repo.PortfolioRepo import dev.arkbuilders.rate.feature.quick.domain.repo.QuickRepo +import kotlinx.coroutines.async +import kotlinx.coroutines.withTimeoutOrNull import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container +private const val UPDATE_RATES_TIMEOUT_MS = 1_500L + sealed class SplashScreenEffect { data object NavigateOnboarding : SplashScreenEffect() @@ -30,6 +35,7 @@ class SplashViewModel( init { intent { currencyRepo.initialize() + val updateRates = updateRatesAsync() val currentVersionCode = buildConfigFields.versionCode val previousVersionCode = prefs.get(PreferenceKey.CurrentVersionCode) @@ -49,6 +55,9 @@ class SplashViewModel( prefs.set(PreferenceKey.CurrentVersionCode, currentVersionCode) val needToShowOnboarding = prefs.get(PreferenceKey.IsOnboardingCompleted).not() + + updateRates.await() + if (needToShowOnboarding) postSideEffect(SplashScreenEffect.NavigateOnboarding) else @@ -56,6 +65,13 @@ class SplashViewModel( } } + private fun updateRatesAsync() = + viewModelScope.async { + withTimeoutOrNull(UPDATE_RATES_TIMEOUT_MS) { + currencyRepo.updateRates() + } + } + private suspend fun skipOnboardingIfUserHasData() { if (quickRepo.getAll().isNotEmpty() || portfolioRepo.allAssets().isNotEmpty()) { prefs.set(PreferenceKey.IsOnboardingCompleted, true) diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CurrencyRepoImpl.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CurrencyRepoImpl.kt index c74b57c86..4e2950604 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CurrencyRepoImpl.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CurrencyRepoImpl.kt @@ -12,6 +12,7 @@ import dev.arkbuilders.rate.core.domain.repo.NetworkStatus import dev.arkbuilders.rate.core.domain.repo.TimestampRepo import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.async import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -72,31 +73,39 @@ class CurrencyRepoImpl @Inject constructor( return infoMap[code] ?: CurrencyInfo.emptyWithCode(code) } - private suspend fun updateRates(): Either> = - updateRatesMutex.withLock { - val updatedDate = - timestampRepo - .getTimestamp(TimestampType.FetchRates) - - if (networkStatus.isOnline().not()) { - return IllegalStateException("Skip rate updates").left() - } - - val remoteUpdatedAt = - ratesUpdatedAtDataSource - .fetchRemote() - .onLeft { return@withLock it.left() } - .getOrNull()!! - if (isNewer(remoteUpdatedAt, updatedDate).not()) { - return IllegalStateException("Skip rate updates").left() + override suspend fun updateRates(): Either> = + withContext(Dispatchers.IO) { + updateRatesMutex.withLock { + val updatedDate = + timestampRepo + .getTimestamp(TimestampType.FetchRates) + + if (networkStatus.isOnline().not()) { + return@withLock IllegalStateException("Skip rate updates").left() + } + + val remoteUpdatedAt = + ratesUpdatedAtDataSource + .fetchRemote() + .onLeft { return@withLock it.left() } + .getOrNull()!! + + if (isNewer(remoteUpdatedAt, updatedDate).not()) { + return@withLock IllegalStateException("Skip rate updates").left() + } + + val cryptoDeferred = async { cryptoDataSource.fetchRemote() } + val fiatDeferred = async { fiatDataSource.fetchRemote() } + val crypto = cryptoDeferred.await() + val fiat = fiatDeferred.await() + crypto.onLeft { return@withLock it.left() } + fiat.onLeft { return@withLock it.left() } + + val rates = crypto.getOrNull()!! + fiat.getOrNull()!! + localCurrencyDataSource.insert(rates) + timestampRepo.rememberTimestamp(TimestampType.FetchRates, remoteUpdatedAt) + return@withLock rates.right() } - - val crypto = cryptoDataSource.fetchRemote().onLeft { return@withLock it.left() } - val fiat = fiatDataSource.fetchRemote().onLeft { return@withLock it.left() } - val rates = crypto.getOrNull()!! + fiat.getOrNull()!! - localCurrencyDataSource.insert(rates) - timestampRepo.rememberTimestamp(TimestampType.FetchRates, remoteUpdatedAt) - return@withLock rates.right() } private suspend fun useFallbackRates(): List { diff --git a/core/domain/src/main/java/dev/arkbuilders/rate/core/domain/repo/CurrencyRepo.kt b/core/domain/src/main/java/dev/arkbuilders/rate/core/domain/repo/CurrencyRepo.kt index eb1bf82a0..09d6d7f03 100644 --- a/core/domain/src/main/java/dev/arkbuilders/rate/core/domain/repo/CurrencyRepo.kt +++ b/core/domain/src/main/java/dev/arkbuilders/rate/core/domain/repo/CurrencyRepo.kt @@ -1,5 +1,6 @@ package dev.arkbuilders.rate.core.domain.repo +import arrow.core.Either import dev.arkbuilders.rate.core.domain.model.CurrencyCode import dev.arkbuilders.rate.core.domain.model.CurrencyInfo import dev.arkbuilders.rate.core.domain.model.CurrencyRate @@ -7,6 +8,8 @@ import dev.arkbuilders.rate.core.domain.model.CurrencyRate interface CurrencyRepo { suspend fun initialize() + suspend fun updateRates(): Either> + suspend fun getCurrencyRates(): List suspend fun getCurrencyInfo(): List From 18e8c633288ede764a81318bcdb730ade9329c13 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Tue, 7 Jul 2026 06:28:42 +0500 Subject: [PATCH 03/12] Refresh quick and portfolio on rates update --- .../presentation/main/PortfolioViewModel.kt | 23 +++++++++++----- .../quick/presentation/main/QuickViewModel.kt | 26 +++++++++++++------ 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/feature/portfolio/src/main/java/dev/arkbuilders/rate/feature/portfolio/presentation/main/PortfolioViewModel.kt b/feature/portfolio/src/main/java/dev/arkbuilders/rate/feature/portfolio/presentation/main/PortfolioViewModel.kt index d654c2c3c..724349960 100644 --- a/feature/portfolio/src/main/java/dev/arkbuilders/rate/feature/portfolio/presentation/main/PortfolioViewModel.kt +++ b/feature/portfolio/src/main/java/dev/arkbuilders/rate/feature/portfolio/presentation/main/PortfolioViewModel.kt @@ -7,11 +7,13 @@ import dev.arkbuilders.rate.core.domain.model.Amount import dev.arkbuilders.rate.core.domain.model.CurrencyCode import dev.arkbuilders.rate.core.domain.model.Group import dev.arkbuilders.rate.core.domain.model.GroupFeatureType +import dev.arkbuilders.rate.core.domain.model.TimestampType import dev.arkbuilders.rate.core.domain.repo.AnalyticsManager import dev.arkbuilders.rate.core.domain.repo.CurrencyRepo import dev.arkbuilders.rate.core.domain.repo.GroupRepo import dev.arkbuilders.rate.core.domain.repo.PreferenceKey import dev.arkbuilders.rate.core.domain.repo.Prefs +import dev.arkbuilders.rate.core.domain.repo.TimestampRepo import dev.arkbuilders.rate.core.domain.usecase.ConvertWithRateUseCase import dev.arkbuilders.rate.core.domain.usecase.GroupReorderSwapUseCase import dev.arkbuilders.rate.core.presentation.ui.group.EditGroupOptionsSheetState @@ -70,6 +72,7 @@ class PortfolioViewModel( private val convertUseCase: ConvertWithRateUseCase, private val groupReorderSwapUseCase: GroupReorderSwapUseCase, private val analyticsManager: AnalyticsManager, + private val timestampRepo: TimestampRepo, ) : ViewModel(), ContainerHost { override val container: Container = container(PortfolioScreenState()) @@ -80,21 +83,25 @@ class PortfolioViewModel( private fun init() = intent { - initPages() - assetsRepo.allAssetsFlow().drop(1).onEach { - initPages() + refreshPages() }.launchIn(viewModelScope) groupRepo.allFlow(GroupFeatureType.Portfolio).drop(1).onEach { - initPages() + refreshPages() + }.launchIn(viewModelScope) + + timestampRepo.timestampFlow(TimestampType.FetchRates).drop(1).onEach { + refreshPages() }.launchIn(viewModelScope) + + refreshPages() } fun onReturnFromAddScreen(result: AddAssetsNavResult) = intent { if (result.added.isNotEmpty()) { - initPages() + refreshPages() postSideEffect(PortfolioScreenEffect.SelectTab(result.added.first().groupId)) postSideEffect(PortfolioScreenEffect.ShowSnackbarAdded(result.added.toList())) } @@ -104,7 +111,7 @@ class PortfolioViewModel( intent { analyticsManager.logEvent("portfolio_base_currency_changed") prefs.set(PreferenceKey.BaseCurrencyCode, baseCode) - initPages() + refreshPages() } fun onAssetRemove(asset: Asset) = @@ -140,7 +147,7 @@ class PortfolioViewModel( } } - private fun initPages() = + private fun refreshPages() = intent { val baseCode = prefs.get(PreferenceKey.BaseCurrencyCode) val assets = assetsRepo.allAssets().toMutableList() @@ -293,6 +300,7 @@ class PortfolioViewModelFactory @Inject constructor( private val groupReorderSwapUseCase: GroupReorderSwapUseCase, private val analyticsManager: AnalyticsManager, private val groupRepo: GroupRepo, + private val timestampRepo: TimestampRepo, ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return PortfolioViewModel( @@ -303,6 +311,7 @@ class PortfolioViewModelFactory @Inject constructor( convertUseCase, groupReorderSwapUseCase, analyticsManager, + timestampRepo, ) as T } } diff --git a/feature/quick/src/main/java/dev/arkbuilders/rate/feature/quick/presentation/main/QuickViewModel.kt b/feature/quick/src/main/java/dev/arkbuilders/rate/feature/quick/presentation/main/QuickViewModel.kt index d8ab103f3..5a6ccf5fd 100644 --- a/feature/quick/src/main/java/dev/arkbuilders/rate/feature/quick/presentation/main/QuickViewModel.kt +++ b/feature/quick/src/main/java/dev/arkbuilders/rate/feature/quick/presentation/main/QuickViewModel.kt @@ -95,9 +95,9 @@ class QuickViewModel( private fun init() = intent { - quickRepo.allFlow().drop(1).onEach { quick -> + quickRepo.allFlow().drop(1).onEach { intent { - val pages = mapCalculationsToPages(quick) + val pages = buildPages() reduce { state.copy( pages = pages, @@ -108,7 +108,7 @@ class QuickViewModel( groupRepo.allFlow(GroupFeatureType.Quick).drop(1).onEach { intent { - val pages = mapCalculationsToPages(quickRepo.getAll()) + val pages = buildPages() reduce { state.copy( pages = pages, @@ -129,10 +129,21 @@ class QuickViewModel( } }.launchIn(viewModelScope) + timestampRepo.timestampFlow(TimestampType.FetchRates).drop(1).onEach { + intent { + val pages = buildPages() + reduce { + state.copy( + pages = pages, + ) + } + } + }.launchIn(viewModelScope) + val frequent = calcFrequentCurrUseCase() .map { currencyRepo.infoByCode(it) } - val pages = mapCalculationsToPages(quickRepo.getAll()) + val pages = buildPages() reduce { state.copy( currencies = allCurrencies, @@ -151,7 +162,7 @@ class QuickViewModel( } val calculation = quickRepo.getById(newCalculationId) ?: return@intent - val pages = mapCalculationsToPages(quickRepo.getAll()) + val pages = buildPages() reduce { state.copy( pages = pages, @@ -253,9 +264,8 @@ class QuickViewModel( } } - private suspend fun mapCalculationsToPages( - calculations: List, - ): List { + private suspend fun buildPages(): List { + val calculations = quickRepo.getAll() val refreshDate = timestampRepo.getTimestamp(TimestampType.FetchRates) val groups = groupRepo.getAllSorted(GroupFeatureType.Quick) val pages = From 641cfa59b36b517aca1b9cec27e3a595512268f3 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Wed, 8 Jul 2026 08:02:25 +0500 Subject: [PATCH 04/12] Add thousands separators to amount inputs --- .../ui/AmountInputVisualTransformation.kt | 98 +++++++++++++++++++ .../core/presentation/ui/ArkBasicTextField.kt | 4 +- .../core/presentation/ui/ArkLargeTextField.kt | 5 + .../ui/AmountInputVisualTransformationTest.kt | 47 +++++++++ .../presentation/add/InputCurrency.kt | 2 + .../presentation/edit/EditAssetScreen.kt | 2 + .../quick/presentation/add/FromInput.kt | 2 + 7 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 core/presentation/src/main/java/dev/arkbuilders/rate/core/presentation/ui/AmountInputVisualTransformation.kt create mode 100644 core/presentation/src/test/java/dev/arkbuilders/rate/core/presentation/ui/AmountInputVisualTransformationTest.kt diff --git a/core/presentation/src/main/java/dev/arkbuilders/rate/core/presentation/ui/AmountInputVisualTransformation.kt b/core/presentation/src/main/java/dev/arkbuilders/rate/core/presentation/ui/AmountInputVisualTransformation.kt new file mode 100644 index 000000000..de66f06b3 --- /dev/null +++ b/core/presentation/src/main/java/dev/arkbuilders/rate/core/presentation/ui/AmountInputVisualTransformation.kt @@ -0,0 +1,98 @@ +package dev.arkbuilders.rate.core.presentation.ui + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation + +object AmountInputVisualTransformation : VisualTransformation { + override fun filter(text: AnnotatedString): TransformedText { + val formatted = formatAmountInput(text.text) + return TransformedText( + AnnotatedString(formatted.text), + AmountInputOffsetMapping( + originalToTransformed = formatted.originalToTransformed, + transformedToOriginal = formatted.transformedToOriginal, + ), + ) + } +} + +private data class FormattedAmountInput( + val text: String, + val originalToTransformed: IntArray, + val transformedToOriginal: IntArray, +) + +private fun formatAmountInput(input: String): FormattedAmountInput { + val result = StringBuilder() + val originalCharPositions = IntArray(input.length) + val decimalSeparatorIndex = input.indexOf('.') + val integerEnd = + if (decimalSeparatorIndex == -1) + input.length + else + decimalSeparatorIndex + + input.forEachIndexed { index, char -> + if (shouldInsertGroupingSeparator(index, integerEnd)) { + result.append(',') + } + originalCharPositions[index] = result.length + result.append(char) + } + + return FormattedAmountInput( + text = result.toString(), + originalToTransformed = buildOriginalToTransformed(originalCharPositions, input.length), + transformedToOriginal = buildTransformedToOriginal(originalCharPositions, result.length), + ) +} + +private fun shouldInsertGroupingSeparator( + index: Int, + integerEnd: Int, +): Boolean { + return index in 1 until integerEnd && (integerEnd - index) % 3 == 0 +} + +private fun buildOriginalToTransformed( + originalCharPositions: IntArray, + originalLength: Int, +): IntArray { + return IntArray(originalLength + 1) { offset -> + if (offset == 0) + 0 + else + originalCharPositions[offset - 1] + 1 + } +} + +private fun buildTransformedToOriginal( + originalCharPositions: IntArray, + transformedLength: Int, +): IntArray { + var originalOffset = 0 + return IntArray(transformedLength + 1) { transformedOffset -> + while ( + originalOffset < originalCharPositions.size && + originalCharPositions[originalOffset] < transformedOffset + ) { + originalOffset++ + } + originalOffset + } +} + +private class AmountInputOffsetMapping( + private val originalToTransformed: IntArray, + private val transformedToOriginal: IntArray, +) : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + return originalToTransformed[offset.coerceIn(originalToTransformed.indices)] + } + + override fun transformedToOriginal(offset: Int): Int { + return transformedToOriginal[offset.coerceIn(transformedToOriginal.indices)] + } +} diff --git a/core/presentation/src/main/java/dev/arkbuilders/rate/core/presentation/ui/ArkBasicTextField.kt b/core/presentation/src/main/java/dev/arkbuilders/rate/core/presentation/ui/ArkBasicTextField.kt index 7a1913421..a24a22d13 100644 --- a/core/presentation/src/main/java/dev/arkbuilders/rate/core/presentation/ui/ArkBasicTextField.kt +++ b/core/presentation/src/main/java/dev/arkbuilders/rate/core/presentation/ui/ArkBasicTextField.kt @@ -25,6 +25,7 @@ fun ArkBasicTextField( placeholder: (@Composable () -> Unit)? = null, leadingIcon: (@Composable () -> Unit)? = null, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + visualTransformation: VisualTransformation = VisualTransformation.None, ) { val interactionSource = remember { MutableInteractionSource() } BasicTextField( @@ -33,6 +34,7 @@ fun ArkBasicTextField( onValueChange = { onValueChange(it) }, textStyle = textStyle, keyboardOptions = keyboardOptions, + visualTransformation = visualTransformation, interactionSource = interactionSource, singleLine = true, ) { innerTextField -> @@ -50,7 +52,7 @@ fun ArkBasicTextField( ), enabled = true, singleLine = true, - visualTransformation = VisualTransformation.None, + visualTransformation = visualTransformation, interactionSource = interactionSource, placeholder = placeholder, ) diff --git a/core/presentation/src/main/java/dev/arkbuilders/rate/core/presentation/ui/ArkLargeTextField.kt b/core/presentation/src/main/java/dev/arkbuilders/rate/core/presentation/ui/ArkLargeTextField.kt index 1f3c55015..c287b62b0 100644 --- a/core/presentation/src/main/java/dev/arkbuilders/rate/core/presentation/ui/ArkLargeTextField.kt +++ b/core/presentation/src/main/java/dev/arkbuilders/rate/core/presentation/ui/ArkLargeTextField.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import dev.arkbuilders.rate.core.presentation.theme.ArkColor @@ -23,6 +24,7 @@ fun ArkLargeTextField( modifier: Modifier, value: String, onValueChange: (String) -> Unit, + visualTransformation: VisualTransformation = VisualTransformation.None, ) { BasicTextField( modifier = @@ -40,6 +42,7 @@ fun ArkLargeTextField( keyboardOptions = KeyboardOptions.Default .copy(keyboardType = KeyboardType.Number), + visualTransformation = visualTransformation, singleLine = true, ) } @@ -50,6 +53,7 @@ fun ArkCursorLargeTextField( modifier: Modifier, value: String, onValueChange: (String) -> Unit, + visualTransformation: VisualTransformation = VisualTransformation.None, ) { var currentTextFieldValue = remember { @@ -78,6 +82,7 @@ fun ArkCursorLargeTextField( keyboardOptions = KeyboardOptions.Default .copy(keyboardType = KeyboardType.Number), + visualTransformation = visualTransformation, singleLine = true, ) } diff --git a/core/presentation/src/test/java/dev/arkbuilders/rate/core/presentation/ui/AmountInputVisualTransformationTest.kt b/core/presentation/src/test/java/dev/arkbuilders/rate/core/presentation/ui/AmountInputVisualTransformationTest.kt new file mode 100644 index 000000000..b2c926fee --- /dev/null +++ b/core/presentation/src/test/java/dev/arkbuilders/rate/core/presentation/ui/AmountInputVisualTransformationTest.kt @@ -0,0 +1,47 @@ +package dev.arkbuilders.rate.core.presentation.ui + +import androidx.compose.ui.text.AnnotatedString +import org.junit.Assert.assertEquals +import org.junit.Test + +class AmountInputVisualTransformationTest { + @Test + fun formatsIntegerPartWithGroupingSeparators() { + val cases = + mapOf( + "" to "", + "." to ".", + "1000" to "1,000", + "1000000" to "1,000,000", + "1000.50" to "1,000.50", + "1000." to "1,000.", + ) + + cases.forEach { (input, expected) -> + assertEquals(expected, transform(input).text.text) + } + } + + @Test + fun mapsOriginalOffsetsToTransformedOffsets() { + val mapping = transform("1000.50").offsetMapping + val expectedOffsets = listOf(0, 1, 3, 4, 5, 6, 7, 8) + + expectedOffsets.forEachIndexed { originalOffset, transformedOffset -> + assertEquals(transformedOffset, mapping.originalToTransformed(originalOffset)) + } + } + + @Test + fun mapsTransformedOffsetsToOriginalOffsets() { + val mapping = transform("1000.50").offsetMapping + val expectedOffsets = listOf(0, 1, 1, 2, 3, 4, 5, 6, 7) + + expectedOffsets.forEachIndexed { transformedOffset, originalOffset -> + assertEquals(originalOffset, mapping.transformedToOriginal(transformedOffset)) + } + } + + private fun transform(input: String) = + AmountInputVisualTransformation.filter(AnnotatedString(input)) +} diff --git a/feature/portfolio/src/main/java/dev/arkbuilders/rate/feature/portfolio/presentation/add/InputCurrency.kt b/feature/portfolio/src/main/java/dev/arkbuilders/rate/feature/portfolio/presentation/add/InputCurrency.kt index 0ce196e30..536c00dd5 100644 --- a/feature/portfolio/src/main/java/dev/arkbuilders/rate/feature/portfolio/presentation/add/InputCurrency.kt +++ b/feature/portfolio/src/main/java/dev/arkbuilders/rate/feature/portfolio/presentation/add/InputCurrency.kt @@ -28,6 +28,7 @@ import dev.arkbuilders.rate.core.presentation.CoreRDrawable import dev.arkbuilders.rate.core.presentation.CoreRString import dev.arkbuilders.rate.core.presentation.R import dev.arkbuilders.rate.core.presentation.theme.ArkColor +import dev.arkbuilders.rate.core.presentation.ui.AmountInputVisualTransformation import dev.arkbuilders.rate.core.presentation.ui.ArkBasicTextField @Composable @@ -88,6 +89,7 @@ fun InputCurrency( keyboardOptions = KeyboardOptions.Default .copy(keyboardType = KeyboardType.Number), + visualTransformation = AmountInputVisualTransformation, placeholder = { Text( text = stringResource(CoreRString.input_value), diff --git a/feature/portfolio/src/main/java/dev/arkbuilders/rate/feature/portfolio/presentation/edit/EditAssetScreen.kt b/feature/portfolio/src/main/java/dev/arkbuilders/rate/feature/portfolio/presentation/edit/EditAssetScreen.kt index 6f8ce6040..e581f30ff 100644 --- a/feature/portfolio/src/main/java/dev/arkbuilders/rate/feature/portfolio/presentation/edit/EditAssetScreen.kt +++ b/feature/portfolio/src/main/java/dev/arkbuilders/rate/feature/portfolio/presentation/edit/EditAssetScreen.kt @@ -37,6 +37,7 @@ import dev.arkbuilders.rate.core.domain.CurrUtils import dev.arkbuilders.rate.core.domain.model.CurrencyInfo import dev.arkbuilders.rate.core.presentation.CoreRString import dev.arkbuilders.rate.core.presentation.theme.ArkColor +import dev.arkbuilders.rate.core.presentation.ui.AmountInputVisualTransformation import dev.arkbuilders.rate.core.presentation.ui.AppHorDiv import dev.arkbuilders.rate.core.presentation.ui.AppTopBarBack import dev.arkbuilders.rate.core.presentation.ui.ArkCursorLargeTextField @@ -168,6 +169,7 @@ private fun Content( }, value = value, onValueChange = { onValueChange(it) }, + visualTransformation = AmountInputVisualTransformation, ) Text( modifier = diff --git a/feature/quick/src/main/java/dev/arkbuilders/rate/feature/quick/presentation/add/FromInput.kt b/feature/quick/src/main/java/dev/arkbuilders/rate/feature/quick/presentation/add/FromInput.kt index 6e78d2df1..6ded37622 100644 --- a/feature/quick/src/main/java/dev/arkbuilders/rate/feature/quick/presentation/add/FromInput.kt +++ b/feature/quick/src/main/java/dev/arkbuilders/rate/feature/quick/presentation/add/FromInput.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.unit.sp import dev.arkbuilders.rate.core.domain.model.CurrencyCode import dev.arkbuilders.rate.core.presentation.R import dev.arkbuilders.rate.core.presentation.theme.ArkColor +import dev.arkbuilders.rate.core.presentation.ui.AmountInputVisualTransformation import dev.arkbuilders.rate.core.presentation.ui.ArkBasicTextField import dev.arkbuilders.rate.core.presentation.utils.ReorderHapticFeedback import dev.arkbuilders.rate.core.presentation.utils.ReorderHapticFeedbackType @@ -114,6 +115,7 @@ fun FromInput( keyboardOptions = KeyboardOptions.Default .copy(keyboardType = KeyboardType.Number), + visualTransformation = AmountInputVisualTransformation, textStyle = TextStyle.Default.copy( color = ArkColor.TextPrimary, From 70517fdbae0720a69b778979972d6641c9dbf49a Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Wed, 8 Jul 2026 10:37:45 +0500 Subject: [PATCH 05/12] Replace Retrofit with Ktor --- app/build.gradle.kts | 4 -- core/data/build.gradle.kts | 9 ++- .../core/data/network/OkHttpClientBuilder.kt | 26 --------- .../rate/core/data/network/api/CryptoAPI.kt | 11 ---- .../rate/core/data/network/api/FiatAPI.kt | 11 ---- .../core/data/network/api/UpdatedAtAPI.kt | 11 ---- .../network/client/KtorHttpClientFactory.kt | 55 ++++++++++++++++++ .../core/data/network/client/NetworkConfig.kt | 13 +++++ .../data/network/remote/RatesApiClient.kt | 45 +++++++++++++++ .../repo/currency/CryptoCurrencyDataSource.kt | 6 +- .../repo/currency/FiatCurrencyDataSource.kt | 6 +- .../repo/currency/RatesUpdatedAtDataSource.kt | 6 +- core/di/build.gradle.kts | 4 +- .../rate/core/di/modules/ApiModule.kt | 56 +------------------ gradle/libs.versions.toml | 12 ++-- 15 files changed, 138 insertions(+), 137 deletions(-) delete mode 100644 core/data/src/main/java/dev/arkbuilders/rate/core/data/network/OkHttpClientBuilder.kt delete mode 100644 core/data/src/main/java/dev/arkbuilders/rate/core/data/network/api/CryptoAPI.kt delete mode 100644 core/data/src/main/java/dev/arkbuilders/rate/core/data/network/api/FiatAPI.kt delete mode 100644 core/data/src/main/java/dev/arkbuilders/rate/core/data/network/api/UpdatedAtAPI.kt create mode 100644 core/data/src/main/java/dev/arkbuilders/rate/core/data/network/client/KtorHttpClientFactory.kt create mode 100644 core/data/src/main/java/dev/arkbuilders/rate/core/data/network/client/NetworkConfig.kt create mode 100644 core/data/src/main/java/dev/arkbuilders/rate/core/data/network/remote/RatesApiClient.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7bde0bf88..676645ec0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -168,10 +168,6 @@ dependencies { implementation(libs.compose.destinations.core) ksp(libs.compose.destinations.compiler) - implementation(libs.retrofit) - implementation(libs.converter.gson) - implementation(libs.logging.interceptor) - implementation(platform(libs.firebase.bom)) implementation(libs.firebase.analytics) implementation(libs.firebase.crashlytics) diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index c81f051ec..a7cbbffe3 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -56,9 +56,12 @@ dependencies { implementation(libs.arrow.core) implementation(libs.arrow.fx.coroutines) - implementation(libs.retrofit) - implementation(libs.converter.gson) - implementation(libs.logging.interceptor) + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.okhttp) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.serialization.gson) + implementation(libs.ktor.client.logging) + implementation(libs.gson) implementation(platform(libs.firebase.bom)) implementation(libs.firebase.analytics) diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/OkHttpClientBuilder.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/OkHttpClientBuilder.kt deleted file mode 100644 index 419ef1fad..000000000 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/OkHttpClientBuilder.kt +++ /dev/null @@ -1,26 +0,0 @@ -package dev.arkbuilders.rate.core.data.network - -import android.content.Context -import android.webkit.WebSettings -import okhttp3.OkHttpClient -import javax.inject.Inject - -class OkHttpClientBuilder @Inject constructor(val context: Context) { - fun build(): OkHttpClient { - val agent = WebSettings.getDefaultUserAgent(context) - - val client = - OkHttpClient.Builder() - .addNetworkInterceptor { chain -> - chain.proceed( - chain.request() - .newBuilder() - .header("User-Agent", agent) - .build(), - ) - } - .build() - - return client - } -} diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/api/CryptoAPI.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/api/CryptoAPI.kt deleted file mode 100644 index eaf468be1..000000000 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/api/CryptoAPI.kt +++ /dev/null @@ -1,11 +0,0 @@ -@file:Suppress("ktlint") - -package dev.arkbuilders.rate.core.data.network.api - -import dev.arkbuilders.rate.core.data.network.dto.CryptoRateResponse -import retrofit2.http.GET - -interface CryptoAPI { - @GET("/ARK-Builders/ARK-Rate/refs/heads/data/currency-icons-and-rates/core/data/src/main/assets/crypto-rates.json") - suspend fun getCryptoRates(): List -} diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/api/FiatAPI.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/api/FiatAPI.kt deleted file mode 100644 index 552ae03b2..000000000 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/api/FiatAPI.kt +++ /dev/null @@ -1,11 +0,0 @@ -@file:Suppress("ktlint") - -package dev.arkbuilders.rate.core.data.network.api - -import dev.arkbuilders.rate.core.data.network.dto.FiatRateResponse -import retrofit2.http.GET - -interface FiatAPI { - @GET("/ARK-Builders/ARK-Rate/refs/heads/data/currency-icons-and-rates/core/data/src/main/assets/fiat-rates.json") - suspend fun get(): FiatRateResponse -} diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/api/UpdatedAtAPI.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/api/UpdatedAtAPI.kt deleted file mode 100644 index 181cca1f1..000000000 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/api/UpdatedAtAPI.kt +++ /dev/null @@ -1,11 +0,0 @@ -@file:Suppress("ktlint") - -package dev.arkbuilders.rate.core.data.network.api - -import okhttp3.ResponseBody -import retrofit2.http.GET - -interface UpdatedAtAPI { - @GET("/ARK-Builders/ARK-Rate/refs/heads/data/currency-icons-and-rates/core/data/src/main/assets/updatedAt") - suspend fun get(): ResponseBody -} diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/client/KtorHttpClientFactory.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/client/KtorHttpClientFactory.kt new file mode 100644 index 000000000..024595b43 --- /dev/null +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/client/KtorHttpClientFactory.kt @@ -0,0 +1,55 @@ +package dev.arkbuilders.rate.core.data.network.client + +import android.content.Context +import android.webkit.WebSettings +import dev.arkbuilders.rate.core.domain.BuildConfigFieldsProvider +import io.ktor.client.HttpClient +import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logger +import io.ktor.client.plugins.logging.Logging +import io.ktor.client.request.header +import io.ktor.http.HttpHeaders +import io.ktor.serialization.gson.gson +import timber.log.Timber +import javax.inject.Inject + +class KtorHttpClientFactory @Inject constructor( + private val context: Context, + private val buildConfigFieldsProvider: BuildConfigFieldsProvider, +) { + fun create(): HttpClient { + val userAgent = WebSettings.getDefaultUserAgent(context.applicationContext) + val buildConfigFields = buildConfigFieldsProvider.provide() + + return HttpClient(OkHttp) { + expectSuccess = true + + defaultRequest { + header(HttpHeaders.UserAgent, userAgent) + } + + install(ContentNegotiation) { + gson() + } + + if (buildConfigFields.buildType == DEBUG_BUILD_TYPE) { + install(Logging) { + logger = + object : Logger { + override fun log(message: String) { + Timber.d(message) + } + } + level = LogLevel.ALL + } + } + } + } + + companion object { + private const val DEBUG_BUILD_TYPE = "debug" + } +} diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/client/NetworkConfig.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/client/NetworkConfig.kt new file mode 100644 index 000000000..bd5ef1a26 --- /dev/null +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/client/NetworkConfig.kt @@ -0,0 +1,13 @@ +package dev.arkbuilders.rate.core.data.network.client + +internal object NetworkConfig { + const val HOST = "raw.githubusercontent.com" + + private const val RATES_ASSETS_PATH = + "/ARK-Builders/ARK-Rate/refs/heads/data/currency-icons-and-rates" + + "/core/data/src/main/assets" + + const val CRYPTO_RATES_PATH = "$RATES_ASSETS_PATH/crypto-rates.json" + const val FIAT_RATES_PATH = "$RATES_ASSETS_PATH/fiat-rates.json" + const val UPDATED_AT_PATH = "$RATES_ASSETS_PATH/updatedAt" +} diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/remote/RatesApiClient.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/remote/RatesApiClient.kt new file mode 100644 index 000000000..aba8da4c0 --- /dev/null +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/remote/RatesApiClient.kt @@ -0,0 +1,45 @@ +package dev.arkbuilders.rate.core.data.network.remote + +import dev.arkbuilders.rate.core.data.network.client.NetworkConfig +import dev.arkbuilders.rate.core.data.network.dto.CryptoRateResponse +import dev.arkbuilders.rate.core.data.network.dto.FiatRateResponse +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import io.ktor.http.URLProtocol +import io.ktor.http.encodedPath +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class RatesApiClient @Inject constructor( + private val client: HttpClient, +) { + suspend fun getCryptoRates(): List = + client.get { + url { + protocol = URLProtocol.HTTPS + host = NetworkConfig.HOST + encodedPath = NetworkConfig.CRYPTO_RATES_PATH + } + }.body() + + suspend fun getFiatRates(): FiatRateResponse = + client.get { + url { + protocol = URLProtocol.HTTPS + host = NetworkConfig.HOST + encodedPath = NetworkConfig.FIAT_RATES_PATH + } + }.body() + + suspend fun getUpdatedAt(): String = + client.get { + url { + protocol = URLProtocol.HTTPS + host = NetworkConfig.HOST + encodedPath = NetworkConfig.UPDATED_AT_PATH + } + }.bodyAsText() +} diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CryptoCurrencyDataSource.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CryptoCurrencyDataSource.kt index ba0179db5..7b051075d 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CryptoCurrencyDataSource.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CryptoCurrencyDataSource.kt @@ -4,20 +4,20 @@ import arrow.core.Either import arrow.core.left import arrow.core.right import dev.arkbuilders.rate.core.data.mapper.CryptoRateResponseMapper -import dev.arkbuilders.rate.core.data.network.api.CryptoAPI +import dev.arkbuilders.rate.core.data.network.remote.RatesApiClient import dev.arkbuilders.rate.core.domain.model.CurrencyRate import dev.arkbuilders.rate.core.domain.model.CurrencyType import javax.inject.Inject class CryptoCurrencyDataSource @Inject constructor( - private val cryptoAPI: CryptoAPI, + private val ratesApiClient: RatesApiClient, private val cryptoRateResponseMapper: CryptoRateResponseMapper, ) : CurrencyDataSource { override val currencyType = CurrencyType.CRYPTO override suspend fun fetchRemote(): Either> { return try { - val response = cryptoAPI.getCryptoRates() + val response = ratesApiClient.getCryptoRates() cryptoRateResponseMapper.map(response).right() } catch (e: Exception) { e.left() diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/FiatCurrencyDataSource.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/FiatCurrencyDataSource.kt index 132874c71..a07b7a355 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/FiatCurrencyDataSource.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/FiatCurrencyDataSource.kt @@ -4,20 +4,20 @@ import arrow.core.Either import arrow.core.left import arrow.core.right import dev.arkbuilders.rate.core.data.mapper.FiatRateResponseMapper -import dev.arkbuilders.rate.core.data.network.api.FiatAPI +import dev.arkbuilders.rate.core.data.network.remote.RatesApiClient import dev.arkbuilders.rate.core.domain.model.CurrencyRate import dev.arkbuilders.rate.core.domain.model.CurrencyType import javax.inject.Inject class FiatCurrencyDataSource @Inject constructor( - private val fiatAPI: FiatAPI, + private val ratesApiClient: RatesApiClient, private val fiatRateResponseMapper: FiatRateResponseMapper, ) : CurrencyDataSource { override val currencyType = CurrencyType.FIAT override suspend fun fetchRemote(): Either> { return try { - val response = fiatAPI.get() + val response = ratesApiClient.getFiatRates() fiatRateResponseMapper.map(response).right() } catch (e: Throwable) { e.left() diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/RatesUpdatedAtDataSource.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/RatesUpdatedAtDataSource.kt index 3af5ba656..b79449388 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/RatesUpdatedAtDataSource.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/RatesUpdatedAtDataSource.kt @@ -4,18 +4,18 @@ import android.content.Context import arrow.core.Either import arrow.core.left import arrow.core.right -import dev.arkbuilders.rate.core.data.network.api.UpdatedAtAPI +import dev.arkbuilders.rate.core.data.network.remote.RatesApiClient import java.time.Instant import java.time.OffsetDateTime import java.time.ZoneOffset class RatesUpdatedAtDataSource( private val ctx: Context, - private val updatedAtAPI: UpdatedAtAPI, + private val ratesApiClient: RatesApiClient, ) { suspend fun fetchRemote(): Either = try { - parseUnixTimestamp(updatedAtAPI.get().string()).right() + parseUnixTimestamp(ratesApiClient.getUpdatedAt()).right() } catch (e: Throwable) { e.left() } diff --git a/core/di/build.gradle.kts b/core/di/build.gradle.kts index 5e3a76de0..5faa93f78 100644 --- a/core/di/build.gradle.kts +++ b/core/di/build.gradle.kts @@ -42,9 +42,7 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.room.runtime) - implementation(libs.retrofit) - implementation(libs.converter.gson) - implementation(libs.logging.interceptor) + implementation(libs.ktor.client.core) implementation(libs.dagger) ksp(libs.dagger.compiler) diff --git a/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/ApiModule.kt b/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/ApiModule.kt index 08d263fc0..26e8ef543 100644 --- a/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/ApiModule.kt +++ b/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/ApiModule.kt @@ -1,64 +1,14 @@ package dev.arkbuilders.rate.core.di.modules -import android.content.Context -import com.google.gson.GsonBuilder import dagger.Module import dagger.Provides -import dev.arkbuilders.rate.core.data.network.OkHttpClientBuilder -import dev.arkbuilders.rate.core.data.network.api.CryptoAPI -import dev.arkbuilders.rate.core.data.network.api.FiatAPI -import dev.arkbuilders.rate.core.data.network.api.UpdatedAtAPI -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory +import dev.arkbuilders.rate.core.data.network.client.KtorHttpClientFactory +import io.ktor.client.HttpClient import javax.inject.Singleton @Module class ApiModule { @Singleton @Provides - fun okHttpClientBuilder(context: Context): OkHttpClientBuilder { - return OkHttpClientBuilder(context) - } - - @Singleton - @Provides - fun cryptoAPI(clientBuilder: OkHttpClientBuilder): CryptoAPI { - val httpClient = clientBuilder.build() - val gson = GsonBuilder().create() - - return Retrofit.Builder() - .baseUrl("https://raw.githubusercontent.com") - .addConverterFactory(GsonConverterFactory.create(gson)) - .client(httpClient) - .build() - .create(CryptoAPI::class.java) - } - - @Singleton - @Provides - fun fiatAPI(clientBuilder: OkHttpClientBuilder): FiatAPI { - val httpClient = clientBuilder.build() - val gson = GsonBuilder().create() - - return Retrofit.Builder() - .baseUrl("https://raw.githubusercontent.com") - .addConverterFactory(GsonConverterFactory.create(gson)) - .client(httpClient) - .build() - .create(FiatAPI::class.java) - } - - @Singleton - @Provides - fun updatedAtAPI(clientBuilder: OkHttpClientBuilder): UpdatedAtAPI { - val httpClient = clientBuilder.build() - val gson = GsonBuilder().create() - - return Retrofit.Builder() - .baseUrl("https://raw.githubusercontent.com") - .addConverterFactory(GsonConverterFactory.create(gson)) - .client(httpClient) - .build() - .create(UpdatedAtAPI::class.java) - } + fun httpClient(factory: KtorHttpClientFactory): HttpClient = factory.create() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 84f6570fc..614c0b289 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,6 @@ composeDestinationsVersion = "2.1.0" arrowFxCoroutines = "1.2.1" arrowCore = "1.2.1" constraintlayoutCompose = "1.1.1" -converterGson = "2.9.0" dagger = "2.50" datastorePreferences = "1.1.4" espressoCore = "3.6.1" @@ -25,12 +24,11 @@ glanceAppwidget = "1.1.1" junit = "4.13.2" junitVersion = "1.2.1" lifecycleRuntimeKtx = "2.8.7" -loggingInterceptor = "4.12.0" +ktor = "3.1.3" material3 = "1.3.2" navigationComposeVersion = "2.8.9" orbitMVI = "9.0.0" qrgenerator = "1.0.1" -retrofit = "2.11.0" roomRuntime = "2.6.1" tagcloud = "1.1.0" timber = "5.0.1" @@ -74,7 +72,6 @@ compose-destinations-compiler = { module = "io.github.raamcosta.compose-destinat arrow-fx-coroutines = { module = "io.arrow-kt:arrow-fx-coroutines", version.ref = "arrowFxCoroutines" } arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrowCore" } constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayoutCompose" } -converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" } dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" } dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" } @@ -89,13 +86,16 @@ firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx", ve firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx", version = "" } junit = { module = "junit:junit", version.ref = "junit" } -logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" } +ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } +ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } +ktor-serialization-gson = { module = "io.ktor:ktor-serialization-gson", version.ref = "ktor" } material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationComposeVersion" } orbit-compose = { module = "org.orbit-mvi:orbit-compose", version.ref = "orbitMVI" } orbit-viewmodel = { module = "org.orbit-mvi:orbit-viewmodel", version.ref = "orbitMVI" } qrgenerator = { module = "com.github.androidmads:QRGenerator", version.ref = "qrgenerator" } -retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } tagcloud = { module = "io.github.oleksandrbalan:tagcloud", version.ref = "tagcloud" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } From 3207d8cb92e624087cbc0946427db5a880f1e228 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Wed, 8 Jul 2026 17:51:10 +0500 Subject: [PATCH 06/12] Fixes --- .../network/client/KtorHttpClientFactory.kt | 11 --- .../rate/core/di/modules/RepoModule.kt | 16 ++-- watchapp/build.gradle.kts | 5 +- .../arkbuilders/rate/watchapp/di/ApiModule.kt | 75 ++----------------- .../rate/watchapp/di/RepoModule.kt | 11 ++- 5 files changed, 20 insertions(+), 98 deletions(-) diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/client/KtorHttpClientFactory.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/client/KtorHttpClientFactory.kt index 024595b43..af2d8474e 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/client/KtorHttpClientFactory.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/client/KtorHttpClientFactory.kt @@ -1,36 +1,25 @@ package dev.arkbuilders.rate.core.data.network.client -import android.content.Context -import android.webkit.WebSettings import dev.arkbuilders.rate.core.domain.BuildConfigFieldsProvider import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.plugins.defaultRequest import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging -import io.ktor.client.request.header -import io.ktor.http.HttpHeaders import io.ktor.serialization.gson.gson import timber.log.Timber import javax.inject.Inject class KtorHttpClientFactory @Inject constructor( - private val context: Context, private val buildConfigFieldsProvider: BuildConfigFieldsProvider, ) { fun create(): HttpClient { - val userAgent = WebSettings.getDefaultUserAgent(context.applicationContext) val buildConfigFields = buildConfigFieldsProvider.provide() return HttpClient(OkHttp) { expectSuccess = true - defaultRequest { - header(HttpHeaders.UserAgent, userAgent) - } - install(ContentNegotiation) { gson() } diff --git a/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/RepoModule.kt b/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/RepoModule.kt index f67c12b5a..3411334cd 100644 --- a/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/RepoModule.kt +++ b/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/RepoModule.kt @@ -6,9 +6,7 @@ import dagger.Provides import dev.arkbuilders.rate.core.data.mapper.CryptoRateResponseMapper import dev.arkbuilders.rate.core.data.mapper.FiatRateResponseMapper import dev.arkbuilders.rate.core.data.network.NetworkStatusImpl -import dev.arkbuilders.rate.core.data.network.api.CryptoAPI -import dev.arkbuilders.rate.core.data.network.api.FiatAPI -import dev.arkbuilders.rate.core.data.network.api.UpdatedAtAPI +import dev.arkbuilders.rate.core.data.network.remote.RatesApiClient import dev.arkbuilders.rate.core.data.preferences.PrefsImpl import dev.arkbuilders.rate.core.data.repo.AnalyticsManagerImpl import dev.arkbuilders.rate.core.data.repo.BuildConfigFieldsProviderImpl @@ -85,9 +83,9 @@ class RepoModule { @Provides fun ratesUpdatedAtDataSource( context: Context, - updatedAtAPI: UpdatedAtAPI, + ratesApiClient: RatesApiClient, ): RatesUpdatedAtDataSource { - return RatesUpdatedAtDataSource(context, updatedAtAPI) + return RatesUpdatedAtDataSource(context, ratesApiClient) } @Singleton @@ -127,16 +125,16 @@ class RepoModule { @Singleton @Provides fun fiatCurrencyDataSource( - fiatAPI: FiatAPI, + ratesApiClient: RatesApiClient, fiatRateResponseMapper: FiatRateResponseMapper, - ): FiatCurrencyDataSource = FiatCurrencyDataSource(fiatAPI, fiatRateResponseMapper) + ): FiatCurrencyDataSource = FiatCurrencyDataSource(ratesApiClient, fiatRateResponseMapper) @Singleton @Provides fun cryptoCurrencyDataSource( - cryptoAPI: CryptoAPI, + ratesApiClient: RatesApiClient, cryptoRateResponseMapper: CryptoRateResponseMapper, - ): CryptoCurrencyDataSource = CryptoCurrencyDataSource(cryptoAPI, cryptoRateResponseMapper) + ): CryptoCurrencyDataSource = CryptoCurrencyDataSource(ratesApiClient, cryptoRateResponseMapper) @Singleton @Provides diff --git a/watchapp/build.gradle.kts b/watchapp/build.gradle.kts index e30f62371..e45721117 100644 --- a/watchapp/build.gradle.kts +++ b/watchapp/build.gradle.kts @@ -118,15 +118,12 @@ dependencies { implementation(project(":core:presentation")) implementation("androidx.hilt:hilt-navigation-compose:1.2.0") - implementation(libs.retrofit) - implementation(libs.converter.gson) - implementation(libs.logging.interceptor) - implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.ktx) ksp(libs.androidx.room.compiler) implementation(libs.hilt.android) ksp(libs.hilt.android.compiler) + implementation(libs.ktor.client.core) implementation(libs.play.services.wearable) // implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.ui) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/ApiModule.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/ApiModule.kt index e2bc83a8e..d98bd4bf7 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/ApiModule.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/ApiModule.kt @@ -1,81 +1,20 @@ package dev.arkbuilders.rate.watchapp.di -import android.content.Context -import com.google.gson.GsonBuilder import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import dev.arkbuilders.rate.core.data.network.api.CryptoAPI -import dev.arkbuilders.rate.core.data.network.api.FiatAPI -import dev.arkbuilders.rate.core.data.network.api.UpdatedAtAPI -import okhttp3.OkHttpClient -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory +import dev.arkbuilders.rate.core.data.network.client.KtorHttpClientFactory +import dev.arkbuilders.rate.core.domain.BuildConfigFieldsProvider +import io.ktor.client.HttpClient import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -class ApiModule { +class ApiModule { @Singleton @Provides - fun clientBuilder( - @ApplicationContext context: Context, - ): OkHttpClient { - val client = - OkHttpClient.Builder() - .addNetworkInterceptor { chain -> - chain.proceed( - chain.request() - .newBuilder() - .build(), - ) - } - .build() - - return client - } - - @Singleton - @Provides - fun cryptoAPI(clientBuilder: OkHttpClient): CryptoAPI { - val httpClient = clientBuilder - val gson = GsonBuilder().create() - - return Retrofit.Builder() - .baseUrl("https://raw.githubusercontent.com") - .addConverterFactory(GsonConverterFactory.create(gson)) - .client(httpClient) - .build() - .create(CryptoAPI::class.java) - } - - @Singleton - @Provides - fun fiatAPI(clientBuilder: OkHttpClient): FiatAPI { - val httpClient = clientBuilder - val gson = GsonBuilder().create() - - return Retrofit.Builder() - .baseUrl("https://raw.githubusercontent.com") - .addConverterFactory(GsonConverterFactory.create(gson)) - .client(httpClient) - .build() - .create(FiatAPI::class.java) - } - - @Singleton - @Provides - fun updatedAtAPI(clientBuilder: OkHttpClient): UpdatedAtAPI { - val httpClient = clientBuilder - val gson = GsonBuilder().create() - - return Retrofit.Builder() - .baseUrl("https://raw.githubusercontent.com") - .addConverterFactory(GsonConverterFactory.create(gson)) - .client(httpClient) - .build() - .create(UpdatedAtAPI::class.java) - } + fun httpClient( + buildConfigFieldsProvider: BuildConfigFieldsProvider, + ): HttpClient = KtorHttpClientFactory(buildConfigFieldsProvider).create() } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/RepoModule.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/RepoModule.kt index 79f1680db..728f930cb 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/RepoModule.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/RepoModule.kt @@ -9,8 +9,7 @@ import dagger.hilt.components.SingletonComponent import dev.arkbuilders.rate.core.data.mapper.CryptoRateResponseMapper import dev.arkbuilders.rate.core.data.mapper.FiatRateResponseMapper import dev.arkbuilders.rate.core.data.network.NetworkStatusImpl -import dev.arkbuilders.rate.core.data.network.api.CryptoAPI -import dev.arkbuilders.rate.core.data.network.api.UpdatedAtAPI +import dev.arkbuilders.rate.core.data.network.remote.RatesApiClient import dev.arkbuilders.rate.core.data.preferences.PrefsImpl import dev.arkbuilders.rate.core.data.repo.AnalyticsManagerImpl import dev.arkbuilders.rate.core.data.repo.BuildConfigFieldsProviderImpl @@ -91,10 +90,10 @@ class RepoModule { @Singleton @Provides fun provideCryptoCurrencyDataSource( - cryptoAPI: CryptoAPI, + ratesApiClient: RatesApiClient, cryptoRateResponseMapper: CryptoRateResponseMapper, ): CryptoCurrencyDataSource { - return CryptoCurrencyDataSource(cryptoAPI, cryptoRateResponseMapper) + return CryptoCurrencyDataSource(ratesApiClient, cryptoRateResponseMapper) } @Singleton @@ -130,9 +129,9 @@ class RepoModule { @Provides fun ratesUpdatedAtDataSource( @ApplicationContext context: Context, - updatedAtAPI: UpdatedAtAPI, + ratesApiClient: RatesApiClient, ): RatesUpdatedAtDataSource { - return RatesUpdatedAtDataSource(context, updatedAtAPI) + return RatesUpdatedAtDataSource(context, ratesApiClient) } @Singleton From dfad5ec130218f849f946bc66adb250a0ee9dd41 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Thu, 9 Jul 2026 15:09:59 +0500 Subject: [PATCH 07/12] Replace Gson with kotlinx serialization --- app/proguard-rules.pro | 34 --------------- core/data/build.gradle.kts | 5 ++- .../dev/arkbuilders/rate/core/data/AppJson.kt | 8 ++++ .../data/mapper/CryptoRateResponseMapper.kt | 2 +- .../network/client/KtorHttpClientFactory.kt | 7 ++-- .../data/network/dto/CryptoRateResponse.kt | 6 ++- .../core/data/network/dto/FiatRateResponse.kt | 2 + .../data/network/remote/RatesApiClient.kt | 41 ++++++++++++------- .../repo/currency/CurrencyInfoDataSource.kt | 10 ++--- .../repo/currency/FallbackRatesProvider.kt | 9 ++-- core/db/build.gradle.kts | 3 +- .../typeconverters/ListAmountTypeConverter.kt | 30 ++++++++++---- gradle/libs.versions.toml | 7 ++-- 13 files changed, 87 insertions(+), 77 deletions(-) create mode 100644 core/data/src/main/java/dev/arkbuilders/rate/core/data/AppJson.kt diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 8ad9fff0c..f1b424510 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -19,37 +19,3 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile - -##---------------Begin: proguard configuration for Gson ---------- -# https://github.com/google/gson/blob/main/examples/android-proguard-example/proguard.cfg -# Gson uses generic type information stored in a class file when working with fields. Proguard -# removes such information by default, so configure it to keep all of it. --keepattributes Signature - -# For using GSON @Expose annotation --keepattributes *Annotation* - -# Gson specific classes --dontwarn sun.misc.** -#-keep class com.google.gson.stream.** { *; } - -# Application classes that will be serialized/deserialized over Gson --keep class com.google.gson.examples.android.model.** { ; } - -# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, -# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) --keep class * extends com.google.gson.TypeAdapter --keep class * implements com.google.gson.TypeAdapterFactory --keep class * implements com.google.gson.JsonSerializer --keep class * implements com.google.gson.JsonDeserializer - -# Prevent R8 from leaving Data object members always null --keepclassmembers,allowobfuscation class * { - @com.google.gson.annotations.SerializedName ; -} - -# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher. --keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken --keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken - -##---------------End: proguard configuration for Gson ---------- diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index a7cbbffe3..b76d56798 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) alias(libs.plugins.ksp) } @@ -59,9 +60,9 @@ dependencies { implementation(libs.ktor.client.core) implementation(libs.ktor.client.okhttp) implementation(libs.ktor.client.content.negotiation) - implementation(libs.ktor.serialization.gson) + implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.ktor.client.logging) - implementation(libs.gson) + implementation(libs.kotlinx.serialization.json) implementation(platform(libs.firebase.bom)) implementation(libs.firebase.analytics) diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/AppJson.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/AppJson.kt new file mode 100644 index 000000000..3b9232569 --- /dev/null +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/AppJson.kt @@ -0,0 +1,8 @@ +package dev.arkbuilders.rate.core.data + +import kotlinx.serialization.json.Json + +internal val appJson = + Json { + ignoreUnknownKeys = true + } diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/mapper/CryptoRateResponseMapper.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/mapper/CryptoRateResponseMapper.kt index 6547cce43..0a3ce9df8 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/mapper/CryptoRateResponseMapper.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/mapper/CryptoRateResponseMapper.kt @@ -12,7 +12,7 @@ class CryptoRateResponseMapper @Inject constructor() { CurrencyRate( CurrencyType.CRYPTO, it.symbol.uppercase(), - BigDecimal.valueOf(it.current_price), + BigDecimal.valueOf(it.currentPrice), ) } } diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/client/KtorHttpClientFactory.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/client/KtorHttpClientFactory.kt index af2d8474e..94adbf079 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/client/KtorHttpClientFactory.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/client/KtorHttpClientFactory.kt @@ -1,5 +1,6 @@ package dev.arkbuilders.rate.core.data.network.client +import dev.arkbuilders.rate.core.data.appJson import dev.arkbuilders.rate.core.domain.BuildConfigFieldsProvider import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp @@ -7,7 +8,7 @@ import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging -import io.ktor.serialization.gson.gson +import io.ktor.serialization.kotlinx.json.json import timber.log.Timber import javax.inject.Inject @@ -21,7 +22,7 @@ class KtorHttpClientFactory @Inject constructor( expectSuccess = true install(ContentNegotiation) { - gson() + json(appJson) } if (buildConfigFields.buildType == DEBUG_BUILD_TYPE) { @@ -32,7 +33,7 @@ class KtorHttpClientFactory @Inject constructor( Timber.d(message) } } - level = LogLevel.ALL + level = LogLevel.INFO } } } diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/dto/CryptoRateResponse.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/dto/CryptoRateResponse.kt index 1cc34c00c..a5fccc7a7 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/dto/CryptoRateResponse.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/dto/CryptoRateResponse.kt @@ -1,9 +1,13 @@ package dev.arkbuilders.rate.core.data.network.dto import androidx.annotation.Keep +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable @Keep +@Serializable data class CryptoRateResponse( val symbol: String, - val current_price: Double, + @SerialName("current_price") + val currentPrice: Double, ) diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/dto/FiatRateResponse.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/dto/FiatRateResponse.kt index b06b289d1..ebecf4d85 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/dto/FiatRateResponse.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/dto/FiatRateResponse.kt @@ -1,8 +1,10 @@ package dev.arkbuilders.rate.core.data.network.dto import androidx.annotation.Keep +import kotlinx.serialization.Serializable @Keep +@Serializable data class FiatRateResponse( val timestamp: Long, val rates: Map, diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/remote/RatesApiClient.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/remote/RatesApiClient.kt index aba8da4c0..4375cd120 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/remote/RatesApiClient.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/remote/RatesApiClient.kt @@ -1,14 +1,15 @@ package dev.arkbuilders.rate.core.data.network.remote +import dev.arkbuilders.rate.core.data.appJson import dev.arkbuilders.rate.core.data.network.client.NetworkConfig import dev.arkbuilders.rate.core.data.network.dto.CryptoRateResponse import dev.arkbuilders.rate.core.data.network.dto.FiatRateResponse import io.ktor.client.HttpClient -import io.ktor.client.call.body import io.ktor.client.request.get import io.ktor.client.statement.bodyAsText import io.ktor.http.URLProtocol import io.ktor.http.encodedPath +import kotlinx.serialization.decodeFromString import javax.inject.Inject import javax.inject.Singleton @@ -16,23 +17,33 @@ import javax.inject.Singleton class RatesApiClient @Inject constructor( private val client: HttpClient, ) { - suspend fun getCryptoRates(): List = - client.get { - url { - protocol = URLProtocol.HTTPS - host = NetworkConfig.HOST - encodedPath = NetworkConfig.CRYPTO_RATES_PATH + suspend fun getCryptoRates(): List { + val response = + client.get { + url { + protocol = URLProtocol.HTTPS + host = NetworkConfig.HOST + encodedPath = NetworkConfig.CRYPTO_RATES_PATH + } } - }.body() + val body = response.bodyAsText() - suspend fun getFiatRates(): FiatRateResponse = - client.get { - url { - protocol = URLProtocol.HTTPS - host = NetworkConfig.HOST - encodedPath = NetworkConfig.FIAT_RATES_PATH + return appJson.decodeFromString(body) + } + + suspend fun getFiatRates(): FiatRateResponse { + val response = + client.get { + url { + protocol = URLProtocol.HTTPS + host = NetworkConfig.HOST + encodedPath = NetworkConfig.FIAT_RATES_PATH + } } - }.body() + val body = response.bodyAsText() + + return appJson.decodeFromString(body) + } suspend fun getUpdatedAt(): String = client.get { diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CurrencyInfoDataSource.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CurrencyInfoDataSource.kt index 9c995d064..133fbff03 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CurrencyInfoDataSource.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CurrencyInfoDataSource.kt @@ -1,15 +1,15 @@ package dev.arkbuilders.rate.core.data.repo.currency import android.content.Context -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken import dev.arkbuilders.rate.core.data.R +import dev.arkbuilders.rate.core.data.appJson import dev.arkbuilders.rate.core.domain.model.CurrencyCode import dev.arkbuilders.rate.core.domain.model.CurrencyInfo import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.withContext +import kotlinx.serialization.decodeFromString import javax.inject.Inject class CurrencyInfoDataSource @Inject constructor( @@ -23,8 +23,7 @@ class CurrencyInfoDataSource @Inject constructor( ctx.resources.openRawResource(R.raw.code_name).bufferedReader().use { it.readText() } - val codeNameType = object : TypeToken>() {}.type - Gson().fromJson(codeNameText, codeNameType) + appJson.decodeFromString>(codeNameText) } val countryDef: Deferred>> = @@ -33,8 +32,7 @@ class CurrencyInfoDataSource @Inject constructor( ctx.resources.openRawResource(R.raw.code_country).bufferedReader().use { it.readText() } - val codeCountryType = object : TypeToken>>() {}.type - Gson().fromJson(codeCountryText, codeCountryType) + appJson.decodeFromString>>(codeCountryText) } val codeToName = nameDef.await() diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/FallbackRatesProvider.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/FallbackRatesProvider.kt index 4ab9eeb67..374baf5b0 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/FallbackRatesProvider.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/FallbackRatesProvider.kt @@ -1,7 +1,7 @@ package dev.arkbuilders.rate.core.data.repo.currency import android.content.Context -import com.google.gson.Gson +import dev.arkbuilders.rate.core.data.appJson import dev.arkbuilders.rate.core.data.mapper.CryptoRateResponseMapper import dev.arkbuilders.rate.core.data.mapper.FiatRateResponseMapper import dev.arkbuilders.rate.core.data.network.dto.CryptoRateResponse @@ -9,6 +9,7 @@ import dev.arkbuilders.rate.core.data.network.dto.FiatRateResponse import dev.arkbuilders.rate.core.domain.model.CurrencyRate import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.serialization.decodeFromString import java.time.OffsetDateTime import javax.inject.Inject @@ -24,15 +25,15 @@ class FallbackRatesProvider @Inject constructor( ctx.assets.open(FIAT_RATES_FILE).bufferedReader().use { it.readText() } - val fiatResp = Gson().fromJson(fiatJson, FiatRateResponse::class.java) + val fiatResp = appJson.decodeFromString(fiatJson) val fiatRates = fiatRateResponseMapper.map(fiatResp) val cryptoJson = ctx.assets.open(CRYPTO_RATES_FILE).bufferedReader().use { it.readText() } - val cryptoResp = Gson().fromJson(cryptoJson, Array::class.java) - val cryptoRates = cryptoRateResponseMapper.map(cryptoResp.toList()) + val cryptoResp = appJson.decodeFromString>(cryptoJson) + val cryptoRates = cryptoRateResponseMapper.map(cryptoResp) val fetchDate = ratesUpdatedAtDataSource.fetchBundled() val rates = cryptoRates + fiatRates diff --git a/core/db/build.gradle.kts b/core/db/build.gradle.kts index 30ef0a54e..9e1a4e919 100644 --- a/core/db/build.gradle.kts +++ b/core/db/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) alias(libs.plugins.ksp) } @@ -45,7 +46,7 @@ dependencies { implementation(project(":core:domain")) implementation(libs.androidx.core.ktx) - implementation(libs.gson) + implementation(libs.kotlinx.serialization.json) implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.ktx) diff --git a/core/db/src/main/java/dev/arkbuilders/rate/core/db/typeconverters/ListAmountTypeConverter.kt b/core/db/src/main/java/dev/arkbuilders/rate/core/db/typeconverters/ListAmountTypeConverter.kt index d2c4adbe0..5d3f3b524 100644 --- a/core/db/src/main/java/dev/arkbuilders/rate/core/db/typeconverters/ListAmountTypeConverter.kt +++ b/core/db/src/main/java/dev/arkbuilders/rate/core/db/typeconverters/ListAmountTypeConverter.kt @@ -1,24 +1,40 @@ package dev.arkbuilders.rate.core.db.typeconverters import androidx.room.TypeConverter -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken import dev.arkbuilders.rate.core.domain.model.Amount import dev.arkbuilders.rate.core.domain.model.AmountStr import dev.arkbuilders.rate.core.domain.model.toAmount import dev.arkbuilders.rate.core.domain.model.toStrAmount +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json object ListAmountTypeConverter { @TypeConverter fun fromListAmount(list: List): String { - val type = object : TypeToken>() {}.type - val listStr = list.map { it.toStrAmount() } - return Gson().toJson(listStr, type) + val listStr = list.map { it.toStrAmount().toSerializableAmountStr() } + return json.encodeToString(listStr) } @TypeConverter fun toListAmount(list: String): List { - val type = object : TypeToken>() {}.type - return Gson().fromJson>(list, type).map { it.toAmount() } + return json.decodeFromString>(list) + .map { it.toAmountStr().toAmount() } } + + private val json = + Json { + ignoreUnknownKeys = true + } + + @Serializable + private data class SerializableAmountStr( + val code: String, + val value: String, + ) + + private fun AmountStr.toSerializableAmountStr() = SerializableAmountStr(code, value) + + private fun SerializableAmountStr.toAmountStr() = AmountStr(code, value) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 614c0b289..d7598855c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,7 +36,7 @@ composeUi = "1.7.5" workRuntimeKtx = "2.8.1" appcompat = "1.7.1" material = "1.12.0" -gson = "2.11.0" +kotlinxSerializationJson = "1.8.1" reorderable = "2.4.3" playReview = "2.0.2" composeBom = "2023.08.00" @@ -74,7 +74,7 @@ arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrowCore" } constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayoutCompose" } dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" } dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" } -gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } @@ -90,7 +90,7 @@ ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negoti ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } -ktor-serialization-gson = { module = "io.ktor:ktor-serialization-gson", version.ref = "ktor" } +ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationComposeVersion" } orbit-compose = { module = "org.orbit-mvi:orbit-compose", version.ref = "orbitMVI" } @@ -119,6 +119,7 @@ androidx-wear-watchface-style = { module = "androidx.wear.watchface:watchface-st [plugins] android-library = { id = "com.android.library", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp"} android-application = { id = "com.android.application", version.ref = "androidApp"} From 7ced63a31481c54b2b0b4ebdfabda3388dc54d3b Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Thu, 9 Jul 2026 15:27:25 +0500 Subject: [PATCH 08/12] Fix duplicate timestamp flow emissions --- .../dev/arkbuilders/rate/core/data/repo/TimestampRepoImpl.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/TimestampRepoImpl.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/TimestampRepoImpl.kt index 7eb7b15bc..c2cd3cb13 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/TimestampRepoImpl.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/TimestampRepoImpl.kt @@ -5,6 +5,7 @@ import dev.arkbuilders.rate.core.db.entity.RoomFetchTimestamp import dev.arkbuilders.rate.core.domain.model.TimestampType import dev.arkbuilders.rate.core.domain.repo.TimestampRepo import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import java.time.OffsetDateTime import javax.inject.Inject @@ -36,5 +37,5 @@ class TimestampRepoImpl @Inject constructor(private val dao: TimestampDao) : Tim override fun timestampFlow(type: TimestampType): Flow = dao.timestampFlow(type.name).map { it?.timestamp - } + }.distinctUntilChanged() } From da97589fcd285675a5955c9585daabc418312dd0 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Thu, 9 Jul 2026 18:51:57 +0500 Subject: [PATCH 09/12] Throttle remote rates updated at checks --- .../core/data/repo/currency/CurrencyRepoImpl.kt | 16 ++++++++++++++++ .../rate/core/domain/model/TimestampType.kt | 1 + 2 files changed, 17 insertions(+) diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CurrencyRepoImpl.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CurrencyRepoImpl.kt index 4e2950604..d4311e86d 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CurrencyRepoImpl.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CurrencyRepoImpl.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import java.time.Duration import java.time.OffsetDateTime import javax.inject.Inject @@ -76,6 +77,11 @@ class CurrencyRepoImpl @Inject constructor( override suspend fun updateRates(): Either> = withContext(Dispatchers.IO) { updateRatesMutex.withLock { + val lastCheckedAt = timestampRepo.getTimestamp(TimestampType.RatesUpdatedAtCheck) + if (shouldCheckRemoteRatesUpdatedAt(lastCheckedAt).not()) { + return@withLock IllegalStateException("Skip rate updates").left() + } + val updatedDate = timestampRepo .getTimestamp(TimestampType.FetchRates) @@ -90,6 +96,8 @@ class CurrencyRepoImpl @Inject constructor( .onLeft { return@withLock it.left() } .getOrNull()!! + timestampRepo.rememberTimestamp(TimestampType.RatesUpdatedAtCheck) + if (isNewer(remoteUpdatedAt, updatedDate).not()) { return@withLock IllegalStateException("Skip rate updates").left() } @@ -126,4 +134,12 @@ class CurrencyRepoImpl @Inject constructor( remoteUpdatedAt: OffsetDateTime, localUpdatedAt: OffsetDateTime?, ) = localUpdatedAt == null || remoteUpdatedAt.isAfter(localUpdatedAt) + + private fun shouldCheckRemoteRatesUpdatedAt(lastCheckedAt: OffsetDateTime?): Boolean = + lastCheckedAt == null || + Duration.between(lastCheckedAt, OffsetDateTime.now()) >= RATES_UPDATED_AT_CHECK_INTERVAL + + private companion object { + val RATES_UPDATED_AT_CHECK_INTERVAL: Duration = Duration.ofMinutes(10) + } } diff --git a/core/domain/src/main/java/dev/arkbuilders/rate/core/domain/model/TimestampType.kt b/core/domain/src/main/java/dev/arkbuilders/rate/core/domain/model/TimestampType.kt index c92a4998d..f17dba706 100644 --- a/core/domain/src/main/java/dev/arkbuilders/rate/core/domain/model/TimestampType.kt +++ b/core/domain/src/main/java/dev/arkbuilders/rate/core/domain/model/TimestampType.kt @@ -2,5 +2,6 @@ package dev.arkbuilders.rate.core.domain.model enum class TimestampType { FetchRates, + RatesUpdatedAtCheck, CheckPairAlerts, } From db0deb30082c410aa26e35eb61c1d447d6ae00d2 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Fri, 10 Jul 2026 04:48:39 +0500 Subject: [PATCH 10/12] Inject BuildConfigFields directly --- .../dev/arkbuilders/rate/presentation/App.kt | 25 ++++++++--------- .../rate/presentation/splash/SplashScreen.kt | 2 +- .../network/client/KtorHttpClientFactory.kt | 6 ++--- .../repo/BuildConfigFieldsProviderImpl.kt | 18 ------------- .../arkbuilders/rate/core/di/CoreComponent.kt | 5 ++-- .../rate/core/di/modules/RepoModule.kt | 11 +++----- .../rate/core/di/modules/UseCaseModule.kt | 5 ++-- .../rate/core/domain/BuildConfigFields.kt | 6 ----- .../usecase/LaunchInAppReviewUseCase.kt | 7 +++-- .../feature/settings/di/SettingsComponent.kt | 4 +-- .../settings/presentation/AboutScreen.kt | 2 +- .../presentation/SettingsViewModel.kt | 5 ++-- .../arkbuilders/rate/watchapp/di/ApiModule.kt | 8 +++--- .../rate/watchapp/di/RepoModule.kt | 18 +++++++++---- .../rate/watchapp/di/UseCaseModule.kt | 9 +++---- .../presentation/RateWatchApplication.kt | 27 +------------------ 16 files changed, 54 insertions(+), 104 deletions(-) delete mode 100644 core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/BuildConfigFieldsProviderImpl.kt diff --git a/app/src/main/java/dev/arkbuilders/rate/presentation/App.kt b/app/src/main/java/dev/arkbuilders/rate/presentation/App.kt index 1c00eaa5f..93c69041f 100644 --- a/app/src/main/java/dev/arkbuilders/rate/presentation/App.kt +++ b/app/src/main/java/dev/arkbuilders/rate/presentation/App.kt @@ -30,8 +30,12 @@ class App : Application(), Configuration.Provider, CoreComponentProvider { override fun onCreate() { super.onCreate() Timber.plant(Timber.DebugTree()) - coreComponent = DaggerCoreComponent.factory().create(this, applicationContext) - initBuildConfigFields() + coreComponent = + DaggerCoreComponent.factory().create( + this, + applicationContext, + buildConfigFields(), + ) instance = this initCrashlytics() @@ -41,17 +45,14 @@ class App : Application(), Configuration.Provider, CoreComponentProvider { ) } - private fun initBuildConfigFields() { - coreComponent.buildConfigFieldsProvider().init( - BuildConfigFields( - buildType = BuildConfig.BUILD_TYPE, - versionCode = BuildConfig.VERSION_CODE, - versionName = BuildConfig.VERSION_NAME, - isGooglePlayBuild = BuildConfig.GOOGLE_PLAY_BUILD, - availableIconCodes = BuildConfig.ICON_CODES.toSet(), - ), + private fun buildConfigFields() = + BuildConfigFields( + buildType = BuildConfig.BUILD_TYPE, + versionCode = BuildConfig.VERSION_CODE, + versionName = BuildConfig.VERSION_NAME, + isGooglePlayBuild = BuildConfig.GOOGLE_PLAY_BUILD, + availableIconCodes = BuildConfig.ICON_CODES.toSet(), ) - } private fun initCrashlytics() = CoroutineScope(Dispatchers.IO).launch { diff --git a/app/src/main/java/dev/arkbuilders/rate/presentation/splash/SplashScreen.kt b/app/src/main/java/dev/arkbuilders/rate/presentation/splash/SplashScreen.kt index 54c0a7c2c..a1421b439 100644 --- a/app/src/main/java/dev/arkbuilders/rate/presentation/splash/SplashScreen.kt +++ b/app/src/main/java/dev/arkbuilders/rate/presentation/splash/SplashScreen.kt @@ -40,7 +40,7 @@ fun SplashScreen(navigator: DestinationsNavigator) { quickComponent.quickRepo(), portfolioComponent.assetsRepo(), coreComponent.prefs(), - coreComponent.buildConfigFieldsProvider().provide(), + coreComponent.buildConfigFields(), coreComponent.currencyRepo(), ), ) diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/client/KtorHttpClientFactory.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/client/KtorHttpClientFactory.kt index 94adbf079..c7c898064 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/client/KtorHttpClientFactory.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/client/KtorHttpClientFactory.kt @@ -1,7 +1,7 @@ package dev.arkbuilders.rate.core.data.network.client import dev.arkbuilders.rate.core.data.appJson -import dev.arkbuilders.rate.core.domain.BuildConfigFieldsProvider +import dev.arkbuilders.rate.core.domain.BuildConfigFields import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.plugins.contentnegotiation.ContentNegotiation @@ -13,11 +13,9 @@ import timber.log.Timber import javax.inject.Inject class KtorHttpClientFactory @Inject constructor( - private val buildConfigFieldsProvider: BuildConfigFieldsProvider, + private val buildConfigFields: BuildConfigFields, ) { fun create(): HttpClient { - val buildConfigFields = buildConfigFieldsProvider.provide() - return HttpClient(OkHttp) { expectSuccess = true diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/BuildConfigFieldsProviderImpl.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/BuildConfigFieldsProviderImpl.kt deleted file mode 100644 index d35122bf1..000000000 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/BuildConfigFieldsProviderImpl.kt +++ /dev/null @@ -1,18 +0,0 @@ -package dev.arkbuilders.rate.core.data.repo - -import dev.arkbuilders.rate.core.domain.BuildConfigFields -import dev.arkbuilders.rate.core.domain.BuildConfigFieldsProvider - -class BuildConfigFieldsProviderImpl : BuildConfigFieldsProvider { - private var fields: BuildConfigFields? = null - - @Synchronized - override fun init(fields: BuildConfigFields) { - this.fields = fields - } - - @Synchronized - override fun provide(): BuildConfigFields { - return fields ?: error("BuildConfigFields not initialized") - } -} diff --git a/core/di/src/main/java/dev/arkbuilders/rate/core/di/CoreComponent.kt b/core/di/src/main/java/dev/arkbuilders/rate/core/di/CoreComponent.kt index e382bd9ba..974ea0052 100644 --- a/core/di/src/main/java/dev/arkbuilders/rate/core/di/CoreComponent.kt +++ b/core/di/src/main/java/dev/arkbuilders/rate/core/di/CoreComponent.kt @@ -7,7 +7,7 @@ import dagger.Component import dev.arkbuilders.rate.core.db.dao.PairAlertDao import dev.arkbuilders.rate.core.db.dao.PortfolioDao import dev.arkbuilders.rate.core.db.dao.QuickCalculationDao -import dev.arkbuilders.rate.core.domain.BuildConfigFieldsProvider +import dev.arkbuilders.rate.core.domain.BuildConfigFields import dev.arkbuilders.rate.core.domain.repo.AnalyticsManager import dev.arkbuilders.rate.core.domain.repo.CodeUseStatRepo import dev.arkbuilders.rate.core.domain.repo.CurrencyRepo @@ -44,7 +44,7 @@ interface CoreComponent { fun appContext(): Context - fun buildConfigFieldsProvider(): BuildConfigFieldsProvider + fun buildConfigFields(): BuildConfigFields fun currencyRepo(): CurrencyRepo @@ -77,6 +77,7 @@ interface CoreComponent { fun create( @BindsInstance application: Application, @BindsInstance context: Context, + @BindsInstance buildConfigFields: BuildConfigFields, ): CoreComponent } } diff --git a/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/RepoModule.kt b/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/RepoModule.kt index 3411334cd..5995086d5 100644 --- a/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/RepoModule.kt +++ b/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/RepoModule.kt @@ -9,7 +9,6 @@ import dev.arkbuilders.rate.core.data.network.NetworkStatusImpl import dev.arkbuilders.rate.core.data.network.remote.RatesApiClient import dev.arkbuilders.rate.core.data.preferences.PrefsImpl import dev.arkbuilders.rate.core.data.repo.AnalyticsManagerImpl -import dev.arkbuilders.rate.core.data.repo.BuildConfigFieldsProviderImpl import dev.arkbuilders.rate.core.data.repo.CodeUseStatRepoImpl import dev.arkbuilders.rate.core.data.repo.GooglePlayInAppReviewManagerImpl import dev.arkbuilders.rate.core.data.repo.GroupRepoImpl @@ -25,7 +24,7 @@ import dev.arkbuilders.rate.core.db.dao.CodeUseStatDao import dev.arkbuilders.rate.core.db.dao.CurrencyRateDao import dev.arkbuilders.rate.core.db.dao.GroupDao import dev.arkbuilders.rate.core.db.dao.TimestampDao -import dev.arkbuilders.rate.core.domain.BuildConfigFieldsProvider +import dev.arkbuilders.rate.core.domain.BuildConfigFields import dev.arkbuilders.rate.core.domain.repo.AnalyticsManager import dev.arkbuilders.rate.core.domain.repo.CodeUseStatRepo import dev.arkbuilders.rate.core.domain.repo.CurrencyRepo @@ -113,10 +112,6 @@ class RepoModule { @Provides fun networkStatus(context: Context): NetworkStatus = NetworkStatusImpl(context) - @Singleton - @Provides - fun buildConfigFieldsProvider(): BuildConfigFieldsProvider = BuildConfigFieldsProviderImpl() - @Singleton @Provides fun defaultGroupNameProvider(context: Context): DefaultGroupNameProvider = @@ -150,10 +145,10 @@ class RepoModule { @Provides fun inAppReviewManager( analyticsManager: AnalyticsManager, - buildConfigFieldsProvider: BuildConfigFieldsProvider, + buildConfigFields: BuildConfigFields, ): InAppReviewManager = GooglePlayInAppReviewManagerImpl( analyticsManager, - buildConfigFieldsProvider.provide(), + buildConfigFields, ) } diff --git a/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/UseCaseModule.kt b/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/UseCaseModule.kt index 0a7be8449..c550c7ae7 100644 --- a/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/UseCaseModule.kt +++ b/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/UseCaseModule.kt @@ -2,7 +2,7 @@ package dev.arkbuilders.rate.core.di.modules import dagger.Module import dagger.Provides -import dev.arkbuilders.rate.core.domain.BuildConfigFieldsProvider +import dev.arkbuilders.rate.core.domain.BuildConfigFields import dev.arkbuilders.rate.core.domain.repo.CodeUseStatRepo import dev.arkbuilders.rate.core.domain.repo.CurrencyRepo import dev.arkbuilders.rate.core.domain.repo.GroupRepo @@ -43,6 +43,5 @@ class UseCaseModule { @Singleton @Provides - fun searchUseCase(buildConfigFieldsProvider: BuildConfigFieldsProvider) = - SearchUseCase(buildConfigFieldsProvider.provide()) + fun searchUseCase(buildConfigFields: BuildConfigFields) = SearchUseCase(buildConfigFields) } diff --git a/core/domain/src/main/java/dev/arkbuilders/rate/core/domain/BuildConfigFields.kt b/core/domain/src/main/java/dev/arkbuilders/rate/core/domain/BuildConfigFields.kt index 116bf634b..afdc3009c 100644 --- a/core/domain/src/main/java/dev/arkbuilders/rate/core/domain/BuildConfigFields.kt +++ b/core/domain/src/main/java/dev/arkbuilders/rate/core/domain/BuildConfigFields.kt @@ -9,9 +9,3 @@ data class BuildConfigFields( val isGooglePlayBuild: Boolean, val availableIconCodes: Set, ) - -interface BuildConfigFieldsProvider { - fun init(fields: BuildConfigFields) - - fun provide(): BuildConfigFields -} diff --git a/feature/quick/src/main/java/dev/arkbuilders/rate/feature/quick/domain/usecase/LaunchInAppReviewUseCase.kt b/feature/quick/src/main/java/dev/arkbuilders/rate/feature/quick/domain/usecase/LaunchInAppReviewUseCase.kt index cea92e1dd..a2fad9bc4 100644 --- a/feature/quick/src/main/java/dev/arkbuilders/rate/feature/quick/domain/usecase/LaunchInAppReviewUseCase.kt +++ b/feature/quick/src/main/java/dev/arkbuilders/rate/feature/quick/domain/usecase/LaunchInAppReviewUseCase.kt @@ -1,7 +1,7 @@ package dev.arkbuilders.rate.feature.quick.domain.usecase import android.app.Activity -import dev.arkbuilders.rate.core.domain.BuildConfigFieldsProvider +import dev.arkbuilders.rate.core.domain.BuildConfigFields import dev.arkbuilders.rate.core.domain.repo.InAppReviewManager import dev.arkbuilders.rate.core.domain.repo.PreferenceKey import dev.arkbuilders.rate.core.domain.repo.Prefs @@ -13,7 +13,7 @@ import javax.inject.Inject class LaunchInAppReviewUseCase @Inject constructor( private val inAppReviewManager: InAppReviewManager, private val prefs: Prefs, - private val buildConfigFieldsProvider: BuildConfigFieldsProvider, + private val buildConfigFields: BuildConfigFields, ) { companion object { private const val MIN_APP_LAUNCHES_FOR_REVIEW = 5 @@ -22,8 +22,7 @@ class LaunchInAppReviewUseCase @Inject constructor( } suspend operator fun invoke(activity: Activity) { - val buildConfig = buildConfigFieldsProvider.provide() - if (buildConfig.isGooglePlayBuild.not()) { + if (buildConfigFields.isGooglePlayBuild.not()) { Timber.d("In-app review skipped: not Google Play build") return } diff --git a/feature/settings/src/main/java/dev/arkbuilders/rate/feature/settings/di/SettingsComponent.kt b/feature/settings/src/main/java/dev/arkbuilders/rate/feature/settings/di/SettingsComponent.kt index 75a5f2536..b906971b3 100644 --- a/feature/settings/src/main/java/dev/arkbuilders/rate/feature/settings/di/SettingsComponent.kt +++ b/feature/settings/src/main/java/dev/arkbuilders/rate/feature/settings/di/SettingsComponent.kt @@ -2,7 +2,7 @@ package dev.arkbuilders.rate.feature.settings.di import dagger.Component import dev.arkbuilders.rate.core.di.CoreComponent -import dev.arkbuilders.rate.core.domain.BuildConfigFieldsProvider +import dev.arkbuilders.rate.core.domain.BuildConfigFields import dev.arkbuilders.rate.core.domain.repo.TimestampRepo import dev.arkbuilders.rate.feature.settings.presentation.SettingsViewModelFactory @@ -11,7 +11,7 @@ import dev.arkbuilders.rate.feature.settings.presentation.SettingsViewModelFacto interface SettingsComponent { fun settingsVMFactory(): SettingsViewModelFactory - fun buildConfigFieldsProvider(): BuildConfigFieldsProvider + fun buildConfigFields(): BuildConfigFields fun timestampRepo(): TimestampRepo } diff --git a/feature/settings/src/main/java/dev/arkbuilders/rate/feature/settings/presentation/AboutScreen.kt b/feature/settings/src/main/java/dev/arkbuilders/rate/feature/settings/presentation/AboutScreen.kt index e814834d6..376813659 100644 --- a/feature/settings/src/main/java/dev/arkbuilders/rate/feature/settings/presentation/AboutScreen.kt +++ b/feature/settings/src/main/java/dev/arkbuilders/rate/feature/settings/presentation/AboutScreen.kt @@ -32,7 +32,7 @@ fun AboutScreen(navigator: DestinationsNavigator) { modifier = Modifier.padding(it), appName = stringResource(id = CoreRString.app_name), appLogoResId = CoreRDrawable.ic_app_logo, - versionName = component.buildConfigFieldsProvider().provide().versionName, + versionName = component.buildConfigFields().versionName, privacyPolicyUrl = stringResource(CoreRString.privacy_policy_url), ) } diff --git a/feature/settings/src/main/java/dev/arkbuilders/rate/feature/settings/presentation/SettingsViewModel.kt b/feature/settings/src/main/java/dev/arkbuilders/rate/feature/settings/presentation/SettingsViewModel.kt index b2de2aff0..4d103cbac 100644 --- a/feature/settings/src/main/java/dev/arkbuilders/rate/feature/settings/presentation/SettingsViewModel.kt +++ b/feature/settings/src/main/java/dev/arkbuilders/rate/feature/settings/presentation/SettingsViewModel.kt @@ -7,7 +7,6 @@ import com.google.firebase.analytics.ktx.analytics import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.ktx.Firebase import dev.arkbuilders.rate.core.domain.BuildConfigFields -import dev.arkbuilders.rate.core.domain.BuildConfigFieldsProvider import dev.arkbuilders.rate.core.domain.model.TimestampType import dev.arkbuilders.rate.core.domain.repo.AnalyticsManager import dev.arkbuilders.rate.core.domain.repo.PreferenceKey @@ -146,7 +145,7 @@ class SettingsViewModelFactory @Inject constructor( private val prefs: Prefs, private val timestampRepo: TimestampRepo, private val analyticsManager: AnalyticsManager, - private val buildConfigFieldsProvider: BuildConfigFieldsProvider, + private val buildConfigFields: BuildConfigFields, private val languageRepo: AppLanguageRepo, ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { @@ -154,7 +153,7 @@ class SettingsViewModelFactory @Inject constructor( prefs, timestampRepo, analyticsManager, - buildConfigFieldsProvider.provide(), + buildConfigFields, languageRepo, ) as T } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/ApiModule.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/ApiModule.kt index d98bd4bf7..17fb494cc 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/ApiModule.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/ApiModule.kt @@ -5,16 +5,16 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dev.arkbuilders.rate.core.data.network.client.KtorHttpClientFactory -import dev.arkbuilders.rate.core.domain.BuildConfigFieldsProvider +import dev.arkbuilders.rate.core.domain.BuildConfigFields import io.ktor.client.HttpClient import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -class ApiModule { +class ApiModule { @Singleton @Provides fun httpClient( - buildConfigFieldsProvider: BuildConfigFieldsProvider, - ): HttpClient = KtorHttpClientFactory(buildConfigFieldsProvider).create() + buildConfigFields: BuildConfigFields, + ): HttpClient = KtorHttpClientFactory(buildConfigFields).create() } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/RepoModule.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/RepoModule.kt index 728f930cb..2139eb89b 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/RepoModule.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/RepoModule.kt @@ -12,7 +12,6 @@ import dev.arkbuilders.rate.core.data.network.NetworkStatusImpl import dev.arkbuilders.rate.core.data.network.remote.RatesApiClient import dev.arkbuilders.rate.core.data.preferences.PrefsImpl import dev.arkbuilders.rate.core.data.repo.AnalyticsManagerImpl -import dev.arkbuilders.rate.core.data.repo.BuildConfigFieldsProviderImpl import dev.arkbuilders.rate.core.data.repo.CodeUseStatRepoImpl import dev.arkbuilders.rate.core.data.repo.GooglePlayInAppReviewManagerImpl import dev.arkbuilders.rate.core.data.repo.GroupRepoImpl @@ -29,7 +28,7 @@ import dev.arkbuilders.rate.core.db.dao.CurrencyRateDao import dev.arkbuilders.rate.core.db.dao.GroupDao import dev.arkbuilders.rate.core.db.dao.QuickCalculationDao import dev.arkbuilders.rate.core.db.dao.TimestampDao -import dev.arkbuilders.rate.core.domain.BuildConfigFieldsProvider +import dev.arkbuilders.rate.core.domain.BuildConfigFields import dev.arkbuilders.rate.core.domain.repo.AnalyticsManager import dev.arkbuilders.rate.core.domain.repo.CodeUseStatRepo import dev.arkbuilders.rate.core.domain.repo.CurrencyRepo @@ -42,6 +41,7 @@ import dev.arkbuilders.rate.core.domain.usecase.DefaultGroupNameProvider import dev.arkbuilders.rate.core.presentation.utils.DefaultGroupNameProviderImpl import dev.arkbuilders.rate.feature.quick.data.QuickRepoImpl import dev.arkbuilders.rate.feature.quick.domain.repo.QuickRepo +import dev.arkbuilders.rate.watchapp.BuildConfig import javax.inject.Singleton @Module @@ -49,7 +49,15 @@ import javax.inject.Singleton class RepoModule { @Singleton @Provides - fun buildConfigFieldsProvider(): BuildConfigFieldsProvider = BuildConfigFieldsProviderImpl() + fun buildConfigFields(): BuildConfigFields = + BuildConfigFields( + buildType = BuildConfig.BUILD_TYPE, + versionCode = BuildConfig.VERSION_CODE, + versionName = BuildConfig.VERSION_NAME, + // Default to false for watch app for now + isGooglePlayBuild = false, + availableIconCodes = BuildConfig.ICON_CODES.toSet(), + ) @Singleton @Provides @@ -173,11 +181,11 @@ class RepoModule { @Provides fun inAppReviewManager( analyticsManager: AnalyticsManager, - buildConfigFieldsProvider: BuildConfigFieldsProvider, + buildConfigFields: BuildConfigFields, ): InAppReviewManager = GooglePlayInAppReviewManagerImpl( analyticsManager, - buildConfigFieldsProvider.provide(), + buildConfigFields, ) @Singleton diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/UseCaseModule.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/UseCaseModule.kt index 656226960..8617bf4c0 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/UseCaseModule.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/UseCaseModule.kt @@ -4,7 +4,7 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import dev.arkbuilders.rate.core.domain.BuildConfigFieldsProvider +import dev.arkbuilders.rate.core.domain.BuildConfigFields import dev.arkbuilders.rate.core.domain.repo.CodeUseStatRepo import dev.arkbuilders.rate.core.domain.repo.CurrencyRepo import dev.arkbuilders.rate.core.domain.repo.GroupRepo @@ -49,14 +49,13 @@ class UseCaseModule { @Singleton @Provides - fun searchUseCase(buildConfigFieldsProvider: BuildConfigFieldsProvider) = - SearchUseCase(buildConfigFieldsProvider.provide()) + fun searchUseCase(buildConfigFields: BuildConfigFields) = SearchUseCase(buildConfigFields) @Singleton @Provides fun provideLaunchInAppReviewUseCase( inAppReviewManager: InAppReviewManager, prefs: Prefs, - buildConfigFieldsProvider: BuildConfigFieldsProvider, - ) = LaunchInAppReviewUseCase(inAppReviewManager, prefs, buildConfigFieldsProvider) + buildConfigFields: BuildConfigFields, + ) = LaunchInAppReviewUseCase(inAppReviewManager, prefs, buildConfigFields) } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/RateWatchApplication.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/RateWatchApplication.kt index 2f03f1675..2d6897945 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/RateWatchApplication.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/RateWatchApplication.kt @@ -2,31 +2,6 @@ package dev.arkbuilders.rate.watchapp.presentation import android.app.Application import dagger.hilt.android.HiltAndroidApp -import dev.arkbuilders.rate.core.domain.BuildConfigFields -import dev.arkbuilders.rate.core.domain.BuildConfigFieldsProvider -import dev.arkbuilders.rate.watchapp.BuildConfig -import javax.inject.Inject @HiltAndroidApp -class RateWatchApplication : Application() { - @Inject - lateinit var buildConfigFieldsProvider: BuildConfigFieldsProvider - - override fun onCreate() { - super.onCreate() - initBuildConfigFields() - } - - private fun initBuildConfigFields() { - buildConfigFieldsProvider.init( - BuildConfigFields( - buildType = BuildConfig.BUILD_TYPE, - versionCode = BuildConfig.VERSION_CODE, - versionName = BuildConfig.VERSION_NAME, - // Default to false for watch app for now - isGooglePlayBuild = false, - availableIconCodes = BuildConfig.ICON_CODES.toSet(), - ), - ) - } -} +class RateWatchApplication : Application() From 640aa0d170adf3064f9c82ae8f91ea9d511af4d0 Mon Sep 17 00:00:00 2001 From: mdrlzy Date: Fri, 10 Jul 2026 08:34:50 +0500 Subject: [PATCH 11/12] Update Kotlin, Gradle and dependencies --- app/build.gradle.kts | 15 +++- .../dev/arkbuilders/rate/presentation/App.kt | 6 +- build.gradle.kts | 6 +- core/data/build.gradle.kts | 11 ++- .../core/data/repo/AnalyticsManagerImpl.kt | 9 +-- core/db/build.gradle.kts | 11 ++- .../rate/core/db/migration/Migration_14_15.kt | 5 +- core/di/build.gradle.kts | 11 ++- .../rate/core/di/modules/RepoModule.kt | 2 +- core/domain/build.gradle.kts | 11 ++- core/presentation/build.gradle.kts | 11 ++- cryptoicons/build.gradle.kts | 21 +++-- feature/onboarding/build.gradle.kts | 12 ++- feature/pairalert/build.gradle.kts | 12 ++- .../presentation/add/EditCondition.kt | 6 +- feature/portfolio/build.gradle.kts | 12 ++- .../presentation/main/PortfolioScreen.kt | 6 +- feature/quick/build.gradle.kts | 12 ++- feature/quickwidget/build.gradle.kts | 11 ++- feature/search/build.gradle.kts | 11 ++- feature/settings/build.gradle.kts | 11 ++- .../settings/presentation/SettingsScreen.kt | 4 +- .../presentation/SettingsViewModel.kt | 6 +- fiaticons/build.gradle.kts | 21 +++-- gradle/libs.versions.toml | 81 ++++++++++--------- gradle/wrapper/gradle-wrapper.properties | 2 +- watchapp/build.gradle.kts | 19 +++-- .../arkbuilders/rate/watchapp/di/ApiModule.kt | 5 +- .../rate/watchapp/di/RepoModule.kt | 2 +- .../presentation/options/OptionsScreen.kt | 12 +-- 30 files changed, 227 insertions(+), 137 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 676645ec0..1920d1921 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -9,6 +9,14 @@ plugins { alias(libs.plugins.compose.compiler) } +kotlin { + compilerOptions { + jvmTarget.set( + org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21, + ) + } +} + android { namespace = "dev.arkbuilders.rate" compileSdk = libs.versions.compileSdk.get().toInt() @@ -102,9 +110,6 @@ android { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } - kotlinOptions { - jvmTarget = "21" - } buildFeatures { buildConfig = true @@ -142,6 +147,7 @@ dependencies { implementation(libs.androidx.appcompat) implementation(libs.androidx.glance.appwidget) + implementation(libs.androidx.concurrent.futures.ktx) implementation(libs.dagger) ksp(libs.dagger.compiler) @@ -186,7 +192,8 @@ tasks.getByPath(":app:preBuild").dependsOn("ktlintFormat") fun collectCurrencyIcons(moduleDir: File): List { val drawableDir = moduleDir.resolve("src/main/res/drawable") - return drawableDir.listFiles()!! + return drawableDir + .listFiles()!! .map { it.nameWithoutExtension.uppercase() } .map { when { diff --git a/app/src/main/java/dev/arkbuilders/rate/presentation/App.kt b/app/src/main/java/dev/arkbuilders/rate/presentation/App.kt index 93c69041f..93b0e4a96 100644 --- a/app/src/main/java/dev/arkbuilders/rate/presentation/App.kt +++ b/app/src/main/java/dev/arkbuilders/rate/presentation/App.kt @@ -8,8 +8,8 @@ import androidx.work.ListenableWorker import androidx.work.NetworkType import androidx.work.PeriodicWorkRequest import androidx.work.WorkManager -import com.google.firebase.crashlytics.ktx.crashlytics -import com.google.firebase.ktx.Firebase +import com.google.firebase.Firebase +import com.google.firebase.crashlytics.crashlytics import dev.arkbuilders.rate.BuildConfig import dev.arkbuilders.rate.core.di.CoreComponent import dev.arkbuilders.rate.core.di.CoreComponentProvider @@ -91,7 +91,7 @@ class App : Application(), Configuration.Provider, CoreComponentProvider { ) } - override fun getWorkManagerConfiguration() = + override val workManagerConfiguration: Configuration = Configuration.Builder() .setMinimumLoggingLevel(android.util.Log.INFO) .setWorkerFactory(AppWorkerFactory()) diff --git a/build.gradle.kts b/build.gradle.kts index 507df665f..d1ef5e758 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,10 +5,10 @@ plugins { alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.ksp) - id("com.google.gms.google-services") version "4.4.2" apply false - id("com.google.firebase.crashlytics") version "3.0.1" apply false + id("com.google.gms.google-services") version "4.5.0" apply false + id("com.google.firebase.crashlytics") version "3.0.7" apply false id("org.jlleitschuh.gradle.ktlint") version "12.1.1" - id("com.google.dagger.hilt.android") version "2.56.2" apply false + id("com.google.dagger.hilt.android") version "2.58" apply false } allprojects { diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index b76d56798..da3539526 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -5,6 +5,14 @@ plugins { alias(libs.plugins.ksp) } +kotlin { + compilerOptions { + jvmTarget.set( + org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21, + ) + } +} + android { namespace = "dev.arkbuilders.rate.core.data" compileSdk = libs.versions.compileSdk.get().toInt() @@ -29,9 +37,6 @@ android { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } - kotlinOptions { - jvmTarget = "21" - } } dependencies { diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/AnalyticsManagerImpl.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/AnalyticsManagerImpl.kt index c6b65e556..350ef0dbf 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/AnalyticsManagerImpl.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/AnalyticsManagerImpl.kt @@ -1,15 +1,12 @@ package dev.arkbuilders.rate.core.data.repo +import com.google.firebase.Firebase import com.google.firebase.analytics.FirebaseAnalytics -import com.google.firebase.analytics.ktx.analytics +import com.google.firebase.analytics.analytics import com.google.firebase.analytics.logEvent -import com.google.firebase.ktx.Firebase import dev.arkbuilders.rate.core.domain.repo.AnalyticsManager -import dev.arkbuilders.rate.core.domain.repo.Prefs -class AnalyticsManagerImpl( - private val prefs: Prefs, -) : AnalyticsManager { +class AnalyticsManagerImpl : AnalyticsManager { override fun trackScreen(name: String) { Firebase.analytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW) { param(FirebaseAnalytics.Param.SCREEN_NAME, name) diff --git a/core/db/build.gradle.kts b/core/db/build.gradle.kts index 9e1a4e919..207bbcd9b 100644 --- a/core/db/build.gradle.kts +++ b/core/db/build.gradle.kts @@ -5,6 +5,14 @@ plugins { alias(libs.plugins.ksp) } +kotlin { + compilerOptions { + jvmTarget.set( + org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21, + ) + } +} + android { namespace = "dev.arkbuilders.rate.core.db" compileSdk = libs.versions.compileSdk.get().toInt() @@ -33,9 +41,6 @@ android { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } - kotlinOptions { - jvmTarget = "21" - } sourceSets { // Adds exported schema location as test app assets. getByName("androidTest").assets.srcDir("$projectDir/schemas") diff --git a/core/db/src/main/java/dev/arkbuilders/rate/core/db/migration/Migration_14_15.kt b/core/db/src/main/java/dev/arkbuilders/rate/core/db/migration/Migration_14_15.kt index 40893d484..3b219a813 100644 --- a/core/db/src/main/java/dev/arkbuilders/rate/core/db/migration/Migration_14_15.kt +++ b/core/db/src/main/java/dev/arkbuilders/rate/core/db/migration/Migration_14_15.kt @@ -60,10 +60,10 @@ private fun extractGroupsAndInsertIntoGroupTable(db: SupportSQLiteDatabase) { fun insertGroups(groups: Set, featureType: GroupFeatureType) { var orderIndex = 0 val creationTime = OffsetDateTimeTypeConverter.fromOffsetDateTime(OffsetDateTime.now()) - groups.forEach { group-> + groups.forEach { group -> db.execSQL( "INSERT INTO RoomGroup (name, orderIndex, creationTime, featureType) VALUES (?, ?, ?, ?)", - arrayOf(group, orderIndex++, creationTime, featureType.name) + arrayOf(group, orderIndex++, creationTime, featureType.name), ) } } @@ -171,4 +171,3 @@ private fun migrateDataAndCleanup(db: SupportSQLiteDatabase) { db.execSQL("ALTER TABLE RoomQuickPair_new RENAME TO RoomQuickPair") db.execSQL("ALTER TABLE RoomPairAlert_new RENAME TO RoomPairAlert") } - diff --git a/core/di/build.gradle.kts b/core/di/build.gradle.kts index 5faa93f78..a2732de95 100644 --- a/core/di/build.gradle.kts +++ b/core/di/build.gradle.kts @@ -4,6 +4,14 @@ plugins { alias(libs.plugins.ksp) } +kotlin { + compilerOptions { + jvmTarget.set( + org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21, + ) + } +} + android { namespace = "dev.arkbuilders.rate.core.di" compileSdk = libs.versions.compileSdk.get().toInt() @@ -28,9 +36,6 @@ android { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } - kotlinOptions { - jvmTarget = "21" - } } dependencies { diff --git a/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/RepoModule.kt b/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/RepoModule.kt index 5995086d5..0eea24737 100644 --- a/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/RepoModule.kt +++ b/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/RepoModule.kt @@ -102,7 +102,7 @@ class RepoModule { @Singleton @Provides - fun analyticsManager(prefs: Prefs): AnalyticsManager = AnalyticsManagerImpl(prefs) + fun analyticsManager(): AnalyticsManager = AnalyticsManagerImpl() @Singleton @Provides diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts index f1e1faffc..f6004772f 100644 --- a/core/domain/build.gradle.kts +++ b/core/domain/build.gradle.kts @@ -4,6 +4,14 @@ plugins { id("kotlin-parcelize") } +kotlin { + compilerOptions { + jvmTarget.set( + org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21, + ) + } +} + android { namespace = "dev.arkbuilders.rate.core.domain" compileSdk = libs.versions.compileSdk.get().toInt() @@ -28,9 +36,6 @@ android { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } - kotlinOptions { - jvmTarget = "21" - } } dependencies { diff --git a/core/presentation/build.gradle.kts b/core/presentation/build.gradle.kts index 894cabb42..e0d5c35c6 100644 --- a/core/presentation/build.gradle.kts +++ b/core/presentation/build.gradle.kts @@ -4,6 +4,14 @@ plugins { alias(libs.plugins.compose.compiler) } +kotlin { + compilerOptions { + jvmTarget.set( + org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21, + ) + } +} + android { namespace = "dev.arkbuilders.rate.core.presentation" compileSdk = libs.versions.compileSdk.get().toInt() @@ -28,9 +36,6 @@ android { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } - kotlinOptions { - jvmTarget = "21" - } } dependencies { diff --git a/cryptoicons/build.gradle.kts b/cryptoicons/build.gradle.kts index a058f53e0..c5dd0dba7 100644 --- a/cryptoicons/build.gradle.kts +++ b/cryptoicons/build.gradle.kts @@ -3,12 +3,20 @@ plugins { id("org.jetbrains.kotlin.android") } +kotlin { + compilerOptions { + jvmTarget.set( + org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21, + ) + } +} + android { namespace = "dev.arkbuilders.rate.cryptoicons" - compileSdk = 35 + compileSdk = libs.versions.compileSdk.get().toInt() defaultConfig { - minSdk = 26 + minSdk = libs.versions.minSdk.get().toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") @@ -27,16 +35,13 @@ android { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } - kotlinOptions { - jvmTarget = "21" - } } dependencies { - implementation("androidx.core:core-ktx:1.13.1") - implementation("androidx.appcompat:appcompat:1.7.0") - implementation("com.google.android.material:material:1.12.0") + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") diff --git a/feature/onboarding/build.gradle.kts b/feature/onboarding/build.gradle.kts index a3374b060..d09c0af20 100644 --- a/feature/onboarding/build.gradle.kts +++ b/feature/onboarding/build.gradle.kts @@ -5,6 +5,14 @@ plugins { alias(libs.plugins.compose.compiler) } +kotlin { + compilerOptions { + jvmTarget.set( + org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17, + ) + } +} + android { namespace = "dev.arkbuilders.rate.feature.onboarding" compileSdk = libs.versions.compileSdk.get().toInt() @@ -32,9 +40,6 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = "17" - } } dependencies { @@ -47,6 +52,7 @@ dependencies { implementation(libs.androidx.ui) implementation(libs.navigation.compose) implementation(libs.material3) + implementation(libs.material.icons.extended) implementation(libs.androidx.ui.tooling) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.lifecycle.runtime.ktx) diff --git a/feature/pairalert/build.gradle.kts b/feature/pairalert/build.gradle.kts index 859ef0e9d..766121570 100644 --- a/feature/pairalert/build.gradle.kts +++ b/feature/pairalert/build.gradle.kts @@ -5,6 +5,14 @@ plugins { alias(libs.plugins.compose.compiler) } +kotlin { + compilerOptions { + jvmTarget.set( + org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21, + ) + } +} + android { namespace = "dev.arkbuilders.rate.feature.pairalert" compileSdk = libs.versions.compileSdk.get().toInt() @@ -32,9 +40,6 @@ android { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } - kotlinOptions { - jvmTarget = "21" - } } dependencies { @@ -49,6 +54,7 @@ dependencies { implementation(libs.androidx.ui) implementation(libs.navigation.compose) implementation(libs.material3) + implementation(libs.material.icons.extended) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) diff --git a/feature/pairalert/src/main/java/dev/arkbuilders/rate/feature/pairalert/presentation/add/EditCondition.kt b/feature/pairalert/src/main/java/dev/arkbuilders/rate/feature/pairalert/presentation/add/EditCondition.kt index a677375d0..fbbc05376 100644 --- a/feature/pairalert/src/main/java/dev/arkbuilders/rate/feature/pairalert/presentation/add/EditCondition.kt +++ b/feature/pairalert/src/main/java/dev/arkbuilders/rate/feature/pairalert/presentation/add/EditCondition.kt @@ -15,7 +15,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -36,7 +35,6 @@ fun EditCondition( onPriceOrPercentInputChanged: (String) -> Unit, onIncreaseToggle: () -> Unit, ) { - val ctx = LocalContext.current Column( modifier = Modifier.padding(top = 48.dp, start = 16.dp, end = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, @@ -97,9 +95,9 @@ fun EditCondition( modifier = Modifier.padding(start = 4.dp), text = if (state.aboveNotBelow) - ctx.getString(CoreRString.above) + stringResource(CoreRString.above) else - ctx.getString(CoreRString.below), + stringResource(CoreRString.below), color = if (state.aboveNotBelow) ArkColor.PairAlertInc diff --git a/feature/portfolio/build.gradle.kts b/feature/portfolio/build.gradle.kts index 1e4f518de..6339558b7 100644 --- a/feature/portfolio/build.gradle.kts +++ b/feature/portfolio/build.gradle.kts @@ -6,6 +6,14 @@ plugins { id("kotlin-parcelize") } +kotlin { + compilerOptions { + jvmTarget.set( + org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21, + ) + } +} + android { namespace = "dev.arkbuilders.rate.feature.portfolio" compileSdk = libs.versions.compileSdk.get().toInt() @@ -33,9 +41,6 @@ android { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } - kotlinOptions { - jvmTarget = "21" - } } dependencies { @@ -50,6 +55,7 @@ dependencies { implementation(libs.androidx.ui) implementation(libs.navigation.compose) implementation(libs.material3) + implementation(libs.material.icons.extended) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) diff --git a/feature/portfolio/src/main/java/dev/arkbuilders/rate/feature/portfolio/presentation/main/PortfolioScreen.kt b/feature/portfolio/src/main/java/dev/arkbuilders/rate/feature/portfolio/presentation/main/PortfolioScreen.kt index 162124ffc..b8a441243 100644 --- a/feature/portfolio/src/main/java/dev/arkbuilders/rate/feature/portfolio/presentation/main/PortfolioScreen.kt +++ b/feature/portfolio/src/main/java/dev/arkbuilders/rate/feature/portfolio/presentation/main/PortfolioScreen.kt @@ -113,6 +113,7 @@ fun PortfolioScreen( val editGroupReorderSheetState = rememberModalBottomSheetState() val editGroupOptionsSheetState = rememberModalBottomSheetState() val editGroupRenameSheetState = rememberModalBottomSheetState() + val changeBaseCurrencyTitle = stringResource(CoreRString.change_base_currency) fun getCurrentGroup() = state.pages.getOrNull(pagerState.currentPage)?.group @@ -164,10 +165,7 @@ fun PortfolioScreen( onChangeBaseCurrency = { navigator.navigate( SearchCurrencyScreenDestination( - title = - ctx.getString( - CoreRString.change_base_currency, - ), + title = changeBaseCurrencyTitle, ), ) }, diff --git a/feature/quick/build.gradle.kts b/feature/quick/build.gradle.kts index e49082b00..88421a1d8 100644 --- a/feature/quick/build.gradle.kts +++ b/feature/quick/build.gradle.kts @@ -5,6 +5,14 @@ plugins { alias(libs.plugins.compose.compiler) } +kotlin { + compilerOptions { + jvmTarget.set( + org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21, + ) + } +} + android { namespace = "dev.arkbuilders.rate.feature.quick" compileSdk = libs.versions.compileSdk.get().toInt() @@ -32,9 +40,6 @@ android { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } - kotlinOptions { - jvmTarget = "21" - } } dependencies { @@ -47,6 +52,7 @@ dependencies { implementation(libs.androidx.ui) implementation(libs.navigation.compose) implementation(libs.material3) + implementation(libs.material.icons.extended) implementation(libs.androidx.ui.tooling) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.lifecycle.runtime.ktx) diff --git a/feature/quickwidget/build.gradle.kts b/feature/quickwidget/build.gradle.kts index fcb4bb911..544d8b569 100644 --- a/feature/quickwidget/build.gradle.kts +++ b/feature/quickwidget/build.gradle.kts @@ -5,6 +5,14 @@ plugins { alias(libs.plugins.compose.compiler) } +kotlin { + compilerOptions { + jvmTarget.set( + org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21, + ) + } +} + android { namespace = "dev.arkbuilders.rate.feature.quickwidget" compileSdk = libs.versions.compileSdk.get().toInt() @@ -29,9 +37,6 @@ android { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } - kotlinOptions { - jvmTarget = "21" - } } dependencies { diff --git a/feature/search/build.gradle.kts b/feature/search/build.gradle.kts index b15860627..be2404f4f 100644 --- a/feature/search/build.gradle.kts +++ b/feature/search/build.gradle.kts @@ -6,6 +6,14 @@ plugins { id("kotlin-parcelize") } +kotlin { + compilerOptions { + jvmTarget.set( + org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21, + ) + } +} + android { namespace = "dev.arkbuilders.rate.feature.search" compileSdk = libs.versions.compileSdk.get().toInt() @@ -33,9 +41,6 @@ android { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } - kotlinOptions { - jvmTarget = "21" - } } dependencies { diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index ac98800d2..509afa760 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -5,6 +5,14 @@ plugins { alias(libs.plugins.compose.compiler) } +kotlin { + compilerOptions { + jvmTarget.set( + org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21, + ) + } +} + android { namespace = "dev.arkbuilders.rate.feature.settings" compileSdk = libs.versions.compileSdk.get().toInt() @@ -32,9 +40,6 @@ android { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } - kotlinOptions { - jvmTarget = "21" - } } dependencies { diff --git a/feature/settings/src/main/java/dev/arkbuilders/rate/feature/settings/presentation/SettingsScreen.kt b/feature/settings/src/main/java/dev/arkbuilders/rate/feature/settings/presentation/SettingsScreen.kt index 7bb883106..9f658fd2c 100644 --- a/feature/settings/src/main/java/dev/arkbuilders/rate/feature/settings/presentation/SettingsScreen.kt +++ b/feature/settings/src/main/java/dev/arkbuilders/rate/feature/settings/presentation/SettingsScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -103,6 +104,7 @@ private fun Content( onAboutClick: () -> Unit, ) { val ctx = LocalContext.current + val resources = LocalResources.current Column( modifier = Modifier @@ -121,7 +123,7 @@ private fun Content( fun formatTime(date: OffsetDateTime): String { val elapsed = DateFormatUtils.formatElapsedTime(ctx, now, date) val time = DateFormatUtils.formatFullDateTime(date) - return ctx.getString(CoreRString.settings_elapsed_ago, elapsed) + time + return resources.getString(CoreRString.settings_elapsed_ago, elapsed) + time } val refreshDesc = diff --git a/feature/settings/src/main/java/dev/arkbuilders/rate/feature/settings/presentation/SettingsViewModel.kt b/feature/settings/src/main/java/dev/arkbuilders/rate/feature/settings/presentation/SettingsViewModel.kt index 4d103cbac..d01e72955 100644 --- a/feature/settings/src/main/java/dev/arkbuilders/rate/feature/settings/presentation/SettingsViewModel.kt +++ b/feature/settings/src/main/java/dev/arkbuilders/rate/feature/settings/presentation/SettingsViewModel.kt @@ -3,9 +3,9 @@ package dev.arkbuilders.rate.feature.settings.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.google.firebase.analytics.ktx.analytics -import com.google.firebase.crashlytics.ktx.crashlytics -import com.google.firebase.ktx.Firebase +import com.google.firebase.Firebase +import com.google.firebase.analytics.analytics +import com.google.firebase.crashlytics.crashlytics import dev.arkbuilders.rate.core.domain.BuildConfigFields import dev.arkbuilders.rate.core.domain.model.TimestampType import dev.arkbuilders.rate.core.domain.repo.AnalyticsManager diff --git a/fiaticons/build.gradle.kts b/fiaticons/build.gradle.kts index ae579d76b..426f84698 100644 --- a/fiaticons/build.gradle.kts +++ b/fiaticons/build.gradle.kts @@ -3,12 +3,20 @@ plugins { id("org.jetbrains.kotlin.android") } +kotlin { + compilerOptions { + jvmTarget.set( + org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21, + ) + } +} + android { namespace = "dev.arkbuilders.rate.fiaticons" - compileSdk = 35 + compileSdk = libs.versions.compileSdk.get().toInt() defaultConfig { - minSdk = 26 + minSdk = libs.versions.minSdk.get().toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") @@ -27,16 +35,13 @@ android { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } - kotlinOptions { - jvmTarget = "21" - } } dependencies { - implementation("androidx.core:core-ktx:1.13.1") - implementation("androidx.appcompat:appcompat:1.6.1") - implementation("com.google.android.material:material:1.12.0") + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d7598855c..766911c07 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,49 +1,54 @@ [versions] -composeNavigation = "1.3.0" -hilt = "2.56.2" -kotlin = "2.1.20" -agp = "8.1.4" -playServicesWearable = "18.2.0" -coreKtx = "1.15.0" +composeNavigation = "1.6.2" +hilt = "2.60.1" +kotlin = "2.4.0" +agp = "8.13.2" +playServicesWearable = "20.0.1" +coreKtx = "1.18.0" minSdk = "26" compileSdk = "36" -ksp = "2.1.20-1.0.32" +ksp = "2.3.10" arkAbout = "0.2.1" -androidApp = "8.9.1" -activityCompose = "1.10.1" -composeDestinationsVersion = "2.1.0" -arrowFxCoroutines = "1.2.1" -arrowCore = "1.2.1" +androidApp = "8.13.2" +activityCompose = "1.13.0" +composeDestinationsVersion = "2.3.0" +arrowFxCoroutines = "2.2.3" +arrowCore = "2.2.3" constraintlayoutCompose = "1.1.1" -dagger = "2.50" -datastorePreferences = "1.1.4" -espressoCore = "3.6.1" -firebaseBom = "33.12.0" +concurrentFutures = "1.2.0" +dagger = "2.58" +datastorePreferences = "1.2.1" +espressoCore = "3.7.0" +firebaseBom = "34.16.0" glanceAppwidget = "1.1.1" junit = "4.13.2" -junitVersion = "1.2.1" -lifecycleRuntimeKtx = "2.8.7" -ktor = "3.1.3" -material3 = "1.3.2" -navigationComposeVersion = "2.8.9" +junitVersion = "1.3.0" +lifecycleRuntimeKtx = "2.10.0" +ktor = "3.5.1" +material3 = "1.4.0" +materialIconsExtended = "1.7.8" +navigationComposeVersion = "2.9.8" orbitMVI = "9.0.0" -qrgenerator = "1.0.1" -roomRuntime = "2.6.1" +qrgenerator = "1.0.5" +roomRuntime = "2.8.4" tagcloud = "1.1.0" timber = "5.0.1" -composeUi = "1.7.5" -workRuntimeKtx = "2.8.1" +composeUi = "1.11.4" +workRuntimeKtx = "2.11.2" appcompat = "1.7.1" -material = "1.12.0" -kotlinxSerializationJson = "1.8.1" +material = "1.14.0" +kotlinxSerializationJson = "1.11.0" reorderable = "2.4.3" playReview = "2.0.2" -composeBom = "2023.08.00" -composeMaterial = "1.2.1" -composeFoundation = "1.2.1" -coreSplashscreen = "1.0.1" +composeBom = "2026.06.01" +composeMaterial = "1.6.2" +composeFoundation = "1.6.2" +coreSplashscreen = "1.2.0" watchface = "1.2.1" +watchfaceComplicationsData = "1.3.0" +hiltNavigationCompose = "1.3.0" +wearComposeMaterial3 = "1.6.2" [libraries] androidx-compose-navigation = { module = "androidx.wear.compose:compose-navigation", version.ref = "composeNavigation" } @@ -52,6 +57,7 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } androidx-glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = "glanceAppwidget" } +androidx-concurrent-futures-ktx = { module = "androidx.concurrent:concurrent-futures-ktx", version.ref = "concurrentFutures" } androidx-junit = { module = "androidx.test.ext:junit", version.ref = "junitVersion" } androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" } @@ -78,12 +84,13 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } +hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } play-review = { group = "com.google.android.play", name = "review", version.ref = "playReview" } play-review-ktx = { group = "com.google.android.play", name = "review-ktx", version.ref = "playReview" } firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } -firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx", version = "" } -firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx", version = "" } +firebase-analytics = { module = "com.google.firebase:firebase-analytics" } +firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" } junit = { module = "junit:junit", version.ref = "junit" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } @@ -92,6 +99,7 @@ ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "k ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } +material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtended" } navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationComposeVersion" } orbit-compose = { module = "org.orbit-mvi:orbit-compose", version.ref = "orbitMVI" } orbit-viewmodel = { module = "org.orbit-mvi:orbit-viewmodel", version.ref = "orbitMVI" } @@ -107,11 +115,12 @@ play-services-wearable = { group = "com.google.android.gms", name = "play-servic androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-compose-material = { group = "androidx.wear.compose", name = "compose-material", version.ref = "composeMaterial" } androidx-compose-foundation = { group = "androidx.wear.compose", name = "compose-foundation", version.ref = "composeFoundation" } +androidx-wear-compose-material3 = { group = "androidx.wear.compose", name = "compose-material3", version.ref = "wearComposeMaterial3" } androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" } androidx-wear-watchface = { module = "androidx.wear.watchface:watchface", version.ref = "watchface" } -androidx-wear-watchface-complications-data = { module = "androidx.wear.watchface:watchface-complications-data", version.ref = "watchface" } -androidx-wear-watchface-complications-data-source = { module = "androidx.wear.watchface:watchface-complications-data-source", version.ref = "watchface" } -androidx-wear-watchface-complications-data-source-ktx = { module = "androidx.wear.watchface:watchface-complications-data-source-ktx", version.ref = "watchface" } +androidx-wear-watchface-complications-data = { module = "androidx.wear.watchface:watchface-complications-data", version.ref = "watchfaceComplicationsData" } +androidx-wear-watchface-complications-data-source = { module = "androidx.wear.watchface:watchface-complications-data-source", version.ref = "watchfaceComplicationsData" } +androidx-wear-watchface-complications-data-source-ktx = { module = "androidx.wear.watchface:watchface-complications-data-source-ktx", version.ref = "watchfaceComplicationsData" } androidx-wear-watchface-complications-rendering = { module = "androidx.wear.watchface:watchface-complications-rendering", version.ref = "watchface" } androidx-wear-watchface-editor = { module = "androidx.wear.watchface:watchface-editor", version.ref = "watchface" } androidx-wear-watchface-style = { module = "androidx.wear.watchface:watchface-style", version.ref = "watchface" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e2847c820..aaaabb3cb 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/watchapp/build.gradle.kts b/watchapp/build.gradle.kts index e45721117..4ca47abdf 100644 --- a/watchapp/build.gradle.kts +++ b/watchapp/build.gradle.kts @@ -5,6 +5,15 @@ plugins { alias(libs.plugins.ksp) id("com.google.dagger.hilt.android") } + +kotlin { + compilerOptions { + jvmTarget.set( + org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21, + ) + } +} + android { namespace = "dev.arkbuilders.rate.watchapp" compileSdk = libs.versions.compileSdk.get().toInt() @@ -91,9 +100,6 @@ android { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } - kotlinOptions { - jvmTarget = "21" - } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" @@ -116,7 +122,7 @@ dependencies { implementation(project(":feature:quick")) implementation(project(":core:domain")) implementation(project(":core:presentation")) - implementation("androidx.hilt:hilt-navigation-compose:1.2.0") + implementation(libs.hilt.navigation.compose) implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.ktx) @@ -127,8 +133,8 @@ dependencies { implementation(libs.play.services.wearable) // implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.ui) - implementation(libs.androidx.compose.navigation) // Or the latest version - implementation("androidx.wear.compose:compose-material3:1.6.1") // Or current version + implementation(libs.androidx.compose.navigation) + implementation(libs.androidx.wear.compose.material3) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.compose.material) @@ -136,6 +142,7 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(libs.androidx.core.splashscreen) implementation(libs.material3) + implementation(libs.material.icons.extended) implementation(libs.navigation.compose) implementation(libs.androidx.wear.watchface) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/ApiModule.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/ApiModule.kt index 17fb494cc..3bef2aa90 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/ApiModule.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/ApiModule.kt @@ -14,7 +14,6 @@ import javax.inject.Singleton class ApiModule { @Singleton @Provides - fun httpClient( - buildConfigFields: BuildConfigFields, - ): HttpClient = KtorHttpClientFactory(buildConfigFields).create() + fun httpClient(buildConfigFields: BuildConfigFields): HttpClient = + KtorHttpClientFactory(buildConfigFields).create() } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/RepoModule.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/RepoModule.kt index 2139eb89b..4efbe2719 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/RepoModule.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/RepoModule.kt @@ -159,7 +159,7 @@ class RepoModule { @Singleton @Provides - fun analyticsManager(prefs: Prefs): AnalyticsManager = AnalyticsManagerImpl(prefs) + fun analyticsManager(): AnalyticsManager = AnalyticsManagerImpl() @Singleton @Provides diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsScreen.kt index 7c50eb324..11208f909 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsScreen.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsScreen.kt @@ -13,7 +13,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -46,16 +45,18 @@ fun OptionsScreen( val showPinLimitDialog by viewModel.showPinLimitDialog.collectAsStateWithLifecycle() val listState = rememberScalingLazyListState() var showDeleteDialog by remember { mutableStateOf(false) } + val deletedSuccessfully = stringResource(R.string.deleted_successfully) + val pinnedSuccessfully = stringResource(R.string.pinned_successfully) + val unpinnedSuccessfully = stringResource(R.string.unpinned_successfully) if (showDeleteDialog) { - val context = LocalContext.current WearConfirmationDialog( title = stringResource(R.string.delete_pair), message = stringResource(R.string.are_you_sure_you_want_to_delete_this_pair), onConfirm = { showDeleteDialog = false viewModel.deletePair(onDeleted = { - onDeleteSuccess(context.getString(R.string.deleted_successfully)) + onDeleteSuccess(deletedSuccessfully) }) }, onDismiss = { @@ -117,7 +118,6 @@ fun OptionsScreen( item { val isPinned = quickPair?.isPinned() == true - val context = LocalContext.current WearOptionButton( text = if (isPinned) @@ -130,9 +130,9 @@ fun OptionsScreen( onClick = { viewModel.togglePin(onSuccess = { pinned -> if (pinned) { - onPinClick(context.getString(R.string.pinned_successfully)) + onPinClick(pinnedSuccessfully) } else { - onPinClick(context.getString(R.string.unpinned_successfully)) + onPinClick(unpinnedSuccessfully) } }) }, From e207c9356961ce0cd412f62d6d58f4076d75a19b Mon Sep 17 00:00:00 2001 From: Shubert Munthali Date: Thu, 16 Jul 2026 12:37:43 +0200 Subject: [PATCH 12/12] Explicitly return empty icons when missing drawable folder on gradle sync --- app/build.gradle.kts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1920d1921..cdba5cf79 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -192,8 +192,11 @@ tasks.getByPath(":app:preBuild").dependsOn("ktlintFormat") fun collectCurrencyIcons(moduleDir: File): List { val drawableDir = moduleDir.resolve("src/main/res/drawable") + if (!drawableDir.exists()) { + return emptyList() + } return drawableDir - .listFiles()!! + .listFiles() .map { it.nameWithoutExtension.uppercase() } .map { when {