From 00d824d302243b70f60166d16be0bd9c61ef6f55 Mon Sep 17 00:00:00 2001 From: mohannadahmed00 Date: Sat, 26 Oct 2024 14:05:52 +0300 Subject: [PATCH 1/3] CoinListAction interface has been implemented by CoinListViewModel --- .../navigation/AdaptiveCoinListDetailPane.kt | 18 ++++--------- .../coin_detail/CoinDetailScreen.kt | 22 ++++++++-------- .../presentation/coin_list/CoinListAction.kt | 4 +-- .../presentation/coin_list/CoinListEvent.kt | 4 ++- .../presentation/coin_list/CoinListScreen.kt | 25 ++++++------------- .../coin_list/CoinListViewModel.kt | 18 +++---------- .../coin_list/components/CoinListItem.kt | 8 +++--- 7 files changed, 34 insertions(+), 65 deletions(-) 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..7bdacf8f 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 @@ -16,7 +16,6 @@ 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 @@ -29,8 +28,9 @@ fun AdaptiveCoinListDetailPane( ) { 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,26 +38,18 @@ 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 - ) - } - } - } + actions = viewModel ) } }, 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..abef9d8f 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,7 +36,6 @@ 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 @@ -53,12 +52,12 @@ fun CoinDetailScreen( 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 +65,7 @@ fun CoinDetailScreen( ) { CircularProgressIndicator() } - } else if(state.selectedCoin != null) { + } else if (state.selectedCoin != null) { val coin = state.selectedCoin Column( modifier = modifier @@ -115,20 +114,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 +141,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 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..e0d7a593 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 @@ -12,32 +11,22 @@ 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.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 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 @Composable fun CoinListScreen( state: CoinListState, - onAction: (CoinListAction) -> Unit, + actions: CoinListAction, modifier: Modifier = Modifier ) { - if(state.isLoading) { + if (state.isLoading) { Box( modifier = modifier .fillMaxSize(), @@ -54,9 +43,7 @@ fun CoinListScreen( items(state.coins) { coinUi -> CoinListItem( coinUi = coinUi, - onClick = { - onAction(CoinListAction.OnCoinClick(coinUi)) - }, + onClick = actions::selectCoin, modifier = Modifier.fillMaxWidth() ) HorizontalDivider() @@ -77,7 +64,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) From c4647797a370cdacb590123e9babea4dbf1ce37e Mon Sep 17 00:00:00 2001 From: mohannadahmed00 Date: Sat, 26 Oct 2024 14:15:57 +0300 Subject: [PATCH 2/3] Hoisting has been implemented in a compose screens --- .../navigation/AdaptiveCoinListDetailPane.kt | 10 ++-------- .../coin_detail/CoinDetailScreen.kt | 17 ++++++++++++++++- .../presentation/coin_list/CoinListScreen.kt | 18 +++++++++++++++++- 3 files changed, 35 insertions(+), 10 deletions(-) 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 7bdacf8f..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,10 +9,8 @@ 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 @@ -26,7 +24,6 @@ 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 -> @@ -47,15 +44,12 @@ fun AdaptiveCoinListDetailPane( navigator = navigator, listPane = { AnimatedPane { - CoinListScreen( - state = state, - actions = viewModel - ) + 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 abef9d8f..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 @@ -39,16 +39,31 @@ import androidx.compose.ui.text.style.TextAlign 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 ) { @@ -185,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/CoinListScreen.kt b/app/src/main/java/com/plcoding/cryptotracker/crypto/presentation/coin_list/CoinListScreen.kt index e0d7a593..2aa032f0 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 @@ -11,17 +11,33 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp +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 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, actions: CoinListAction, modifier: Modifier = Modifier @@ -56,7 +72,7 @@ fun CoinListScreen( @Composable private fun CoinListScreenPreview() { CryptoTrackerTheme { - CoinListScreen( + CoinListContent( state = CoinListState( coins = (1..100).map { previewCoin.copy(id = it.toString()) From 300c7f88c23f5b9f99d697132f7804855c46a961 Mon Sep 17 00:00:00 2001 From: mohannadahmed00 Date: Sat, 26 Oct 2024 14:25:11 +0300 Subject: [PATCH 3/3] Scroll to the selected coin when returning back from the CoinDetailScreen to the CoinListScreen --- .../presentation/coin_list/CoinListScreen.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) 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 2aa032f0..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 @@ -7,11 +7,14 @@ 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.tooling.preview.PreviewLightDark @@ -21,6 +24,7 @@ import com.plcoding.cryptotracker.crypto.presentation.coin_list.components.CoinL 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.launch import org.koin.androidx.compose.koinViewModel @Composable @@ -42,6 +46,21 @@ fun CoinListContent( actions: CoinListAction, modifier: Modifier = Modifier ) { + 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 @@ -52,6 +71,7 @@ fun CoinListContent( } } else { LazyColumn( + state = lazyListState, modifier = modifier .fillMaxSize(), verticalArrangement = Arrangement.spacedBy(8.dp)