Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,11 @@ import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneScaffold
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.plcoding.cryptotracker.core.presentation.util.ObserveAsEvents
import com.plcoding.cryptotracker.core.presentation.util.toString
import com.plcoding.cryptotracker.crypto.presentation.coin_detail.CoinDetailScreen
import com.plcoding.cryptotracker.crypto.presentation.coin_list.CoinListAction
import com.plcoding.cryptotracker.crypto.presentation.coin_list.CoinListEvent
import com.plcoding.cryptotracker.crypto.presentation.coin_list.CoinListScreen
import com.plcoding.cryptotracker.crypto.presentation.coin_list.CoinListViewModel
Expand All @@ -27,43 +24,32 @@ fun AdaptiveCoinListDetailPane(
modifier: Modifier = Modifier,
viewModel: CoinListViewModel = koinViewModel()
) {
val state by viewModel.state.collectAsStateWithLifecycle()
val context = LocalContext.current
val navigator = rememberListDetailPaneScaffoldNavigator<Any>()
ObserveAsEvents(events = viewModel.events) { event ->
when(event) {
when (event) {
is CoinListEvent.Error -> {
Toast.makeText(
context,
event.error.toString(context),
Toast.LENGTH_LONG
).show()
}

is CoinListEvent.NavigateToDetails -> navigator.navigateTo(pane = ListDetailPaneScaffoldRole.Detail)
}
}

val navigator = rememberListDetailPaneScaffoldNavigator<Any>()
NavigableListDetailPaneScaffold(
navigator = navigator,
listPane = {
AnimatedPane {
CoinListScreen(
state = state,
onAction = { action ->
viewModel.onAction(action)
when(action) {
is CoinListAction.OnCoinClick -> {
navigator.navigateTo(
pane = ListDetailPaneScaffoldRole.Detail
)
}
}
}
)
CoinListScreen()
}
},
detailPane = {
AnimatedPane {
CoinDetailScreen(state = state)
CoinDetailScreen()
}
},
modifier = modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,37 +36,51 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.plcoding.cryptotracker.R
import com.plcoding.cryptotracker.crypto.presentation.coin_detail.components.InfoCard
import com.plcoding.cryptotracker.crypto.presentation.coin_list.CoinListState
import com.plcoding.cryptotracker.crypto.presentation.coin_list.CoinListViewModel
import com.plcoding.cryptotracker.crypto.presentation.coin_list.components.previewCoin
import com.plcoding.cryptotracker.crypto.presentation.models.toDisplayableNumber
import com.plcoding.cryptotracker.ui.theme.CryptoTrackerTheme
import com.plcoding.cryptotracker.ui.theme.greenBackground
import org.koin.androidx.compose.koinViewModel

@Composable
fun CoinDetailScreen(
viewModel: CoinListViewModel = koinViewModel(),
modifier: Modifier = Modifier
) {
val state by viewModel.state.collectAsStateWithLifecycle()
CoinDetailContent(
state = state,
modifier = modifier
)
}

@Composable
fun CoinDetailContent(
state: CoinListState,
modifier: Modifier = Modifier
) {
val contentColor = if(isSystemInDarkTheme()) {
val contentColor = if (isSystemInDarkTheme()) {
Color.White
} else {
Color.Black
}
if(state.isLoading) {
if (state.isLoading) {
Box(
modifier = modifier
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else if(state.selectedCoin != null) {
} else if (state.selectedCoin != null) {
val coin = state.selectedCoin
Column(
modifier = modifier
Expand Down Expand Up @@ -115,20 +129,19 @@ fun CoinDetailScreen(
(coin.priceUsd.value * (coin.changePercent24Hr.value / 100))
.toDisplayableNumber()
val isPositive = coin.changePercent24Hr.value > 0.0
val contentColor = if(isPositive) {
if(isSystemInDarkTheme()) Color.Green else greenBackground
} else {
MaterialTheme.colorScheme.error
}
InfoCard(
title = stringResource(id = R.string.change_last_24h),
formattedText = absoluteChangeFormatted.formatted,
icon = if(isPositive) {
icon = if (isPositive) {
ImageVector.vectorResource(id = R.drawable.trending)
} else {
ImageVector.vectorResource(id = R.drawable.trending_down)
},
contentColor = contentColor
contentColor = if (isPositive) {
if (isSystemInDarkTheme()) Color.Green else greenBackground
} else {
MaterialTheme.colorScheme.error
}
)
}
AnimatedVisibility(
Expand All @@ -143,7 +156,7 @@ fun CoinDetailScreen(
var totalChartWidth by remember {
mutableFloatStateOf(0f)
}
val amountOfVisibleDataPoints = if(labelWidth > 0) {
val amountOfVisibleDataPoints = if (labelWidth > 0) {
((totalChartWidth - 2.5 * labelWidth) / labelWidth).toInt()
} else {
0
Expand Down Expand Up @@ -187,7 +200,7 @@ fun CoinDetailScreen(
@Composable
private fun CoinDetailScreenPreview() {
CryptoTrackerTheme {
CoinDetailScreen(
CoinDetailContent(
state = CoinListState(
selectedCoin = previewCoin,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ package com.plcoding.cryptotracker.crypto.presentation.coin_list

import com.plcoding.cryptotracker.crypto.presentation.models.CoinUi

sealed interface CoinListAction {
data class OnCoinClick(val coinUi: CoinUi): CoinListAction
interface CoinListAction {
fun selectCoin(coinUi: CoinUi)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.plcoding.cryptotracker.crypto.presentation.coin_list

import com.plcoding.cryptotracker.core.domain.util.NetworkError
import com.plcoding.cryptotracker.crypto.presentation.models.CoinUi

sealed interface CoinListEvent {
data class Error(val error: NetworkError): CoinListEvent
data class Error(val error: NetworkError) : CoinListEvent
data class NavigateToDetails(val coinUi: CoinUi) : CoinListEvent
}
Original file line number Diff line number Diff line change
@@ -1,43 +1,67 @@
package com.plcoding.cryptotracker.crypto.presentation.coin_list

import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle
import com.plcoding.cryptotracker.core.presentation.util.toString
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.plcoding.cryptotracker.crypto.presentation.coin_list.components.CoinListItem
import com.plcoding.cryptotracker.crypto.presentation.coin_list.components.previewCoin
import com.plcoding.cryptotracker.crypto.presentation.models.CoinUi
import com.plcoding.cryptotracker.ui.theme.CryptoTrackerTheme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.withContext
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel

@Composable
fun CoinListScreen(
viewModel: CoinListViewModel = koinViewModel(),
modifier: Modifier = Modifier
) {
val state by viewModel.state.collectAsStateWithLifecycle()
CoinListContent(
state = state,
actions = viewModel,
modifier = modifier
)
}

@Composable
fun CoinListContent(
state: CoinListState,
onAction: (CoinListAction) -> Unit,
actions: CoinListAction,
modifier: Modifier = Modifier
) {
if(state.isLoading) {
val lazyListState = rememberLazyListState()
val scope = rememberCoroutineScope()
LaunchedEffect(true) {
scope.launch {
val coin = state.coins.firstOrNull {
it.id == state.selectedCoin?.id
}
coin?.let { item ->
val itemIndex = state.coins.indexOf(item)
lazyListState.scrollToItem(itemIndex)
}

}
}

if (state.isLoading) {
Box(
modifier = modifier
.fillMaxSize(),
Expand All @@ -47,16 +71,15 @@ fun CoinListScreen(
}
} else {
LazyColumn(
state = lazyListState,
modifier = modifier
.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(state.coins) { coinUi ->
CoinListItem(
coinUi = coinUi,
onClick = {
onAction(CoinListAction.OnCoinClick(coinUi))
},
onClick = actions::selectCoin,
modifier = Modifier.fillMaxWidth()
)
HorizontalDivider()
Expand All @@ -69,15 +92,17 @@ fun CoinListScreen(
@Composable
private fun CoinListScreenPreview() {
CryptoTrackerTheme {
CoinListScreen(
CoinListContent(
state = CoinListState(
coins = (1..100).map {
previewCoin.copy(id = it.toString())
}
),
modifier = Modifier
.background(MaterialTheme.colorScheme.background),
onAction = {}
actions = object : CoinListAction {
override fun selectCoin(coinUi: CoinUi) {}
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.plcoding.cryptotracker.core.domain.util.onError
import com.plcoding.cryptotracker.core.domain.util.onSuccess
import com.plcoding.cryptotracker.crypto.data.networking.RemoteCoinDataSource
import com.plcoding.cryptotracker.crypto.domain.CoinDataSource
import com.plcoding.cryptotracker.crypto.presentation.coin_detail.DataPoint
import com.plcoding.cryptotracker.crypto.presentation.models.CoinUi
import com.plcoding.cryptotracker.crypto.presentation.models.toCoinUi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
Expand All @@ -23,7 +21,7 @@ import java.time.format.DateTimeFormatter

class CoinListViewModel(
private val coinDataSource: CoinDataSource
) : ViewModel() {
) : ViewModel(), CoinListAction {

private val _state = MutableStateFlow(CoinListState())
val state = _state
Expand All @@ -37,18 +35,10 @@ class CoinListViewModel(
private val _events = Channel<CoinListEvent>()
val events = _events.receiveAsFlow()

fun onAction(action: CoinListAction) {
when (action) {
is CoinListAction.OnCoinClick -> {
selectCoin(action.coinUi)
}
}
}

private fun selectCoin(coinUi: CoinUi) {
override fun selectCoin(coinUi: CoinUi) {
_state.update { it.copy(selectedCoin = coinUi) }

viewModelScope.launch {
_events.send(CoinListEvent.NavigateToDetails(coinUi = coinUi))
coinDataSource
.getCoinHistory(
coinId = coinUi.id,
Expand Down Expand Up @@ -96,7 +86,7 @@ class CoinListViewModel(
_state.update {
it.copy(
isLoading = false,
coins = coins.map { it.toCoinUi() }
coins = coins.map { coin -> coin.toCoinUi() }
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewDynamicColors
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
Expand All @@ -33,17 +31,17 @@ import com.plcoding.cryptotracker.ui.theme.CryptoTrackerTheme
@Composable
fun CoinListItem(
coinUi: CoinUi,
onClick: () -> Unit,
onClick: (CoinUi) -> Unit,
modifier: Modifier = Modifier
) {
val contentColor = if(isSystemInDarkTheme()) {
val contentColor = if (isSystemInDarkTheme()) {
Color.White
} else {
Color.Black
}
Row(
modifier = modifier
.clickable(onClick = onClick)
.clickable(onClick = { onClick(coinUi) })
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
Expand Down