Skip to content

Commit

Permalink
Merge pull request #48 from nimblehq/feature/20-chart-of-coin-prices-…
Browse files Browse the repository at this point in the history
…integrate

[#20][Integrate] As a user, I can see the chart of coin prices
  • Loading branch information
luongvo authored Oct 13, 2022
2 parents 4d366b0 + 4312446 commit 14524e6
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,13 @@ import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.hilt.navigation.compose.hiltViewModel
import co.nimblehq.compose.crypto.R
import co.nimblehq.compose.crypto.data.extension.orZero
import co.nimblehq.compose.crypto.domain.model.CoinPrice
import co.nimblehq.compose.crypto.extension.toFormattedString
import co.nimblehq.compose.crypto.lib.IsLoading
import co.nimblehq.compose.crypto.ui.common.price.PriceChangeButton
import co.nimblehq.compose.crypto.ui.components.chartintervals.ChartIntervalsButtonGroup
import co.nimblehq.compose.crypto.ui.components.chartintervals.TimeIntervals
import co.nimblehq.compose.crypto.ui.components.linechart.CoinPriceChart
import co.nimblehq.compose.crypto.ui.components.linechart.CoinPriceLabelDrawer
import co.nimblehq.compose.crypto.ui.navigation.AppDestination
Expand All @@ -51,7 +54,6 @@ import me.bytebeats.views.charts.line.render.line.GradientLineShader
import me.bytebeats.views.charts.line.render.line.SolidLineDrawer
import me.bytebeats.views.charts.line.render.point.EmptyPointDrawer
import me.bytebeats.views.charts.simpleChartAnimation
import kotlin.random.Random

@Composable
fun DetailScreen(
Expand All @@ -71,11 +73,16 @@ fun DetailScreen(
}

val coinDetailUiModel: CoinDetailUiModel? by viewModel.output.coinDetailUiModel.collectAsState()
val coinPrices: List<CoinPrice> by viewModel.output.coinPrices.collectAsState()
val showLoading: IsLoading by viewModel.showLoading.collectAsState()

DetailScreenContent(
coinDetailUiModel = coinDetailUiModel,
coinPrices = coinPrices,
onBackIconClick = { navigator(AppDestination.Up) },
onTimeIntervalsChanged = { timeIntervals ->
viewModel.getCoinPrices(coinId = coinId, timeIntervals)
},
showLoading = showLoading
)

Expand All @@ -84,16 +91,17 @@ fun DetailScreen(
}
}

@Suppress("MagicNumber")
@Suppress("LongMethod", "LongParameterList")
@Composable
private fun DetailScreenContent(
coinDetailUiModel: CoinDetailUiModel?,
coinPrices: List<CoinPrice>,
onBackIconClick: () -> Unit,
onTimeIntervalsChanged: (TimeIntervals) -> Unit,
showLoading: Boolean
) {
val localDensity = LocalDensity.current
val sellBuyLayoutHeight = remember { mutableStateOf(Dp0) }
val context = LocalContext.current

Surface {
ConstraintLayout(
Expand Down Expand Up @@ -163,16 +171,6 @@ private fun DetailScreenContent(
displayForDetailPage = true
)

// TODO: Update this section when work create UI for a graph.
val mockData = mutableListOf<LineChartData.Point>()
for (i in 1..10) {
mockData.add(
LineChartData.Point(
Random.nextInt(18000, 30000).toFloat(),
stringResource(R.string.coin_currency, "3,260.62")
)
)
}
CoinPriceChart(
modifier = Modifier
.fillMaxWidth()
Expand All @@ -182,7 +180,13 @@ private fun DetailScreenContent(
end.linkTo(parent.end)
},
lineChartData = LineChartData(
points = mockData
points = coinPrices.map { coinPrice ->
val price = stringResource(
R.string.coin_currency,
coinPrice.price.toFormattedString()
)
LineChartData.Point(coinPrice.price.orZero().toFloat(), price)
}
),
animation = simpleChartAnimation(),
pointDrawer = EmptyPointDrawer,
Expand All @@ -205,14 +209,7 @@ private fun DetailScreenContent(
start.linkTo(parent.start)
end.linkTo(parent.end)
},
onIntervalChanged = { timeIntervals ->
// TODO Refresh the chart on time interval changed
Toast.makeText(
context,
"Time interval changed: $timeIntervals",
Toast.LENGTH_SHORT
).show()
}
onIntervalChanged = onTimeIntervalsChanged::invoke
)

CoinInfo(
Expand Down Expand Up @@ -294,7 +291,9 @@ fun DetailScreenPreview(
ComposeTheme {
DetailScreenContent(
coinDetailUiModel = params.detail,
coinPrices = emptyList(),
onBackIconClick = {},
onTimeIntervalsChanged = {},
showLoading = params.showLoading
)
}
Expand All @@ -308,7 +307,9 @@ fun DetailScreenPreviewDark(
ComposeTheme {
DetailScreenContent(
coinDetailUiModel = params.detail,
coinPrices = emptyList(),
onBackIconClick = {},
onTimeIntervalsChanged = {},
showLoading = params.showLoading
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
package co.nimblehq.compose.crypto.ui.screens.detail

import co.nimblehq.compose.crypto.domain.model.CoinPrice
import co.nimblehq.compose.crypto.domain.usecase.GetCoinDetailUseCase
import co.nimblehq.compose.crypto.ui.base.*
import co.nimblehq.compose.crypto.domain.usecase.GetCoinPricesUseCase
import co.nimblehq.compose.crypto.ui.base.BaseInput
import co.nimblehq.compose.crypto.ui.base.BaseOutput
import co.nimblehq.compose.crypto.ui.base.BaseViewModel
import co.nimblehq.compose.crypto.ui.components.chartintervals.TimeIntervals
import co.nimblehq.compose.crypto.ui.screens.home.FIAT_CURRENCY
import co.nimblehq.compose.crypto.ui.uimodel.CoinDetailUiModel
import co.nimblehq.compose.crypto.ui.uimodel.toUiModel
import co.nimblehq.compose.crypto.util.DispatchersProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.filterNot
import java.util.*
import javax.inject.Inject

interface Input : BaseInput {
Expand All @@ -17,12 +27,14 @@ interface Input : BaseInput {
interface Output : BaseOutput {

val coinDetailUiModel: StateFlow<CoinDetailUiModel?>
val coinPrices: StateFlow<List<CoinPrice>>
}

@HiltViewModel
class DetailViewModel @Inject constructor(
dispatchers: DispatchersProvider,
private val getCoinDetailUseCase: GetCoinDetailUseCase
private val getCoinDetailUseCase: GetCoinDetailUseCase,
private val getCoinPricesUseCase: GetCoinPricesUseCase
) : BaseViewModel(dispatchers), Input, Output {

override val input = this
Expand All @@ -32,8 +44,13 @@ class DetailViewModel @Inject constructor(
override val coinDetailUiModel: StateFlow<CoinDetailUiModel?>
get() = _coinDetailUiModel

private val _coinPrices = MutableStateFlow<List<CoinPrice>>(emptyList())
override val coinPrices: StateFlow<List<CoinPrice>>
get() = _coinPrices

override fun getCoinId(coinId: String) {
getCoinDetail(coinId = coinId)
getCoinPrices(coinId = coinId)
}

private fun getCoinDetail(coinId: String) = execute {
Expand All @@ -47,4 +64,35 @@ class DetailViewModel @Inject constructor(
}
hideLoading()
}

@Suppress("MagicNumber")
fun getCoinPrices(coinId: String, timeIntervals: TimeIntervals = TimeIntervals.ONE_DAY) =
execute {
val fromTimestamp = Calendar.getInstance().apply {
when (timeIntervals) {
TimeIntervals.ONE_DAY -> add(Calendar.DAY_OF_YEAR, -1)
TimeIntervals.ONE_WEEK -> add(Calendar.DAY_OF_YEAR, -7)
TimeIntervals.ONE_MONTH -> add(Calendar.MONTH, -1)
TimeIntervals.ONE_YEAR -> add(Calendar.YEAR, -1)
TimeIntervals.FIVE_YEAR -> add(Calendar.YEAR, -5)
}
}

showLoading()
getCoinPricesUseCase.execute(
GetCoinPricesUseCase.Input(
coinId = coinId,
currency = FIAT_CURRENCY,
fromTimestamp = fromTimestamp.timeInMillis.div(1000),
toTimestamp = Calendar.getInstance().timeInMillis.div(1000)
)
).catch { e ->
_error.emit(e)
}.filterNot {
it.isEmpty()
}.collect { coinPrices ->
_coinPrices.emit(coinPrices)
}
hideLoading()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package co.nimblehq.compose.crypto.ui.screens.detail

import app.cash.turbine.test
import co.nimblehq.compose.crypto.domain.usecase.GetCoinDetailUseCase
import co.nimblehq.compose.crypto.domain.usecase.GetCoinPricesUseCase
import co.nimblehq.compose.crypto.test.MockUtil
import co.nimblehq.compose.crypto.ui.screens.BaseViewModelTest
import co.nimblehq.compose.crypto.ui.uimodel.toUiModel
Expand All @@ -14,17 +15,21 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.*
import org.junit.After
import org.junit.Before
import org.junit.Test

@ExperimentalCoroutinesApi
class DetailViewModelTest : BaseViewModelTest() {

private val mockGetCoinDetailUseCase = mockk<GetCoinDetailUseCase>()
private val mockGetCoinPricesUseCase = mockk<GetCoinPricesUseCase>()
private lateinit var viewModel: DetailViewModel

@Before
fun setUp() {
every { mockGetCoinDetailUseCase.execute(any()) } returns flowOf(MockUtil.coinDetail)
every { mockGetCoinPricesUseCase.execute(any()) } returns flowOf(emptyList())

Dispatchers.setMain(testDispatcher)
}
Expand Down Expand Up @@ -69,7 +74,8 @@ class DetailViewModelTest : BaseViewModelTest() {
private fun initViewModel() {
viewModel = DetailViewModel(
testDispatcherProvider,
mockGetCoinDetailUseCase
mockGetCoinDetailUseCase,
mockGetCoinPricesUseCase
)
}
}

0 comments on commit 14524e6

Please sign in to comment.