diff --git a/app/src/main/java/com/plcoding/cryptotracker/core/navigation/AdaptiveCoinListDetailPane.kt b/app/src/main/java/com/plcoding/cryptotracker/core/navigation/AdaptiveCoinListDetailPane.kt index 7f05c215..ebff0321 100644 --- a/app/src/main/java/com/plcoding/cryptotracker/core/navigation/AdaptiveCoinListDetailPane.kt +++ b/app/src/main/java/com/plcoding/cryptotracker/core/navigation/AdaptiveCoinListDetailPane.kt @@ -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 @@ -27,10 +24,10 @@ fun AdaptiveCoinListDetailPane( modifier: Modifier = Modifier, viewModel: CoinListViewModel = koinViewModel() ) { - val state by viewModel.state.collectAsStateWithLifecycle() val context = LocalContext.current + val navigator = rememberListDetailPaneScaffoldNavigator() ObserveAsEvents(events = viewModel.events) { event -> - when(event) { + when (event) { is CoinListEvent.Error -> { Toast.makeText( context, @@ -38,32 +35,21 @@ fun AdaptiveCoinListDetailPane( Toast.LENGTH_LONG ).show() } + + is CoinListEvent.NavigateToDetails -> navigator.navigateTo(pane = ListDetailPaneScaffoldRole.Detail) } } - val navigator = rememberListDetailPaneScaffoldNavigator() 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 diff --git a/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_detail/CoinDetailScreen.kt b/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_detail/CoinDetailScreen.kt index e12104d6..d87698b7 100644 --- a/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_detail/CoinDetailScreen.kt +++ b/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_detail/CoinDetailScreen.kt @@ -36,29 +36,43 @@ 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(), @@ -66,7 +80,7 @@ fun CoinDetailScreen( ) { CircularProgressIndicator() } - } else if(state.selectedCoin != null) { + } else if (state.selectedCoin != null) { val coin = state.selectedCoin Column( modifier = modifier @@ -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( @@ -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 @@ -187,7 +200,7 @@ fun CoinDetailScreen( @Composable private fun CoinDetailScreenPreview() { CryptoTrackerTheme { - CoinDetailScreen( + CoinDetailContent( state = CoinListState( selectedCoin = previewCoin, ), diff --git a/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_list/CoinListAction.kt b/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_list/CoinListAction.kt index 214c9fe5..e3831022 100644 --- a/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_list/CoinListAction.kt +++ b/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_list/CoinListAction.kt @@ -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) } \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_list/CoinListEvent.kt b/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_list/CoinListEvent.kt index 9215371c..cf960eac 100644 --- a/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_list/CoinListEvent.kt +++ b/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_list/CoinListEvent.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_list/CoinListScreen.kt b/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_list/CoinListScreen.kt index b0f4a4aa..c901cc52 100644 --- a/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_list/CoinListScreen.kt +++ b/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_list/CoinListScreen.kt @@ -1,6 +1,5 @@ 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 @@ -8,36 +7,61 @@ 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(), @@ -47,6 +71,7 @@ fun CoinListScreen( } } else { LazyColumn( + state = lazyListState, modifier = modifier .fillMaxSize(), verticalArrangement = Arrangement.spacedBy(8.dp) @@ -54,9 +79,7 @@ fun CoinListScreen( items(state.coins) { coinUi -> CoinListItem( coinUi = coinUi, - onClick = { - onAction(CoinListAction.OnCoinClick(coinUi)) - }, + onClick = actions::selectCoin, modifier = Modifier.fillMaxWidth() ) HorizontalDivider() @@ -69,7 +92,7 @@ fun CoinListScreen( @Composable private fun CoinListScreenPreview() { CryptoTrackerTheme { - CoinListScreen( + CoinListContent( state = CoinListState( coins = (1..100).map { previewCoin.copy(id = it.toString()) @@ -77,7 +100,9 @@ private fun CoinListScreenPreview() { ), modifier = Modifier .background(MaterialTheme.colorScheme.background), - onAction = {} + actions = object : CoinListAction { + override fun selectCoin(coinUi: CoinUi) {} + } ) } } \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_list/CoinListViewModel.kt b/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_list/CoinListViewModel.kt index 5a21006f..65e8f9be 100644 --- a/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_list/CoinListViewModel.kt +++ b/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_list/CoinListViewModel.kt @@ -4,7 +4,6 @@ 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 @@ -12,7 +11,6 @@ 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 @@ -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 @@ -37,18 +35,10 @@ class CoinListViewModel( private val _events = Channel() 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, @@ -96,7 +86,7 @@ class CoinListViewModel( _state.update { it.copy( isLoading = false, - coins = coins.map { it.toCoinUi() } + coins = coins.map { coin -> coin.toCoinUi() } ) } } diff --git a/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_list/components/CoinListItem.kt b/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_list/components/CoinListItem.kt index 3de02840..674c9a93 100644 --- a/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_list/components/CoinListItem.kt +++ b/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_list/components/CoinListItem.kt @@ -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 @@ -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)