Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-02 13:30:24 +05:00
parent c8070ec66e
commit bee2be0994
7 changed files with 340 additions and 1 deletions

View file

@ -22,6 +22,11 @@ dependencies {
implementation(projects.core.ui)
implementation(projects.core.navigation)
/** Compose */
implementation(tangemDeps.vico.core)
implementation(tangemDeps.vico.compose)
implementation(tangemDeps.vico.compose.m3)
/** Common */
implementation(projects.common.ui)
implementation(projects.common.routing)

View file

@ -0,0 +1,35 @@
package com.tangem.features.yield.supply.impl.chart
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.yield.supply.impl.chart.model.YieldSupplyChartModel
import com.tangem.features.yield.supply.impl.chart.ui.YieldSupplyChartContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultYieldSupplyChartComponent @AssistedInject constructor(
@Assisted private val appComponentContext: AppComponentContext,
@Assisted private val params: YieldSupplyChartModel.Params,
) : AppComponentContext by appComponentContext {
private val model: YieldSupplyChartModel = getOrCreateModel(params = params)
@Composable
fun Content(modifier: Modifier = Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
YieldSupplyChartContent(
state = state,
modifier = modifier,
)
}
@AssistedFactory
internal interface Factory {
fun create(context: AppComponentContext, params: YieldSupplyChartModel.Params): DefaultYieldSupplyChartComponent
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.yield.supply.impl.chart.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.yield.supply.impl.chart.model.YieldSupplyChartModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(ModelComponent::class)
internal interface YieldSupplyChartModule {
@Binds
@IntoMap
@ClassKey(YieldSupplyChartModel::class)
fun bindYieldSupplyChartModel(impl: YieldSupplyChartModel): Model
}

View file

@ -0,0 +1,36 @@
package com.tangem.features.yield.supply.impl.chart.entity
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Immutable
internal sealed class YieldSupplyChartUM {
@Immutable
data object Loading : YieldSupplyChartUM()
@Immutable
data class Error(val onRetry: () -> Unit) : YieldSupplyChartUM()
@Immutable
data class Data(val chartData: YieldSupplyMarketChartData) : YieldSupplyChartUM()
}
@Immutable
internal data class YieldSupplyMarketChartData(
val y: ImmutableList<Double>,
val x: ImmutableList<Double>,
) {
companion object {
@Suppress("MagicNumber")
fun mock(): YieldSupplyMarketChartData {
// TODO [REDACTED_TASK_KEY] replace mock values with real APY series
val y = listOf(
4.6, 4.0, 4.2, 3.3, 2.5, 5.6, 4.2, 6.7, 3.5, 2.5, 4.5, 3.4,
).toImmutableList()
val x = List(y.size) { 1.0 }.toImmutableList()
return YieldSupplyMarketChartData(y = y, x = x)
}
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.features.yield.supply.impl.chart.model
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.yield.supply.impl.chart.entity.YieldSupplyChartUM
import com.tangem.features.yield.supply.impl.chart.entity.YieldSupplyMarketChartData
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
@Suppress("UnusedPrivateProperty")
@ModelScoped
internal class YieldSupplyChartModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
data class Params(
val tokenContractAddress: String,
val onRetry: () -> Unit = {},
)
private val params: Params = paramsContainer.require()
val uiState: StateFlow<YieldSupplyChartUM>
field = MutableStateFlow<YieldSupplyChartUM>(YieldSupplyChartUM.Loading)
init {
// TODO [REDACTED_TASK_KEY]
val chartData = YieldSupplyMarketChartData.mock()
uiState.update { YieldSupplyChartUM.Data(chartData = chartData) }
}
}

View file

@ -0,0 +1,205 @@
package com.tangem.features.yield.supply.impl.chart.ui
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisLabelComponent
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottomAxis
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberCustomStartAxis
import com.patrykandpatrick.vico.compose.cartesian.decoration.rememberHorizontalLine
import com.patrykandpatrick.vico.compose.cartesian.fullWidth
import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart
import com.patrykandpatrick.vico.compose.cartesian.rememberVicoScrollState
import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberColumnCartesianLayer
import com.patrykandpatrick.vico.compose.common.component.rememberLineComponent
import com.patrykandpatrick.vico.compose.common.shape.dashed
import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout
import com.patrykandpatrick.vico.core.cartesian.Zoom
import com.patrykandpatrick.vico.core.cartesian.axis.AxisPosition
import com.patrykandpatrick.vico.core.cartesian.axis.HorizontalAxis
import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel
import com.patrykandpatrick.vico.core.cartesian.data.ColumnCartesianLayerModel
import com.patrykandpatrick.vico.core.common.shape.Shape
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.yield.supply.impl.chart.entity.YieldSupplyChartUM
import com.tangem.features.yield.supply.impl.chart.entity.YieldSupplyMarketChartData
@Composable
internal fun YieldSupplyChartContent(state: YieldSupplyChartUM, modifier: Modifier = Modifier) {
val itemModifier = modifier
.height(105.dp)
.fillMaxWidth()
when (state) {
is YieldSupplyChartUM.Loading -> YieldSupplyChartLoading(modifier = itemModifier)
is YieldSupplyChartUM.Error -> {
Column(
modifier = itemModifier,
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResourceSafe(R.string.markets_loading_error_title),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.tertiary,
)
SpacerH12()
SecondarySmallButton(
config = SmallButtonConfig(
text = resourceReference(R.string.try_to_load_data_again_button_title),
onClick = state.onRetry,
),
)
}
}
is YieldSupplyChartUM.Data -> YieldSupplyChartData(state = state, modifier = itemModifier)
}
}
@Suppress("MagicNumber")
@Composable
private fun YieldSupplyChartLoading(modifier: Modifier = Modifier) {
Box(
modifier = modifier,
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 12.dp),
) {
repeat(4) { index ->
TextShimmer(
modifier = Modifier
.size(16.dp),
radius = 4.dp,
style = TangemTheme.typography.body1,
)
Spacer(modifier = Modifier.height(6.dp))
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 24.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
repeat(5) {
TextShimmer(
modifier = Modifier
.weight(1f)
.height(16.dp),
radius = 4.dp,
style = TangemTheme.typography.body1,
)
}
}
}
CircularProgressIndicator(
modifier = Modifier
.size(TangemTheme.dimens.size16)
.align(Alignment.Center),
color = TangemTheme.colors.text.tertiary,
)
}
}
@Composable
private fun YieldSupplyChartData(state: YieldSupplyChartUM.Data, modifier: Modifier = Modifier) {
val yValues = state.chartData.y
val model = CartesianChartModel(ColumnCartesianLayerModel.build { series(yValues) })
val layer = rememberColumnCartesianLayer()
val startAxis: VerticalAxis<AxisPosition.Vertical.Start> = rememberCustomStartAxis(
label = rememberAxisLabelComponent(color = TangemTheme.colors.text.tertiary),
valueFormatter = CartesianValueFormatter { value, _, _ ->
val pct = value.toInt()
"$pct%"
},
)
val bottomAxis: HorizontalAxis<AxisPosition.Horizontal.Bottom> = rememberBottomAxis(
label = rememberAxisLabelComponent(color = TangemTheme.colors.text.tertiary),
guideline = null,
tick = null,
line = null,
)
val average = yValues.map { it.toDouble() }.average()
val referenceLine = rememberHorizontalLine(
y = { average },
line = rememberLineComponent(
color = TangemTheme.colors.text.tertiary,
thickness = TangemTheme.dimens.size1,
shape = Shape.dashed(Shape.Rectangle, TangemTheme.dimens.size4, TangemTheme.dimens.size4),
),
)
val chart = rememberCartesianChart(
layer,
startAxis = startAxis,
bottomAxis = bottomAxis,
decorations = listOf(referenceLine),
horizontalLayout = HorizontalLayout.fullWidth(),
)
CartesianChartHost(
modifier = modifier,
chart = chart,
model = model,
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),
scrollState = rememberVicoScrollState(scrollEnabled = false),
)
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun YieldSupplyChartContent_Preview(
@PreviewParameter(YieldSupplyChartPreviewProvider::class) state: YieldSupplyChartUM,
) {
TangemThemePreview {
YieldSupplyChartContent(state = state)
}
}
private class YieldSupplyChartPreviewProvider : PreviewParameterProvider<YieldSupplyChartUM> {
override val values: Sequence<YieldSupplyChartUM>
get() = sequenceOf(
YieldSupplyChartUM.Loading,
YieldSupplyChartUM.Error(onRetry = {}),
YieldSupplyChartUM.Data(chartData = YieldSupplyMarketChartData.mock()),
)
}
// endregion

View file

@ -178,7 +178,9 @@ internal class YieldSupplyModel @Inject constructor(
uiState.update { yieldSupplyUM }
(cryptoCurrency as? CryptoCurrency.Token)?.let(::loadTokenStatus)
if (yieldSupplyUM is YieldSupplyUM.Loading) {
(cryptoCurrency as? CryptoCurrency.Token)?.let(::loadTokenStatus)
}
}
private companion object {