Skip to content

Commit

Permalink
#8: Global refactoring (#13)
Browse files Browse the repository at this point in the history
* use StateList, models refactoring

* Caching with Room, NetworkStatus implemented

* Added user agent

* fix Key [CurrencyCode] was already used

java.lang.IllegalArgumentException: Key TRX was already used. If you are using LazyColumn/Row please make sure you provide a unique key for each item.

* Format amount input

* Fix api links
  • Loading branch information
mdrlzy authored Nov 12, 2022
1 parent 80ed729 commit f956f95
Show file tree
Hide file tree
Showing 51 changed files with 1,378 additions and 1,044 deletions.
20 changes: 14 additions & 6 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,25 @@ android {
}

dependencies {
implementation 'com.squareup.moshi:moshi-kotlin:1.14.0'
kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.14.0'
implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.3"
implementation "androidx.compose.ui:ui:$compose_version"
implementation 'io.coil-kt:coil:2.2.1'
implementation 'io.coil-kt:coil-gif:2.2.1'
implementation 'io.coil-kt:coil-compose:2.2.1'
implementation "androidx.navigation:navigation-compose:2.5.2"
implementation "com.google.accompanist:accompanist-navigation-animation:0.27.0"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation 'androidx.activity:activity-compose:1.5.1'

implementation "com.google.dagger:dagger:2.42"
kapt "com.google.dagger:dagger-compiler:2.42"

implementation "androidx.room:room-runtime:2.4.3"
implementation "androidx.room:room-ktx:2.4.3"
kapt "androidx.room:room-compiler:2.4.3"

implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1'

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
Expand Down
5 changes: 3 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
xmlns:tools="http://schemas.android.com/tools"
package="space.taran.arkrate">

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />

<application
android:name=".App"
android:name=".presentation.App"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
Expand All @@ -18,7 +19,7 @@
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:name=".presentation.MainActivity"
android:exported="true"
android:theme="@style/Theme.Exchange">
<intent-filter>
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/java/space/taran/arkrate/data/CurrencyAmount.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package space.taran.arkrate.data

data class CurrencyAmount(
val code: String,
var amount: Double
)
6 changes: 6 additions & 0 deletions app/src/main/java/space/taran/arkrate/data/CurrencyName.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package space.taran.arkrate.data

data class CurrencyName(
val code: String,
val name: String
)
6 changes: 6 additions & 0 deletions app/src/main/java/space/taran/arkrate/data/CurrencyRate.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package space.taran.arkrate.data

data class CurrencyRate(
val code: String,
val rate: Double
)
56 changes: 56 additions & 0 deletions app/src/main/java/space/taran/arkrate/data/CurrencyRepo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package space.taran.arkrate.data

import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import space.taran.arkrate.data.db.CurrencyRateLocalDataSource
import space.taran.arkrate.data.db.FetchTimestampDataSource
import space.taran.arkrate.data.network.NetworkStatus
import space.taran.arkrate.utils.withContextAndLock
import java.util.concurrent.TimeUnit

abstract class CurrencyRepo(
private val local: CurrencyRateLocalDataSource,
private val networkStatus: NetworkStatus,
private val fetchTimestampDataSource: FetchTimestampDataSource
) {
protected abstract val type: CurrencyType
private var currencyRates: List<CurrencyRate>? = null
private var updatedTS: Long? = null
private val mutex = Mutex()

suspend fun getCurrencyRate(): List<CurrencyRate> =
withContextAndLock(Dispatchers.IO, mutex) {
if (!networkStatus.isOnline()) {
currencyRates = local.getByType(type)
return@withContextAndLock currencyRates!!
}

updatedTS ?: let {
updatedTS = fetchTimestampDataSource.getTimestamp(type)
}

if (
updatedTS == null ||
updatedTS!! + dayInMillis < System.currentTimeMillis()
) {
currencyRates = fetchRemote()
launch { fetchTimestampDataSource.rememberTimestamp(type) }
launch { local.insert(currencyRates!!, type) }
updatedTS = System.currentTimeMillis()
}

currencyRates ?: let {
currencyRates = local.getByType(type)
}
Log.d("Test", "${currencyRates!!.sortedBy { it.code }}")
return@withContextAndLock currencyRates!!
}

protected abstract suspend fun fetchRemote(): List<CurrencyRate>

abstract suspend fun getCurrencyName(): List<CurrencyName>

private val dayInMillis = TimeUnit.DAYS.toMillis(1)
}
5 changes: 5 additions & 0 deletions app/src/main/java/space/taran/arkrate/data/CurrencyType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package space.taran.arkrate.data

enum class CurrencyType {
FIAT, CRYPTO
}
27 changes: 27 additions & 0 deletions app/src/main/java/space/taran/arkrate/data/GeneralCurrencyRepo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package space.taran.arkrate.data

import space.taran.arkrate.data.crypto.CryptoCurrencyRepo
import space.taran.arkrate.data.fiat.FiatCurrencyRepo
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class GeneralCurrencyRepo @Inject constructor(
val fiatRepo: FiatCurrencyRepo,
val cryptoRepo: CryptoCurrencyRepo
) {
private val currencyRepos = listOf(
fiatRepo,
cryptoRepo
)

suspend fun getCurrencyRate(): List<CurrencyRate> =
currencyRepos.fold(emptyList()) { codeToRate, repo ->
codeToRate + repo.getCurrencyRate()
}

suspend fun getCurrencyName(): List<CurrencyName> =
currencyRepos.fold(emptyList()) { currencyName, repo ->
currencyName + repo.getCurrencyName()
}
}
60 changes: 60 additions & 0 deletions app/src/main/java/space/taran/arkrate/data/assets/AssetsRepo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package space.taran.arkrate.data.assets

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import space.taran.arkrate.data.CurrencyAmount
import space.taran.arkrate.data.db.AssetsLocalDataSource
import space.taran.arkrate.utils.replace
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class AssetsRepo @Inject constructor(
private val local: AssetsLocalDataSource
) {
private var currencyAmountList = listOf<CurrencyAmount>()
private val currencyAmountFlow =
MutableStateFlow<List<CurrencyAmount>>(emptyList())
private val scope = CoroutineScope(Dispatchers.IO)

init {
scope.launch {
currencyAmountList = local.getAll()
currencyAmountFlow.emit(currencyAmountList)
}
}

fun allCurrencyAmount(): List<CurrencyAmount> = currencyAmountList

fun allCurrencyAmountFlow(): StateFlow<List<CurrencyAmount>> = currencyAmountFlow

suspend fun setCurrencyAmount(code: String, amount: Double) =
withContext(Dispatchers.IO) {
currencyAmountList.find {
it.code == code
}?.let { currencyAmount ->
currencyAmountList = currencyAmountList.replace(
currencyAmount,
currencyAmount.copy(amount = amount)
)
} ?: let {
currencyAmountList =
currencyAmountList + CurrencyAmount(code, amount)
}
currencyAmountFlow.emit(currencyAmountList.toList())
local.insert(CurrencyAmount(code, amount))
}

suspend fun removeCurrency(code: String) = withContext(Dispatchers.IO) {
currencyAmountList.find { it.code == code }?.let {
currencyAmountList = currencyAmountList - it
}
currencyAmountFlow.emit(currencyAmountList)
local.delete(code)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package space.taran.arkrate.data.crypto

import retrofit2.http.GET

interface CryptoAPI {
@GET("/ARK-Builders/ark-exchange-rates/main/crypto-rates.json")
suspend fun getCryptoRates(): List<CryptoRateResponse>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package space.taran.arkrate.data.crypto

import space.taran.arkrate.data.CurrencyName
import space.taran.arkrate.data.CurrencyRate
import space.taran.arkrate.data.CurrencyRepo
import space.taran.arkrate.data.CurrencyType
import space.taran.arkrate.data.network.NetworkStatus
import space.taran.arkrate.data.db.CurrencyRateLocalDataSource
import space.taran.arkrate.data.db.FetchTimestampDataSource
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class CryptoCurrencyRepo @Inject constructor(
private val cryptoAPI: CryptoAPI,
private val local: CurrencyRateLocalDataSource,
private val networkStatus: NetworkStatus,
private val fetchTimestampDataSource: FetchTimestampDataSource
) : CurrencyRepo(local, networkStatus, fetchTimestampDataSource) {
override val type = CurrencyType.CRYPTO

override suspend fun fetchRemote(): List<CurrencyRate> =
cryptoAPI.getCryptoRates().findUSDTPairs()

override suspend fun getCurrencyName(): List<CurrencyName> =
getCurrencyRate().map {
CurrencyName(it.code, name = "")
}

// api returns pairs like ETHBTC, ETHBNB, ETHTRX, ETHUSDT
// we only take USDT pairs
private fun List<CryptoRateResponse>.findUSDTPairs() =
mapNotNull { (code, price) ->
if (code.takeLast(4) == "USDT") {
CurrencyRate(code.dropLast(4), price)
} else
null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package space.taran.arkrate.data.crypto

data class CryptoRateResponse(
val symbol: String,
val price: Double
)
41 changes: 41 additions & 0 deletions app/src/main/java/space/taran/arkrate/data/db/AssetsDao.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package space.taran.arkrate.data.db

import androidx.room.Dao
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import space.taran.arkrate.data.CurrencyAmount
import javax.inject.Inject

@Entity
data class RoomCurrencyAmount(
@PrimaryKey
val code: String,
val amount: Double
)

@Dao
interface AssetsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(currencyAmount: RoomCurrencyAmount)

@Query("SELECT * FROM RoomCurrencyAmount")
suspend fun getAll(): List<RoomCurrencyAmount>

@Query("DELETE FROM RoomCurrencyAmount where code = :code")
suspend fun delete(code: String)
}

class AssetsLocalDataSource @Inject constructor(val dao: AssetsDao) {
suspend fun insert(currencyAmount: CurrencyAmount) =
dao.insert(currencyAmount.toRoom())

suspend fun getAll() = dao.getAll().map { it.toCurrencyAmount() }

suspend fun delete(code: String) = dao.delete(code)
}

private fun RoomCurrencyAmount.toCurrencyAmount() = CurrencyAmount(code, amount)
private fun CurrencyAmount.toRoom() = RoomCurrencyAmount(code, amount)
42 changes: 42 additions & 0 deletions app/src/main/java/space/taran/arkrate/data/db/CurrencyRateDao.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package space.taran.arkrate.data.db

import androidx.room.Dao
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import space.taran.arkrate.data.CurrencyRate
import space.taran.arkrate.data.CurrencyType
import javax.inject.Inject

@Entity
data class RoomCurrencyRate(
@PrimaryKey
val code: String,
val currencyType: String,
val rate: Double
)

@Dao
interface CurrencyRateDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(currencyRate: List<RoomCurrencyRate>)

@Query("SELECT * FROM RoomCurrencyRate where currencyType = :currencyType")
suspend fun getByType(currencyType: String): List<RoomCurrencyRate>
}

class CurrencyRateLocalDataSource @Inject constructor(val dao: CurrencyRateDao) {
suspend fun insert(
currencyRate: List<CurrencyRate>,
currencyType: CurrencyType
) = dao.insert(currencyRate.map { it.toRoom(currencyType) })

suspend fun getByType(currencyType: CurrencyType) =
dao.getByType(currencyType.name).map { it.toCurrencyRate() }
}

private fun RoomCurrencyRate.toCurrencyRate() = CurrencyRate(code, rate)
private fun CurrencyRate.toRoom(currencyType: CurrencyType) =
RoomCurrencyRate(code, currencyType.name, rate)
Loading

0 comments on commit f956f95

Please sign in to comment.