From 45c8479cac6d2f3106ffe14b2f8f319bdc474cb9 Mon Sep 17 00:00:00 2001 From: Harry S Date: Mon, 23 Oct 2023 20:11:28 +0100 Subject: [PATCH 01/78] Add new navigation bar screens --- .../shorthouse/coinwatch/navigation/Screen.kt | 7 --- .../coinwatch/navigation/ScreenOld.kt | 49 +++++++++++++++++++ 2 files changed, 49 insertions(+), 7 deletions(-) delete mode 100644 app/src/main/java/dev/shorthouse/coinwatch/navigation/Screen.kt create mode 100644 app/src/main/java/dev/shorthouse/coinwatch/navigation/ScreenOld.kt diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/Screen.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/Screen.kt deleted file mode 100644 index ef8d7d53..00000000 --- a/app/src/main/java/dev/shorthouse/coinwatch/navigation/Screen.kt +++ /dev/null @@ -1,7 +0,0 @@ -package dev.shorthouse.coinwatch.navigation - -sealed class Screen(val route: String) { - object CoinList : Screen("coin_list_screen") - object CoinDetail : Screen("coin_detail_screen") - object CoinSearch : Screen("coin_search_screen") -} diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/ScreenOld.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/ScreenOld.kt new file mode 100644 index 00000000..0cebcfa9 --- /dev/null +++ b/app/src/main/java/dev/shorthouse/coinwatch/navigation/ScreenOld.kt @@ -0,0 +1,49 @@ +package dev.shorthouse.coinwatch.navigation + +import androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.InsertChart +import androidx.compose.material.icons.rounded.InsertChartOutlined +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material.icons.rounded.Star +import androidx.compose.material.icons.rounded.StarOutline +import androidx.compose.ui.graphics.vector.ImageVector +import dev.shorthouse.coinwatch.R + +sealed class ScreenOld(val route: String) { + object CoinList : ScreenOld("coin_list_screen") + object CoinDetail : ScreenOld("coin_detail_screen") + object CoinSearch : ScreenOld("coin_search_screen") +} + +sealed class NavigationBarScreen( + val route: String, + @StringRes val nameResourceId: Int, + val icon: ImageVector, + val selectedIcon: ImageVector +) { + object Market : NavigationBarScreen( + route = "market_screen", + nameResourceId = R.string.market_screen, + icon = Icons.Rounded.InsertChartOutlined, + selectedIcon = Icons.Rounded.InsertChart + ) + + object Favourites : NavigationBarScreen( + route = "favourites_screen", + nameResourceId = R.string.favourites_screen, + icon = Icons.Rounded.StarOutline, + selectedIcon = Icons.Rounded.Star + ) + + object Search : NavigationBarScreen( + route = "search_screen", + nameResourceId = R.string.search_screen, + icon = Icons.Rounded.Search, + selectedIcon = Icons.Rounded.Search + ) +} + +sealed class Screen(val route: String) { + object CoinDetail : Screen("detail_screen") +} From 796c95032aba33e638066adab6402b7f63902ac6 Mon Sep 17 00:00:00 2001 From: Harry S Date: Mon, 23 Oct 2023 20:11:38 +0100 Subject: [PATCH 02/78] Add new bottom nav screen to main activity --- app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt b/app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt index f59fa9a5..e98ed3b5 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt @@ -5,7 +5,7 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import dagger.hilt.android.AndroidEntryPoint -import dev.shorthouse.coinwatch.navigation.AppNavHost +import dev.shorthouse.coinwatch.navigation.BottomNavigationScreen import dev.shorthouse.coinwatch.ui.theme.AppTheme @AndroidEntryPoint @@ -16,7 +16,7 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { AppTheme { - AppNavHost() + BottomNavigationScreen() } } } From 1885447f379bf956954c51a27c8b4214d9efddc8 Mon Sep 17 00:00:00 2001 From: Harry S Date: Mon, 23 Oct 2023 20:11:51 +0100 Subject: [PATCH 03/78] Add new bottom navigation screen root composable --- .../navigation/BottomNavigationScreen.kt | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 app/src/main/java/dev/shorthouse/coinwatch/navigation/BottomNavigationScreen.kt diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/BottomNavigationScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/BottomNavigationScreen.kt new file mode 100644 index 00000000..45249a15 --- /dev/null +++ b/app/src/main/java/dev/shorthouse/coinwatch/navigation/BottomNavigationScreen.kt @@ -0,0 +1,118 @@ +package dev.shorthouse.coinwatch.navigation + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import dev.shorthouse.coinwatch.ui.screen.detail.CoinDetailScreen +import dev.shorthouse.coinwatch.ui.screen.favourites.FavouritesScreen +import dev.shorthouse.coinwatch.ui.screen.list.CoinListScreen +import dev.shorthouse.coinwatch.ui.screen.search.CoinSearchScreen +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun BottomNavigationScreen( + modifier: Modifier = Modifier +) { + val navController = rememberNavController() + + val navigationBarScreens = remember { + persistentListOf( + NavigationBarScreen.Market, + NavigationBarScreen.Favourites, + NavigationBarScreen.Search + ) + } + + Scaffold( + bottomBar = { + NavigationBar( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface + ) { + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + + navigationBarScreens.forEach { screen -> + val selected = currentDestination?.hierarchy?.any { + it.route == screen.route + } == true + + NavigationBarItem( + icon = { + if (selected) { + Icon( + imageVector = screen.selectedIcon, + contentDescription = null + ) + } else { + Icon( + imageVector = screen.icon, + contentDescription = null + ) + } + }, + label = { + Text(text = stringResource(screen.nameResourceId)) + }, + selected = currentDestination?.hierarchy?.any { + it.route == screen.route + } == true, + onClick = { + navController.navigate(screen.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + }, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = MaterialTheme.colorScheme.onSurface, + selectedTextColor = MaterialTheme.colorScheme.onSurface, + indicatorColor = MaterialTheme.colorScheme.background, + unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, + unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) + } + } + }, + content = { scaffoldPadding -> + NavHost( + navController = navController, + startDestination = NavigationBarScreen.Market.route, + modifier = Modifier.padding(scaffoldPadding) + ) { + composable(route = NavigationBarScreen.Market.route) { + CoinListScreen(navController = navController) + } + composable(route = NavigationBarScreen.Favourites.route) { + FavouritesScreen(navController = navController) + } + composable(route = NavigationBarScreen.Search.route) { + CoinSearchScreen(navController = navController) + } + composable(route = Screen.CoinDetail.route + "/{coinId}") { + CoinDetailScreen(navController = navController) + } + } + }, + modifier = modifier + ) +} From f70492df79b5c6456459be83b04b833da01b949d Mon Sep 17 00:00:00 2001 From: Harry S Date: Mon, 23 Oct 2023 20:12:05 +0100 Subject: [PATCH 04/78] Separate out and create favourites screen --- .../ui/screen/favourites/FavouritesScreen.kt | 160 ++++++++++++++++++ .../ui/screen/favourites/FavouritesUiState.kt | 13 ++ .../screen/favourites/FavouritesViewModel.kt | 46 +++++ 3 files changed, 219 insertions(+) create mode 100644 app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt create mode 100644 app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesUiState.kt create mode 100644 app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesViewModel.kt diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt new file mode 100644 index 00000000..da139945 --- /dev/null +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt @@ -0,0 +1,160 @@ +package dev.shorthouse.coinwatch.ui.screen.favourites + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import dev.shorthouse.coinwatch.R +import dev.shorthouse.coinwatch.model.Coin +import dev.shorthouse.coinwatch.navigation.Screen +import dev.shorthouse.coinwatch.ui.component.ErrorState +import dev.shorthouse.coinwatch.ui.previewdata.FavouritesUiStatePreviewProvider +import dev.shorthouse.coinwatch.ui.screen.list.component.CoinFavouriteItem +import dev.shorthouse.coinwatch.ui.screen.list.component.FavouriteCoinsEmptyState +import dev.shorthouse.coinwatch.ui.theme.AppTheme +import kotlinx.collections.immutable.ImmutableList + +@Composable +fun FavouritesScreen( + navController: NavController, + viewModel: FavouritesViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + FavouriteScreen( + uiState = uiState, + onCoinClick = { coin -> + navController.navigate(Screen.CoinDetail.route + "/${coin.id}") + }, + onErrorRetry = { viewModel.initialiseUiState() } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FavouriteScreen( + uiState: FavouritesUiState, + onCoinClick: (Coin) -> Unit, + onErrorRetry: () -> Unit, + modifier: Modifier = Modifier +) { + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + + when (uiState) { + is FavouritesUiState.Success -> { + Scaffold( + topBar = { + FavouritesTopBar( + scrollBehavior = scrollBehavior + ) + }, + content = { scaffoldPadding -> + FavouritesContent( + favouriteCoins = uiState.favouriteCoins, + onCoinClick = onCoinClick, + modifier = Modifier.padding(scaffoldPadding) + ) + } + ) + } + + FavouritesUiState.Loading -> { + // TODO + } + + is FavouritesUiState.Error -> { + ErrorState( + message = uiState.message, + onRetry = onErrorRetry + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FavouritesTopBar( + scrollBehavior: TopAppBarScrollBehavior, + modifier: Modifier = Modifier +) { + TopAppBar( + title = { + Text( + text = stringResource(R.string.favourites_screen), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.offset(x = (-4).dp) + ) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.background, + scrolledContainerColor = MaterialTheme.colorScheme.background + ), + scrollBehavior = scrollBehavior, + modifier = modifier + ) +} + +@Composable +fun FavouritesContent( + favouriteCoins: ImmutableList, + onCoinClick: (Coin) -> Unit, + modifier: Modifier = Modifier +) { + if (favouriteCoins.isEmpty()) { + FavouriteCoinsEmptyState( + modifier = Modifier.padding(end = 12.dp) + ) + } else { + LazyRow( + horizontalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(end = 12.dp), + modifier = modifier + ) { + items( + count = favouriteCoins.size, + key = { favouriteCoins[it].id }, + itemContent = { index -> + val favouriteCoinItem = favouriteCoins[index] + + CoinFavouriteItem( + coin = favouriteCoinItem, + onCoinClick = { onCoinClick(favouriteCoinItem) } + ) + } + ) + } + } +} + +@Composable +@Preview(showBackground = true) +private fun FavouritesScreenPreview( + @PreviewParameter(FavouritesUiStatePreviewProvider::class) uiState: FavouritesUiState +) { + AppTheme { + FavouriteScreen( + uiState = uiState, + onCoinClick = {}, + onErrorRetry = {} + ) + } +} diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesUiState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesUiState.kt new file mode 100644 index 00000000..80d4f5a8 --- /dev/null +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesUiState.kt @@ -0,0 +1,13 @@ +package dev.shorthouse.coinwatch.ui.screen.favourites + +import dev.shorthouse.coinwatch.model.Coin +import kotlinx.collections.immutable.ImmutableList + +sealed interface FavouritesUiState { + object Loading : FavouritesUiState + data class Success( + val favouriteCoins: ImmutableList + ) : FavouritesUiState + + data class Error(val message: String?) : FavouritesUiState +} diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesViewModel.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesViewModel.kt new file mode 100644 index 00000000..0abf8f4e --- /dev/null +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesViewModel.kt @@ -0,0 +1,46 @@ +package dev.shorthouse.coinwatch.ui.screen.favourites + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.shorthouse.coinwatch.common.Result +import dev.shorthouse.coinwatch.domain.GetFavouriteCoinsUseCase +import javax.inject.Inject +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update + +@HiltViewModel +class FavouritesViewModel @Inject constructor( + private val getFavouriteCoinsUseCase: GetFavouriteCoinsUseCase +) : ViewModel() { + private val _uiState = MutableStateFlow(FavouritesUiState.Loading) + val uiState = _uiState.asStateFlow() + + init { + initialiseUiState() + } + + fun initialiseUiState() { + _uiState.update { FavouritesUiState.Loading } + + val favouriteCoinsFlow = getFavouriteCoinsUseCase() + + favouriteCoinsFlow.map { favouriteCoinsResult -> + when (favouriteCoinsResult) { + is Result.Error -> { + _uiState.update { FavouritesUiState.Error(favouriteCoinsResult.message) } + } + + is Result.Success -> { + _uiState.update { + FavouritesUiState.Success(favouriteCoinsResult.data.toImmutableList()) + } + } + } + }.launchIn(viewModelScope) + } +} From 27cf8af59c1d15b01947d62f61e33cf279142459 Mon Sep 17 00:00:00 2001 From: Harry S Date: Mon, 23 Oct 2023 20:12:14 +0100 Subject: [PATCH 05/78] Add favourites screen ui state preview provider --- .../FavouritesUiStatePreviewProvider.kt | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/FavouritesUiStatePreviewProvider.kt diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/FavouritesUiStatePreviewProvider.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/FavouritesUiStatePreviewProvider.kt new file mode 100644 index 00000000..9aa45399 --- /dev/null +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/FavouritesUiStatePreviewProvider.kt @@ -0,0 +1,232 @@ +package dev.shorthouse.coinwatch.ui.previewdata + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import dev.shorthouse.coinwatch.model.Coin +import dev.shorthouse.coinwatch.model.Percentage +import dev.shorthouse.coinwatch.model.Price +import dev.shorthouse.coinwatch.ui.previewdata.FavouritesPreviewData.favouriteCoins +import dev.shorthouse.coinwatch.ui.screen.favourites.FavouritesUiState +import java.math.BigDecimal +import kotlinx.collections.immutable.persistentListOf + +class FavouritesUiStatePreviewProvider : PreviewParameterProvider { + override val values = sequenceOf( + FavouritesUiState.Success( + favouriteCoins = favouriteCoins + ), + FavouritesUiState.Success( + favouriteCoins = persistentListOf() + ), + FavouritesUiState.Loading, + FavouritesUiState.Error("No internet connection") + ) +} + +private object FavouritesPreviewData { + val favouriteCoins = persistentListOf( + Coin( + id = "bitcoin", + symbol = "BTC", + name = "Bitcoin", + imageUrl = "https://cdn.coinranking.com/bOabBYkcX/bitcoin_btc.svg", + currentPrice = Price("29446.336548759988"), + priceChangePercentage24h = Percentage("0.76833"), + prices24h = persistentListOf( + BigDecimal("29245.370873051394"), + BigDecimal("29205.501195094886"), + BigDecimal("29210.97710800848"), + BigDecimal("29183.90996906209"), + BigDecimal("29191.187134377586"), + BigDecimal("29167.309535190096"), + BigDecimal("29223.071887272858"), + BigDecimal("29307.753433422175"), + BigDecimal("29267.687825355235"), + BigDecimal("29313.499192934243"), + BigDecimal("29296.218518715148"), + BigDecimal("29276.651666477588"), + BigDecimal("29343.71801186576"), + BigDecimal("29354.73988657794"), + BigDecimal("29614.69857297837"), + BigDecimal("29473.762709346545"), + BigDecimal("29460.63779255003"), + BigDecimal("29363.672907978616"), + BigDecimal("29325.29799021886"), + BigDecimal("29370.611267446548"), + BigDecimal("29390.15178296929"), + BigDecimal("29428.222505493162"), + BigDecimal("29475.12359313808"), + BigDecimal("29471.20179209623") + ) + ), + Coin( + id = "ethereum", + symbol = "ETH", + name = "Ethereum", + imageUrl = "https://cdn.coinranking.com/rk4RKHOuW/eth.svg", + currentPrice = Price("1875.473083380222"), + priceChangePercentage24h = Percentage("-1.11008"), + prices24h = persistentListOf( + BigDecimal("1879.89804628163"), + BigDecimal("1877.1265051203513"), + BigDecimal("1874.813847463032"), + BigDecimal("1872.5227299255032"), + BigDecimal("1868.6028895583647"), + BigDecimal("1871.0393773711166"), + BigDecimal("1870.0560363427128"), + BigDecimal("1870.8836588622398"), + BigDecimal("1883.596820245353"), + BigDecimal("1872.2660234438479"), + BigDecimal("1870.3643514336038"), + BigDecimal("1857.714042675004"), + BigDecimal("1859.699110729761"), + BigDecimal("1860.670790300295"), + BigDecimal("1857.800775098814"), + BigDecimal("1858.8543780601171"), + BigDecimal("1854.9767268239643"), + BigDecimal("1850.7124615073333"), + BigDecimal("1851.8376138000401"), + BigDecimal("1850.1796229972815"), + BigDecimal("1854.8824120105778"), + BigDecimal("1853.3272421902477"), + BigDecimal("1857.8290158859397"), + BigDecimal("1859.4549720388395") + ) + ), + Coin( + id = "tether", + symbol = "USDT", + name = "Tether USD", + imageUrl = "https://cdn.coinranking.com/mgHqwlCLj/usdt.svg", + currentPrice = Price("1.00"), + priceChangePercentage24h = Percentage("0.00"), + prices24h = persistentListOf( + BigDecimal("1.00"), + BigDecimal("1.00"), + BigDecimal("1.00"), + BigDecimal("1.00"), + BigDecimal("1.00"), + BigDecimal("1.00"), + BigDecimal("1.00"), + BigDecimal("1.00"), + BigDecimal("1.00"), + BigDecimal("1.00"), + BigDecimal("1.00"), + BigDecimal("1.00"), + BigDecimal("1.00"), + BigDecimal("1.00"), + BigDecimal("1.00"), + BigDecimal("1.00"), + BigDecimal("1.00"), + BigDecimal("1.00"), + BigDecimal("1.00"), + BigDecimal("1.00"), + BigDecimal("1.00"), + BigDecimal("1.00"), + BigDecimal("1.00"), + BigDecimal("1.00") + ) + ), + Coin( + id = "binancecoin", + symbol = "BNB", + name = "BNB", + imageUrl = "https://cdn.coinranking.com/B1N19L_dZ/bnb.svg", + currentPrice = Price("242.13321783678734"), + priceChangePercentage24h = Percentage("1.84955"), + prices24h = persistentListOf( + BigDecimal("238.07237986085968"), + BigDecimal("237.59065248042927"), + BigDecimal("237.62300826740525"), + BigDecimal("237.2262878988098"), + BigDecimal("237.55818626006544"), + BigDecimal("236.80571195718406"), + BigDecimal("237.64781722479938"), + BigDecimal("238.2193416170009"), + BigDecimal("238.15348489842916"), + BigDecimal("238.20808952580057"), + BigDecimal("237.78606577278475"), + BigDecimal("237.09906305700125"), + BigDecimal("238.36365737933727"), + BigDecimal("238.5692322030582"), + BigDecimal("239.75072819043407"), + BigDecimal("239.16062125843843"), + BigDecimal("239.00025751516466"), + BigDecimal("238.94901761793733"), + BigDecimal("238.5714730594989"), + BigDecimal("239.27886677723362"), + BigDecimal("239.67490966723844"), + BigDecimal("240.13674947839255"), + BigDecimal("240.41687032176682"), + BigDecimal("241.82729323371586") + ) + ), + Coin( + id = "ripple", + symbol = "XRP", + name = "XRP", + imageUrl = "https://cdn.coinranking.com/B1oPuTyfX/xrp.svg", + currentPrice = Price("0.7142802333064954"), + priceChangePercentage24h = Percentage("1.77031"), + prices24h = persistentListOf( + BigDecimal("0.7078633715412483"), + BigDecimal("0.703154172261876"), + BigDecimal("0.6994823867542781"), + BigDecimal("0.7014706603483004"), + BigDecimal("0.69879109571246"), + BigDecimal("0.6966649080752425"), + BigDecimal("0.6975200860526335"), + BigDecimal("0.7011758683759688"), + BigDecimal("0.7021223773179766"), + BigDecimal("0.7023799603937112"), + BigDecimal("0.7044909385003845"), + BigDecimal("0.7017835251269512"), + BigDecimal("0.6995375362059472"), + BigDecimal("0.7143777711709876"), + BigDecimal("0.7125634338075278"), + BigDecimal("0.727321981146483"), + BigDecimal("0.7198675986002214"), + BigDecimal("0.7175166290060175"), + BigDecimal("0.7158774882632872"), + BigDecimal("0.7091036220562065"), + BigDecimal("0.7123303286388961"), + BigDecimal("0.7156576118999355"), + BigDecimal("0.7192302623965658"), + BigDecimal("0.7186324625859829") + ) + ), + Coin( + id = "Polkadot", + symbol = "DOT", + name = "Polkadot", + imageUrl = "https://cdn.coinranking.com/V3NSSybv-/polkadot-dot.svg", + currentPrice = Price("4.422860504529326"), + priceChangePercentage24h = Percentage("-0.44"), + prices24h = persistentListOf( + BigDecimal("4.4335207642244985"), + BigDecimal("4.419218533934902"), + BigDecimal("4.408466485673207"), + BigDecimal("4.4294324727491805"), + BigDecimal("4.413899208406151"), + BigDecimal("4.401393755728434"), + BigDecimal("4.396723632911107"), + BigDecimal("4.377061345398131"), + BigDecimal("4.3560039819830845"), + BigDecimal("4.3399040314183175"), + BigDecimal("4.353164049533105"), + BigDecimal("4.350395484668915"), + BigDecimal("4.33731487488839"), + BigDecimal("4.351328494851948"), + BigDecimal("4.411811911359132"), + BigDecimal("4.430526467556776"), + BigDecimal("4.42281998566154"), + BigDecimal("4.426950307081649"), + BigDecimal("4.414644575485274"), + BigDecimal("4.4112137336313175"), + BigDecimal("4.399984935305785"), + BigDecimal("4.413983474703376"), + BigDecimal("4.424187893749479"), + BigDecimal("4.421437665534955") + ) + ) + ) +} From f8f263027d901c554d558b437629ccb72ff7e0fd Mon Sep 17 00:00:00 2001 From: Harry S Date: Mon, 23 Oct 2023 20:12:22 +0100 Subject: [PATCH 06/78] Temporary change app nav host variables --- .../dev/shorthouse/coinwatch/navigation/AppNavHost.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt index c5f0df03..4fceaf7c 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt @@ -14,20 +14,20 @@ import dev.shorthouse.coinwatch.ui.screen.search.CoinSearchScreen fun AppNavHost( modifier: Modifier = Modifier, navController: NavHostController = rememberNavController(), - startDestination: String = Screen.CoinList.route + startDestination: String = ScreenOld.CoinList.route ) { NavHost( navController = navController, startDestination = startDestination, modifier = modifier ) { - composable(route = Screen.CoinList.route) { + composable(route = ScreenOld.CoinList.route) { CoinListScreen(navController = navController) } - composable(route = Screen.CoinDetail.route + "/{coinId}") { + composable(route = ScreenOld.CoinDetail.route + "/{coinId}") { CoinDetailScreen(navController = navController) } - composable(route = Screen.CoinSearch.route) { + composable(route = ScreenOld.CoinSearch.route) { CoinSearchScreen(navController = navController) } } From 148ae4df59aa9de362b5b737ad044acf108b9710 Mon Sep 17 00:00:00 2001 From: Harry S Date: Mon, 23 Oct 2023 20:12:32 +0100 Subject: [PATCH 07/78] Add new strings for navigation bar titles --- app/src/main/res/values/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a3980eb4..f6efed84 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,4 +51,7 @@ Try using the search function! Scroll to top Error + Market + Search + Favourites From 5b6c7717de429dce340af271e54615af3162bebd Mon Sep 17 00:00:00 2001 From: Harry S Date: Mon, 23 Oct 2023 20:12:51 +0100 Subject: [PATCH 08/78] Make search screen top level navigation screen --- .../ui/screen/search/CoinSearchScreen.kt | 36 +++++-------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt index 93a0540d..b3c464fc 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt @@ -1,6 +1,5 @@ package dev.shorthouse.coinwatch.ui.screen.search -import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -8,8 +7,8 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ArrowBack import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.Search import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -19,13 +18,9 @@ import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -36,7 +31,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import dev.shorthouse.coinwatch.R import dev.shorthouse.coinwatch.model.SearchCoin -import dev.shorthouse.coinwatch.navigation.Screen +import dev.shorthouse.coinwatch.navigation.ScreenOld import dev.shorthouse.coinwatch.ui.component.ErrorState import dev.shorthouse.coinwatch.ui.previewdata.CoinSearchUiStatePreviewProvider import dev.shorthouse.coinwatch.ui.screen.search.component.CoinSearchListItem @@ -55,10 +50,9 @@ fun CoinSearchScreen( uiState = uiState, searchQuery = viewModel.searchQuery, onSearchQueryChange = { viewModel.updateSearchQuery(it) }, - onNavigateUp = { navController.navigateUp() }, onCoinClick = { coin -> - navController.navigate(Screen.CoinDetail.route + "/${coin.id}") { - popUpTo(Screen.CoinSearch.route) { inclusive = true } + navController.navigate(ScreenOld.CoinDetail.route + "/${coin.id}") { + popUpTo(ScreenOld.CoinSearch.route) { inclusive = true } } }, onErrorRetry = { viewModel.initialiseUiState() } @@ -70,7 +64,6 @@ fun CoinSearchScreen( uiState: CoinSearchUiState, searchQuery: String, onSearchQueryChange: (String) -> Unit, - onNavigateUp: () -> Unit, onCoinClick: (SearchCoin) -> Unit, onErrorRetry: () -> Unit, modifier: Modifier = Modifier @@ -82,7 +75,6 @@ fun CoinSearchScreen( searchQuery = searchQuery, isSearchResultsEmpty = uiState.queryHasNoResults, onSearchQueryChange = onSearchQueryChange, - onNavigateUp = onNavigateUp, onCoinClick = onCoinClick, modifier = modifier ) @@ -91,8 +83,7 @@ fun CoinSearchScreen( is CoinSearchUiState.Error -> { ErrorState( message = uiState.message, - onRetry = onErrorRetry, - onNavigateUp = onNavigateUp + onRetry = onErrorRetry ) } @@ -109,12 +100,10 @@ fun CoinSearchContent( searchQuery: String, isSearchResultsEmpty: Boolean, onSearchQueryChange: (String) -> Unit, - onNavigateUp: () -> Unit, onCoinClick: (SearchCoin) -> Unit, modifier: Modifier = Modifier ) { val keyboardController = LocalSoftwareKeyboardController.current - val focusRequester = remember { FocusRequester() } SearchBar( query = searchQuery, @@ -128,9 +117,9 @@ fun CoinSearchContent( ) }, leadingIcon = { - IconButton(onClick = onNavigateUp) { + IconButton(onClick = {}) { Icon( - imageVector = Icons.Rounded.ArrowBack, + imageVector = Icons.Rounded.Search, tint = MaterialTheme.colorScheme.onSurface, contentDescription = stringResource(R.string.cd_top_bar_back) ) @@ -198,16 +187,8 @@ fun CoinSearchContent( active = true, onActiveChange = {}, tonalElevation = 0.dp, - modifier = modifier.focusRequester(focusRequester).fillMaxSize() + modifier = modifier.fillMaxSize() ) - - LaunchedEffect(focusRequester) { - focusRequester.requestFocus() - } - - BackHandler(enabled = true) { - onNavigateUp() - } } @Composable @@ -220,7 +201,6 @@ private fun CoinSearchScreenPreview( uiState = uiState, searchQuery = "", onSearchQueryChange = {}, - onNavigateUp = {}, onCoinClick = {}, onErrorRetry = {} ) From 890b740c53080bacf20d1c42a2bc9787cde82de8 Mon Sep 17 00:00:00 2001 From: Harry S Date: Mon, 23 Oct 2023 20:13:13 +0100 Subject: [PATCH 09/78] Remove search from coin list screen and separate out --- .../ui/screen/list/CoinListScreen.kt | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt index fbf28dca..e500d195 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt @@ -19,10 +19,8 @@ import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.KeyboardDoubleArrowUp -import androidx.compose.material.icons.rounded.Search import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SmallFloatingActionButton @@ -73,7 +71,6 @@ fun CoinListScreen( onCoinClick = { coin -> navController.navigate(Screen.CoinDetail.route + "/${coin.id}") }, - onNavigateSearch = { navController.navigate(Screen.CoinSearch.route) }, onErrorRetry = { viewModel.initialiseUiState() } ) } @@ -83,7 +80,6 @@ fun CoinListScreen( fun CoinListScreen( uiState: CoinListUiState, onCoinClick: (Coin) -> Unit, - onNavigateSearch: () -> Unit, onErrorRetry: () -> Unit, modifier: Modifier = Modifier ) { @@ -103,8 +99,7 @@ fun CoinListScreen( topBar = { CoinListTopBar( timeOfDay = uiState.timeOfDay, - scrollBehavior = scrollBehavior, - onNavigateSearch = onNavigateSearch + scrollBehavior = scrollBehavior ) }, content = { scaffoldPadding -> @@ -144,7 +139,7 @@ fun CoinListScreen( ) } - is CoinListUiState.Loading -> { + CoinListUiState.Loading -> { CoinListSkeletonLoader() } @@ -162,7 +157,6 @@ fun CoinListScreen( private fun CoinListTopBar( timeOfDay: TimeOfDay, scrollBehavior: TopAppBarScrollBehavior, - onNavigateSearch: () -> Unit, modifier: Modifier = Modifier ) { TopAppBar( @@ -178,16 +172,7 @@ private fun CoinListTopBar( modifier = Modifier.offset(x = (-4).dp) ) }, - actions = { - IconButton(onClick = onNavigateSearch) { - Icon( - imageVector = Icons.Rounded.Search, - tint = MaterialTheme.colorScheme.onBackground, - contentDescription = stringResource(R.string.cd_top_app_bar_search) - ) - } - }, - colors = TopAppBarDefaults.largeTopAppBarColors( + colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.background, scrolledContainerColor = MaterialTheme.colorScheme.background ), @@ -323,7 +308,6 @@ private fun CoinListScreenPreview( CoinListScreen( uiState = uiState, onCoinClick = {}, - onNavigateSearch = {}, onErrorRetry = {} ) } From b2a6f20870c09c25696e5bf05ea751d6f6d9ffd6 Mon Sep 17 00:00:00 2001 From: Harry S Date: Mon, 23 Oct 2023 20:22:07 +0100 Subject: [PATCH 10/78] Remove favourite coins logic from coin viewmodel --- .../ui/screen/list/CoinListViewModel.kt | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListViewModel.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListViewModel.kt index 04a25148..6a9d69da 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListViewModel.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListViewModel.kt @@ -6,7 +6,6 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dev.shorthouse.coinwatch.common.Result import dev.shorthouse.coinwatch.domain.GetCoinsUseCase -import dev.shorthouse.coinwatch.domain.GetFavouriteCoinsUseCase import dev.shorthouse.coinwatch.ui.model.TimeOfDay import java.time.LocalTime import javax.inject.Inject @@ -22,8 +21,7 @@ import kotlinx.coroutines.flow.update @HiltViewModel class CoinListViewModel @Inject constructor( - private val getCoinsUseCase: GetCoinsUseCase, - private val getFavouriteCoinsUseCase: GetFavouriteCoinsUseCase + private val getCoinsUseCase: GetCoinsUseCase ) : ViewModel() { private val _uiState = MutableStateFlow(CoinListUiState.Loading) val uiState = _uiState.asStateFlow() @@ -36,7 +34,6 @@ class CoinListViewModel @Inject constructor( _uiState.update { CoinListUiState.Loading } val coinsFlow = getCoinsUseCase() - val favouriteCoinsFlow = getFavouriteCoinsUseCase() val currentHourFlow = flow { emit(LocalTime.now().hour) delay(5.minutes.inWholeMilliseconds) @@ -44,27 +41,20 @@ class CoinListViewModel @Inject constructor( combine( coinsFlow, - favouriteCoinsFlow, currentHourFlow - ) { coinsResult, favouriteCoinsResult, currentHour -> - when { - coinsResult is Result.Error -> { + ) { coinsResult, currentHour -> + when (coinsResult) { + is Result.Error -> { _uiState.update { CoinListUiState.Error(coinsResult.message) } } - favouriteCoinsResult is Result.Error -> { - _uiState.update { CoinListUiState.Error(favouriteCoinsResult.message) } - } - - coinsResult is Result.Success && favouriteCoinsResult is Result.Success -> { + is Result.Success -> { val coins = coinsResult.data.toImmutableList() - val favouriteCoins = favouriteCoinsResult.data.toImmutableList() val timeOfDay = calculateTimeOfDay(currentHour) _uiState.update { CoinListUiState.Success( coins = coins, - favouriteCoins = favouriteCoins, timeOfDay = timeOfDay ) } From 53396c2bd298d2760debce49cfe39b93001f7130 Mon Sep 17 00:00:00 2001 From: Harry S Date: Mon, 23 Oct 2023 20:22:22 +0100 Subject: [PATCH 11/78] Separate out favourite coins from coin list --- .../ui/screen/list/CoinListScreen.kt | 52 +------------------ .../ui/screen/list/CoinListUiState.kt | 1 - 2 files changed, 2 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt index e500d195..8ae371ab 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape @@ -50,11 +49,9 @@ import dev.shorthouse.coinwatch.navigation.Screen import dev.shorthouse.coinwatch.ui.component.ErrorState import dev.shorthouse.coinwatch.ui.model.TimeOfDay import dev.shorthouse.coinwatch.ui.previewdata.CoinListUiStatePreviewProvider -import dev.shorthouse.coinwatch.ui.screen.list.component.CoinFavouriteItem import dev.shorthouse.coinwatch.ui.screen.list.component.CoinListItem import dev.shorthouse.coinwatch.ui.screen.list.component.CoinListSkeletonLoader import dev.shorthouse.coinwatch.ui.screen.list.component.CoinsEmptyState -import dev.shorthouse.coinwatch.ui.screen.list.component.FavouriteCoinsEmptyState import dev.shorthouse.coinwatch.ui.theme.AppTheme import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.launch @@ -105,7 +102,6 @@ fun CoinListScreen( content = { scaffoldPadding -> CoinListContent( coins = uiState.coins, - favouriteCoins = uiState.favouriteCoins, onCoinClick = onCoinClick, lazyListState = lazyListState, modifier = Modifier.padding(scaffoldPadding) @@ -184,58 +180,15 @@ private fun CoinListTopBar( @Composable private fun CoinListContent( coins: ImmutableList, - favouriteCoins: ImmutableList, onCoinClick: (Coin) -> Unit, lazyListState: LazyListState, modifier: Modifier = Modifier ) { LazyColumn( state = lazyListState, - contentPadding = PaddingValues(start = 12.dp, top = 12.dp), + contentPadding = PaddingValues(horizontal = 12.dp), modifier = modifier ) { - item { - Text( - text = stringResource(R.string.header_favourites), - style = MaterialTheme.typography.titleMedium - ) - - Spacer(Modifier.height(8.dp)) - - if (favouriteCoins.isEmpty()) { - FavouriteCoinsEmptyState( - modifier = Modifier.padding(end = 12.dp) - ) - } else { - LazyRow( - horizontalArrangement = Arrangement.spacedBy(12.dp), - contentPadding = PaddingValues(end = 12.dp) - ) { - items( - count = favouriteCoins.size, - key = { favouriteCoins[it].id }, - itemContent = { index -> - val favouriteCoinItem = favouriteCoins[index] - - CoinFavouriteItem( - coin = favouriteCoinItem, - onCoinClick = { onCoinClick(favouriteCoinItem) } - ) - } - ) - } - } - - Spacer(Modifier.height(24.dp)) - - Text( - text = stringResource(R.string.header_coins), - style = MaterialTheme.typography.titleMedium - ) - - Spacer(Modifier.height(8.dp)) - } - if (coins.isEmpty()) { item { CoinsEmptyState() @@ -264,8 +217,7 @@ private fun CoinListContent( CoinListItem( coin = coinListItem, onCoinClick = { onCoinClick(coinListItem) }, - cardShape = cardShape, - modifier = Modifier.padding(end = 12.dp) + cardShape = cardShape ) } ) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListUiState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListUiState.kt index ef426577..388f5422 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListUiState.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListUiState.kt @@ -8,7 +8,6 @@ sealed interface CoinListUiState { object Loading : CoinListUiState data class Success( val coins: ImmutableList, - val favouriteCoins: ImmutableList, val timeOfDay: TimeOfDay ) : CoinListUiState From ed331b290d77e151cb8a36769f5ef15052710853 Mon Sep 17 00:00:00 2001 From: Harry S Date: Mon, 23 Oct 2023 20:22:30 +0100 Subject: [PATCH 12/78] Separate out favourites from coin list --- .../CoinListUiStatePreviewProvider.kt | 108 ------------------ 1 file changed, 108 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinListUiStatePreviewProvider.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinListUiStatePreviewProvider.kt index 5faf6cdb..d7d0d016 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinListUiStatePreviewProvider.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinListUiStatePreviewProvider.kt @@ -6,7 +6,6 @@ import dev.shorthouse.coinwatch.model.Percentage import dev.shorthouse.coinwatch.model.Price import dev.shorthouse.coinwatch.ui.model.TimeOfDay import dev.shorthouse.coinwatch.ui.previewdata.CoinListPreviewData.coins -import dev.shorthouse.coinwatch.ui.previewdata.CoinListPreviewData.favouriteCoins import dev.shorthouse.coinwatch.ui.screen.list.CoinListUiState import java.math.BigDecimal import kotlinx.collections.immutable.persistentListOf @@ -15,12 +14,10 @@ class CoinListUiStatePreviewProvider : PreviewParameterProvider override val values = sequenceOf( CoinListUiState.Success( coins = coins, - favouriteCoins = favouriteCoins, timeOfDay = TimeOfDay.Evening ), CoinListUiState.Success( coins = persistentListOf(), - favouriteCoins = persistentListOf(), timeOfDay = TimeOfDay.Morning ), CoinListUiState.Loading, @@ -235,109 +232,4 @@ private object CoinListPreviewData { ) ) ) - - val favouriteCoins = persistentListOf( - Coin( - id = "bitcoin", - symbol = "BTC", - name = "Bitcoin", - imageUrl = "https://cdn.coinranking.com/bOabBYkcX/bitcoin_btc.svg", - currentPrice = Price("29446.336548759988"), - priceChangePercentage24h = Percentage("0.76833"), - prices24h = persistentListOf( - BigDecimal("29245.370873051394"), - BigDecimal("29205.501195094886"), - BigDecimal("29210.97710800848"), - BigDecimal("29183.90996906209"), - BigDecimal("29191.187134377586"), - BigDecimal("29167.309535190096"), - BigDecimal("29223.071887272858"), - BigDecimal("29307.753433422175"), - BigDecimal("29267.687825355235"), - BigDecimal("29313.499192934243"), - BigDecimal("29296.218518715148"), - BigDecimal("29276.651666477588"), - BigDecimal("29343.71801186576"), - BigDecimal("29354.73988657794"), - BigDecimal("29614.69857297837"), - BigDecimal("29473.762709346545"), - BigDecimal("29460.63779255003"), - BigDecimal("29363.672907978616"), - BigDecimal("29325.29799021886"), - BigDecimal("29370.611267446548"), - BigDecimal("29390.15178296929"), - BigDecimal("29428.222505493162"), - BigDecimal("29475.12359313808"), - BigDecimal("29471.20179209623") - ) - ), - Coin( - id = "ethereum", - symbol = "ETH", - name = "Ethereum", - imageUrl = "https://cdn.coinranking.com/rk4RKHOuW/eth.svg", - currentPrice = Price("1875.473083380222"), - priceChangePercentage24h = Percentage("-1.11008"), - prices24h = persistentListOf( - BigDecimal("1879.89804628163"), - BigDecimal("1877.1265051203513"), - BigDecimal("1874.813847463032"), - BigDecimal("1872.5227299255032"), - BigDecimal("1868.6028895583647"), - BigDecimal("1871.0393773711166"), - BigDecimal("1870.0560363427128"), - BigDecimal("1870.8836588622398"), - BigDecimal("1883.596820245353"), - BigDecimal("1872.2660234438479"), - BigDecimal("1870.3643514336038"), - BigDecimal("1857.714042675004"), - BigDecimal("1859.699110729761"), - BigDecimal("1860.670790300295"), - BigDecimal("1857.800775098814"), - BigDecimal("1858.8543780601171"), - BigDecimal("1854.9767268239643"), - BigDecimal("1850.7124615073333"), - BigDecimal("1851.8376138000401"), - BigDecimal("1850.1796229972815"), - BigDecimal("1854.8824120105778"), - BigDecimal("1853.3272421902477"), - BigDecimal("1857.8290158859397"), - BigDecimal("1859.4549720388395") - ) - ), - Coin( - id = "polygon", - symbol = "MATIC", - name = "Polygon", - imageUrl = "https://cdn.coinranking.com/M-pwilaq-/polygon-matic-logo.svg", - currentPrice = Price("0.5396174533730119"), - priceChangePercentage24h = Percentage("1.77031"), - prices24h = persistentListOf( - BigDecimal("0.7078633715412483"), - BigDecimal("0.703154172261876"), - BigDecimal("0.6994823867542781"), - BigDecimal("0.7014706603483004"), - BigDecimal("0.69879109571246"), - BigDecimal("0.6966649080752425"), - BigDecimal("0.6975200860526335"), - BigDecimal("0.7011758683759688"), - BigDecimal("0.7021223773179766"), - BigDecimal("0.7023799603937112"), - BigDecimal("0.7044909385003845"), - BigDecimal("0.7017835251269512"), - BigDecimal("0.6995375362059472"), - BigDecimal("0.7143777711709876"), - BigDecimal("0.7125634338075278"), - BigDecimal("0.727321981146483"), - BigDecimal("0.7198675986002214"), - BigDecimal("0.7175166290060175"), - BigDecimal("0.7158774882632872"), - BigDecimal("0.7091036220562065"), - BigDecimal("0.7123303286388961"), - BigDecimal("0.7156576118999355"), - BigDecimal("0.7192302623965658"), - BigDecimal("0.7186324625859829") - ) - ) - ) } From e44f0e58e968a854f77ee974d0412b764b7c17ee Mon Sep 17 00:00:00 2001 From: Harry S Date: Mon, 23 Oct 2023 20:22:44 +0100 Subject: [PATCH 13/78] Add scroll logic to favourites --- .../coinwatch/ui/screen/favourites/FavouritesScreen.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt index da139945..c1e1361a 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt @@ -15,6 +15,7 @@ import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -72,7 +73,8 @@ fun FavouriteScreen( onCoinClick = onCoinClick, modifier = Modifier.padding(scaffoldPadding) ) - } + }, + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection) ) } From 0f18fa524da2d54fb89fab26473d1c07b2e55795 Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Tue, 24 Oct 2023 18:43:13 +0100 Subject: [PATCH 14/78] Refactor out common empty state --- .../coinwatch/ui/component/EmptyState.kt | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 app/src/main/java/dev/shorthouse/coinwatch/ui/component/EmptyState.kt diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/component/EmptyState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/component/EmptyState.kt new file mode 100644 index 00000000..ef9f925c --- /dev/null +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/component/EmptyState.kt @@ -0,0 +1,89 @@ +package dev.shorthouse.coinwatch.ui.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import dev.shorthouse.coinwatch.R + +@Composable +fun EmptyState( + image: Painter, + title: String, + subtitle: @Composable () -> Unit, + modifier: Modifier = Modifier + // onRetry: (() -> Unit)? = null, +) { + Box( + modifier = modifier.background(MaterialTheme.colorScheme.background) + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxSize() + .padding(12.dp) + ) { + Image( + painter = image, + contentDescription = null, + modifier = Modifier.size(250.dp) + ) + + Spacer(Modifier.height(12.dp)) + + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onBackground + ) + + Spacer(Modifier.height(4.dp)) + + subtitle() + + Spacer(Modifier.height(24.dp)) + +// Button( +// onClick = onRetry, +// shape = MaterialTheme.shapes.small, +// colors = ButtonDefaults.buttonColors( +// containerColor = MaterialTheme.colorScheme.surface, +// contentColor = MaterialTheme.colorScheme.onSurface +// ) +// ) { +// Text( +// text = stringResource(R.string.button_retry), +// style = MaterialTheme.typography.titleSmall +// ) +// } + } + } +} + +@Composable +@Preview(showBackground = true) +private fun EmptyStatePreview() { + EmptyState( + image = painterResource(R.drawable.empty_state_coins), + title = "No coins", + subtitle = { + Text(text = "Please try again later") + } + ) +} From 3e2d2fe012425e4aff180235c01191bd77d8a0b4 Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Tue, 24 Oct 2023 18:43:25 +0100 Subject: [PATCH 15/78] Refactor empty state image colours to contrast background --- .../main/res/drawable/empty_state_coins.xml | 36 +++++++++---------- .../drawable/empty_state_favourite_coins.xml | 23 +++++------- .../main/res/drawable/empty_state_search.xml | 4 +-- 3 files changed, 29 insertions(+), 34 deletions(-) diff --git a/app/src/main/res/drawable/empty_state_coins.xml b/app/src/main/res/drawable/empty_state_coins.xml index 74d14e85..e0025b7f 100644 --- a/app/src/main/res/drawable/empty_state_coins.xml +++ b/app/src/main/res/drawable/empty_state_coins.xml @@ -11,13 +11,13 @@ android:fillColor="#a0616a"/> + android:fillColor="#3f3d56"/> + android:fillColor="#3f3d56"/> + android:fillColor="#3f3d56"/> @@ -26,73 +26,73 @@ android:fillColor="#a0616a"/> + android:fillColor="#253667"/> + android:fillColor="#3f3d56"/> + android:fillColor="#253667"/> + android:fillColor="#3f3d56"/> + android:fillColor="#253667"/> + android:fillColor="#253667"/> + android:fillColor="#253667"/> + android:fillColor="#3f3d56"/> + android:fillColor="#3f3d56"/> + android:fillColor="#3f3d56"/> + android:fillColor="#253667"/> + android:fillColor="#3f3d56"/> + android:fillColor="#3f3d56"/> + android:fillColor="#3f3d56"/> + android:fillColor="#3f3d56"/> diff --git a/app/src/main/res/drawable/empty_state_favourite_coins.xml b/app/src/main/res/drawable/empty_state_favourite_coins.xml index 436e3c73..2883495c 100644 --- a/app/src/main/res/drawable/empty_state_favourite_coins.xml +++ b/app/src/main/res/drawable/empty_state_favourite_coins.xml @@ -35,42 +35,37 @@ android:fillColor="#ffb8b8"/> + android:fillColor="#3f3d56"/> + android:fillColor="#3f3d56"/> + android:fillColor="#3f3d56"/> + android:fillColor="#253667"/> + android:fillColor="#253667"/> + android:fillColor="#253667"/> - + android:fillColor="#3f3d56"/> @@ -79,10 +74,10 @@ android:fillColor="#fff"/> + android:fillColor="#253667"/> + android:fillColor="#253667"/> diff --git a/app/src/main/res/drawable/empty_state_search.xml b/app/src/main/res/drawable/empty_state_search.xml index e206aa91..9fc1e873 100644 --- a/app/src/main/res/drawable/empty_state_search.xml +++ b/app/src/main/res/drawable/empty_state_search.xml @@ -47,10 +47,10 @@ android:fillColor="#97636b"/> + android:fillColor="#3f3d56"/> + android:fillColor="#3f3d56"/> From d040d3dee7f6fe5d2e3c7778db404f2049d2fdc3 Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Tue, 24 Oct 2023 18:43:42 +0100 Subject: [PATCH 16/78] Add new empty states using base composable --- .../component/FavouritesEmptyState.kt} | 51 +++++-------------- .../screen/list/component/CoinsEmptyState.kt | 45 ++++------------ .../search/component/SearchEmptyState.kt | 48 +++-------------- 3 files changed, 31 insertions(+), 113 deletions(-) rename app/src/main/java/dev/shorthouse/coinwatch/ui/screen/{list/component/FavouriteCoinsEmptyState.kt => favourites/component/FavouritesEmptyState.kt} (59%) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/FavouriteCoinsEmptyState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesEmptyState.kt similarity index 59% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/FavouriteCoinsEmptyState.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesEmptyState.kt index ce2183ab..53eee5e0 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/FavouriteCoinsEmptyState.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesEmptyState.kt @@ -1,19 +1,12 @@ -package dev.shorthouse.coinwatch.ui.screen.list.component +package dev.shorthouse.coinwatch.ui.screen.favourites.component -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.StarOutline import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -23,34 +16,15 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import dev.shorthouse.coinwatch.R +import dev.shorthouse.coinwatch.ui.component.EmptyState import dev.shorthouse.coinwatch.ui.theme.AppTheme @Composable -fun FavouriteCoinsEmptyState(modifier: Modifier = Modifier) { - Surface( - shape = MaterialTheme.shapes.medium, - modifier = modifier - .fillMaxWidth() - .height(200.dp) - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier.padding(12.dp) - ) { - Image( - painter = painterResource(R.drawable.empty_state_favourite_coins), - contentDescription = null, - modifier = Modifier.weight(1f) - ) - - Spacer(Modifier.height(16.dp)) - - Text( - text = stringResource(R.string.empty_state_favourite_coins_title), - style = MaterialTheme.typography.titleSmall - ) - +fun FavouritesEmptyState(modifier: Modifier = Modifier) { + EmptyState( + image = painterResource(R.drawable.empty_state_favourite_coins), + title = stringResource(R.string.empty_state_favourite_coins_title), + subtitle = { Row(verticalAlignment = Alignment.CenterVertically) { Text( text = stringResource(R.string.empty_state_favourite_coins_subtitle_start), @@ -73,14 +47,15 @@ fun FavouriteCoinsEmptyState(modifier: Modifier = Modifier) { color = MaterialTheme.colorScheme.onSurfaceVariant ) } - } - } + }, + modifier = modifier + ) } -@Preview @Composable -private fun FavouriteCoinsEmptyStatePreview() { +@Preview +fun FavouritesEmptyStatePreview() { AppTheme { - FavouriteCoinsEmptyState() + FavouritesEmptyState() } } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinsEmptyState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinsEmptyState.kt index 52f057da..17bac25f 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinsEmptyState.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinsEmptyState.kt @@ -1,60 +1,35 @@ package dev.shorthouse.coinwatch.ui.screen.list.component -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import dev.shorthouse.coinwatch.R +import dev.shorthouse.coinwatch.ui.component.EmptyState import dev.shorthouse.coinwatch.ui.theme.AppTheme @Composable fun CoinsEmptyState(modifier: Modifier = Modifier) { - Surface(modifier = modifier.fillMaxSize()) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier.padding(12.dp) - ) { - Image( - painter = painterResource(R.drawable.empty_state_coins), - contentDescription = null, - modifier = Modifier.size(180.dp) - ) - - Spacer(Modifier.height(16.dp)) - - Text( - text = stringResource(R.string.empty_state_coins_title), - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.onSurface - ) - + EmptyState( + image = painterResource(R.drawable.empty_state_coins), + title = stringResource(R.string.empty_state_coins_title), + subtitle = { Text( text = stringResource(R.string.empty_state_coins_subtitle), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) - } - } + }, + modifier = modifier + ) } -@Preview @Composable -private fun CoinsEmptyStatePreview() { +@Preview +fun CoinsEmptyStatePreview() { AppTheme { CoinsEmptyState() } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchEmptyState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchEmptyState.kt index a7023add..e56c24e9 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchEmptyState.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchEmptyState.kt @@ -1,64 +1,32 @@ package dev.shorthouse.coinwatch.ui.screen.search.component -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import dev.shorthouse.coinwatch.R +import dev.shorthouse.coinwatch.ui.component.EmptyState import dev.shorthouse.coinwatch.ui.theme.AppTheme @Composable fun SearchEmptyState( modifier: Modifier = Modifier ) { - Box(modifier = modifier.background(MaterialTheme.colorScheme.background)) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(0.7f) - .padding(12.dp) - ) { - Image( - painter = painterResource(R.drawable.empty_state_search), - contentDescription = null, - modifier = Modifier.size(240.dp) - ) - - Spacer(Modifier.height(12.dp)) - - Text( - text = stringResource(R.string.empty_state_search_title), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onBackground - ) - - Spacer(Modifier.height(4.dp)) - + EmptyState( + image = painterResource(R.drawable.empty_state_search), + title = stringResource(R.string.empty_state_search_title), + subtitle = { Text( text = stringResource(R.string.empty_state_search_subtitle), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) - } - } + }, + modifier = modifier + ) } @Composable From 2525252312bdef59816b38061e9d42741aae2dcd Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Tue, 24 Oct 2023 18:43:50 +0100 Subject: [PATCH 17/78] Extract out search prompt composable --- .../screen/list/component/CoinSearchPrompt.kt | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinSearchPrompt.kt diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinSearchPrompt.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinSearchPrompt.kt new file mode 100644 index 00000000..564bc2ab --- /dev/null +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinSearchPrompt.kt @@ -0,0 +1,46 @@ +package dev.shorthouse.coinwatch.ui.screen.list.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import dev.shorthouse.coinwatch.R +import dev.shorthouse.coinwatch.ui.theme.AppTheme + +@Composable +fun CoinSearchPrompt(modifier: Modifier = Modifier) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = modifier.fillMaxWidth() + ) { + Text( + text = stringResource(R.string.cant_find_coin_prompt), + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Text( + text = stringResource(R.string.search_coin_prompt), + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} + +@Composable +@Preview +fun CoinSearchPromptPreview() { + AppTheme { + CoinSearchPrompt() + } +} From 2b16a1ae24fcd58d17bf430c9774cff485666d98 Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Tue, 24 Oct 2023 18:44:01 +0100 Subject: [PATCH 18/78] Refactor tests to match new functionality --- .../ui/screen/CoinDetailScreenTest.kt | 20 ++-- .../coinwatch/ui/screen/CoinListScreenTest.kt | 107 +++--------------- .../ui/screen/CoinSearchScreenTest.kt | 64 ----------- 3 files changed, 23 insertions(+), 168 deletions(-) diff --git a/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinDetailScreenTest.kt b/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinDetailScreenTest.kt index 32aaae65..6db6babf 100644 --- a/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinDetailScreenTest.kt +++ b/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinDetailScreenTest.kt @@ -15,8 +15,8 @@ import dev.shorthouse.coinwatch.model.CoinDetail import dev.shorthouse.coinwatch.model.Percentage import dev.shorthouse.coinwatch.model.Price import dev.shorthouse.coinwatch.ui.model.ChartPeriod -import dev.shorthouse.coinwatch.ui.screen.detail.CoinDetailScreen -import dev.shorthouse.coinwatch.ui.screen.detail.CoinDetailUiState +import dev.shorthouse.coinwatch.ui.screen.details.CoinDetailScreen +import dev.shorthouse.coinwatch.ui.screen.details.DetailsUiState import dev.shorthouse.coinwatch.ui.theme.AppTheme import java.math.BigDecimal import kotlinx.collections.immutable.persistentListOf @@ -30,7 +30,7 @@ class CoinDetailScreenTest { @Test fun when_uiStateLoading_should_showSkeletonLoader() { - val uiStateLoading = CoinDetailUiState.Loading + val uiStateLoading = DetailsUiState.Loading composeTestRule.setContent { AppTheme { @@ -54,7 +54,7 @@ class CoinDetailScreenTest { @Test fun when_uiStateError_should_showErrorState() { - val uiStateError = CoinDetailUiState.Error("Error message") + val uiStateError = DetailsUiState.Error("Error message") composeTestRule.setContent { AppTheme { @@ -79,7 +79,7 @@ class CoinDetailScreenTest { @Test fun when_uiStateErrorRetryClicked_should_callOnErrorRetry() { var onErrorRetryCalled = false - val uiStateError = CoinDetailUiState.Error("Error message") + val uiStateError = DetailsUiState.Error("Error message") composeTestRule.setContent { AppTheme { @@ -103,7 +103,7 @@ class CoinDetailScreenTest { @Test fun when_uiStateErrorBackClicked_should_callOnNavigateUp() { var onNavigateUpCalled = false - val uiStateError = CoinDetailUiState.Error("Error message") + val uiStateError = DetailsUiState.Error("Error message") composeTestRule.setContent { AppTheme { @@ -126,7 +126,7 @@ class CoinDetailScreenTest { @Test fun when_uiStateSuccess_should_showExpectedContent() { - val uiStateSuccess = CoinDetailUiState.Success( + val uiStateSuccess = DetailsUiState.Success( CoinDetail( id = "ethereum", name = "Ethereum", @@ -219,7 +219,7 @@ class CoinDetailScreenTest { fun when_backClicked_should_callOnNavigateUp() { var onNavigateUpCalled = false - val uiStateSuccess = CoinDetailUiState.Success( + val uiStateSuccess = DetailsUiState.Success( CoinDetail( id = "ethereum", name = "Ethereum", @@ -274,7 +274,7 @@ class CoinDetailScreenTest { fun when_favouriteCoinClicked_should_callOnClickFavouriteCoin() { var onClickFavouriteCoinCalled = false - val uiStateSuccess = CoinDetailUiState.Success( + val uiStateSuccess = DetailsUiState.Success( CoinDetail( id = "ethereum", name = "Ethereum", @@ -331,7 +331,7 @@ class CoinDetailScreenTest { .associateWith { false } .toMutableMap() - val uiStateSuccess = CoinDetailUiState.Success( + val uiStateSuccess = DetailsUiState.Success( CoinDetail( id = "ethereum", name = "Ethereum", diff --git a/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinListScreenTest.kt b/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinListScreenTest.kt index b9e8b189..5fb1c0ae 100644 --- a/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinListScreenTest.kt +++ b/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinListScreenTest.kt @@ -34,8 +34,7 @@ class CoinListScreenTest { CoinListScreen( uiState = uiStateLoading, onCoinClick = {}, - onNavigateSearch = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -55,8 +54,7 @@ class CoinListScreenTest { CoinListScreen( uiState = uiStateError, onCoinClick = {}, - onNavigateSearch = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -79,8 +77,7 @@ class CoinListScreenTest { CoinListScreen( uiState = uiStateError, onCoinClick = {}, - onNavigateSearch = {}, - onErrorRetry = { onErrorRetryCalled = true } + onRefresh = { onErrorRetryCalled = true } ) } } @@ -96,7 +93,6 @@ class CoinListScreenTest { fun when_uiStateSuccess_should_showExpectedContent() { val uiStateSuccess = CoinListUiState.Success( coins = persistentListOf(), - favouriteCoins = persistentListOf(), timeOfDay = TimeOfDay.Morning ) @@ -105,8 +101,7 @@ class CoinListScreenTest { CoinListScreen( uiState = uiStateSuccess, onCoinClick = {}, - onNavigateSearch = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -121,7 +116,6 @@ class CoinListScreenTest { fun when_uiStateSuccess_favouriteCoinsEmpty_should_showEmptyState() { val uiStateSuccess = CoinListUiState.Success( coins = persistentListOf(), - favouriteCoins = persistentListOf(), timeOfDay = TimeOfDay.Morning ) @@ -130,8 +124,7 @@ class CoinListScreenTest { CoinListScreen( uiState = uiStateSuccess, onCoinClick = {}, - onNavigateSearch = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -148,50 +141,6 @@ class CoinListScreenTest { fun when_uiStateSuccess_favouriteCoinsList_should_showExpectedContent() { val uiStateSuccess = CoinListUiState.Success( coins = persistentListOf(), - favouriteCoins = persistentListOf( - Coin( - id = "bitcoin", - symbol = "BTC", - name = "Bitcoin", - imageUrl = "https://cdn.coinranking.com/bOabBYkcX/bitcoin_btc.svg", - currentPrice = Price("29446.336548759988"), - priceChangePercentage24h = Percentage("0.76833"), - prices24h = persistentListOf( - BigDecimal("29390.15178296929"), - BigDecimal("29428.222505493162"), - BigDecimal("29475.12359313808"), - BigDecimal("29471.20179209623") - ) - ), - Coin( - id = "ethereum", - symbol = "ETH", - name = "Ethereum", - imageUrl = "https://cdn.coinranking.com/rk4RKHOuW/eth.svg", - currentPrice = Price("1875.473083380222"), - priceChangePercentage24h = Percentage("-1.11008"), - prices24h = persistentListOf( - BigDecimal("1854.8824120105778"), - BigDecimal("1853.3272421902477"), - BigDecimal("1857.8290158859397"), - BigDecimal("1859.4549720388395") - ) - ), - Coin( - id = "tether", - symbol = "USDT", - name = "Tether", - imageUrl = "https://cdn.coinranking.com/mgHqwlCLj/usdt.svg", - currentPrice = Price("1.00"), - priceChangePercentage24h = Percentage("0.00"), - prices24h = persistentListOf( - BigDecimal("1.00"), - BigDecimal("1.00"), - BigDecimal("1.00"), - BigDecimal("1.00") - ) - ) - ), timeOfDay = TimeOfDay.Morning ) @@ -200,8 +149,7 @@ class CoinListScreenTest { CoinListScreen( uiState = uiStateSuccess, onCoinClick = {}, - onNavigateSearch = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -234,7 +182,6 @@ class CoinListScreenTest { fun when_uiStateSuccess_coinsEmpty_should_showEmptyState() { val uiStateSuccess = CoinListUiState.Success( coins = persistentListOf(), - favouriteCoins = persistentListOf(), timeOfDay = TimeOfDay.Morning ) @@ -243,8 +190,7 @@ class CoinListScreenTest { CoinListScreen( uiState = uiStateSuccess, onCoinClick = {}, - onNavigateSearch = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -302,7 +248,6 @@ class CoinListScreenTest { ) ) ), - favouriteCoins = persistentListOf(), timeOfDay = TimeOfDay.Morning ) @@ -311,8 +256,7 @@ class CoinListScreenTest { CoinListScreen( uiState = uiStateSuccess, onCoinClick = {}, - onNavigateSearch = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -359,7 +303,6 @@ class CoinListScreenTest { ) ) ), - favouriteCoins = persistentListOf(), timeOfDay = TimeOfDay.Morning ) @@ -368,8 +311,7 @@ class CoinListScreenTest { CoinListScreen( uiState = uiStateSuccess, onCoinClick = ({ onCoinClickCalled = true }), - onNavigateSearch = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -387,22 +329,6 @@ class CoinListScreenTest { val uiStateSuccess = CoinListUiState.Success( coins = persistentListOf(), - favouriteCoins = persistentListOf( - Coin( - id = "bitcoin", - symbol = "BTC", - name = "Bitcoin", - imageUrl = "https://cdn.coinranking.com/bOabBYkcX/bitcoin_btc.svg", - currentPrice = Price("29446.336548759988"), - priceChangePercentage24h = Percentage("0.76833"), - prices24h = persistentListOf( - BigDecimal("29390.15178296929"), - BigDecimal("29428.222505493162"), - BigDecimal("29475.12359313808"), - BigDecimal("29471.20179209623") - ) - ) - ), timeOfDay = TimeOfDay.Morning ) @@ -411,8 +337,7 @@ class CoinListScreenTest { CoinListScreen( uiState = uiStateSuccess, onCoinClick = ({ onCoinClickCalled = true }), - onNavigateSearch = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -428,7 +353,6 @@ class CoinListScreenTest { fun when_timeOfDayMorning_should_showMorningGreeting() { val uiStateSuccess = CoinListUiState.Success( coins = persistentListOf(), - favouriteCoins = persistentListOf(), timeOfDay = TimeOfDay.Morning ) @@ -437,8 +361,7 @@ class CoinListScreenTest { CoinListScreen( uiState = uiStateSuccess, onCoinClick = {}, - onNavigateSearch = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -452,7 +375,6 @@ class CoinListScreenTest { fun when_timeOfDayAfternoon_should_showAfternoonGreeting() { val uiStateSuccess = CoinListUiState.Success( coins = persistentListOf(), - favouriteCoins = persistentListOf(), timeOfDay = TimeOfDay.Afternoon ) @@ -461,8 +383,7 @@ class CoinListScreenTest { CoinListScreen( uiState = uiStateSuccess, onCoinClick = {}, - onNavigateSearch = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -476,7 +397,6 @@ class CoinListScreenTest { fun when_timeOfDayEvening_should_showEveningGreeting() { val uiStateSuccess = CoinListUiState.Success( coins = persistentListOf(), - favouriteCoins = persistentListOf(), timeOfDay = TimeOfDay.Evening ) @@ -485,8 +405,7 @@ class CoinListScreenTest { CoinListScreen( uiState = uiStateSuccess, onCoinClick = {}, - onNavigateSearch = {}, - onErrorRetry = {} + onRefresh = {} ) } } diff --git a/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinSearchScreenTest.kt b/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinSearchScreenTest.kt index a34f1c00..6955fc7e 100644 --- a/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinSearchScreenTest.kt +++ b/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinSearchScreenTest.kt @@ -32,7 +32,6 @@ class CoinSearchScreenTest { uiState = uiStateError, searchQuery = "", onSearchQueryChange = {}, - onNavigateUp = {}, onCoinClick = {}, onErrorRetry = {} ) @@ -58,7 +57,6 @@ class CoinSearchScreenTest { uiState = uiStateError, searchQuery = "", onSearchQueryChange = {}, - onNavigateUp = {}, onCoinClick = {}, onErrorRetry = { onErrorRetryCalled = true } ) @@ -72,31 +70,6 @@ class CoinSearchScreenTest { assertThat(onErrorRetryCalled).isTrue() } - @Test - fun when_uiStateErrorBackClicked_should_callOnNavigateUp() { - var onNavigateUpCalled = false - val uiStateError = CoinSearchUiState.Error("Error message") - - composeTestRule.setContent { - AppTheme { - CoinSearchScreen( - uiState = uiStateError, - searchQuery = "", - onSearchQueryChange = {}, - onNavigateUp = { onNavigateUpCalled = true }, - onCoinClick = {}, - onErrorRetry = {} - ) - } - } - - composeTestRule.apply { - onNodeWithContentDescription("Back").performClick() - } - - assertThat(onNavigateUpCalled).isTrue() - } - @Test fun when_uiStateSuccess_should_showExpectedContent() { val uiStateSuccess = CoinSearchUiState.Success( @@ -110,7 +83,6 @@ class CoinSearchScreenTest { uiState = uiStateSuccess, searchQuery = "", onSearchQueryChange = {}, - onNavigateUp = {}, onCoinClick = {}, onErrorRetry = {} ) @@ -123,35 +95,6 @@ class CoinSearchScreenTest { } } - @Test - fun when_backClicked_should_callOnNavigateUp() { - var onNavigateUpCalled = false - - val uiStateSuccess = CoinSearchUiState.Success( - searchResults = persistentListOf(), - queryHasNoResults = false - ) - - composeTestRule.setContent { - AppTheme { - CoinSearchScreen( - uiState = uiStateSuccess, - searchQuery = "", - onSearchQueryChange = {}, - onNavigateUp = { onNavigateUpCalled = true }, - onCoinClick = {}, - onErrorRetry = {} - ) - } - } - - composeTestRule.apply { - onNodeWithContentDescription("Back").performClick() - } - - assertThat(onNavigateUpCalled).isTrue() - } - @Test fun when_searchQueryEntered_should_displaySearchQuery() { val searchQuery = "Bitcoin" @@ -167,7 +110,6 @@ class CoinSearchScreenTest { uiState = uiStateSuccess, searchQuery = searchQuery, onSearchQueryChange = {}, - onNavigateUp = {}, onCoinClick = {}, onErrorRetry = {} ) @@ -194,7 +136,6 @@ class CoinSearchScreenTest { uiState = uiStateSuccess, searchQuery = searchQuery, onSearchQueryChange = {}, - onNavigateUp = {}, onCoinClick = {}, onErrorRetry = {} ) @@ -221,7 +162,6 @@ class CoinSearchScreenTest { uiState = uiStateSuccess, searchQuery = searchQuery.value, onSearchQueryChange = { searchQuery.value = it }, - onNavigateUp = {}, onCoinClick = {}, onErrorRetry = {} ) @@ -250,7 +190,6 @@ class CoinSearchScreenTest { uiState = uiStateSuccess, searchQuery = searchQuery.value, onSearchQueryChange = { searchQuery.value = it }, - onNavigateUp = {}, onCoinClick = {}, onErrorRetry = {} ) @@ -293,7 +232,6 @@ class CoinSearchScreenTest { uiState = uiStateSuccess, searchQuery = "", onSearchQueryChange = {}, - onNavigateUp = {}, onCoinClick = {}, onErrorRetry = {} ) @@ -332,7 +270,6 @@ class CoinSearchScreenTest { uiState = uiStateSuccess, searchQuery = "", onSearchQueryChange = {}, - onNavigateUp = {}, onCoinClick = { onCoinClickCalled = true }, onErrorRetry = {} ) @@ -359,7 +296,6 @@ class CoinSearchScreenTest { uiState = uiStateSuccess, searchQuery = "abcdefghijk", onSearchQueryChange = {}, - onNavigateUp = {}, onCoinClick = {}, onErrorRetry = {} ) From a1615326d10eb86d9bbef37830a94438691cd505 Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Tue, 24 Oct 2023 18:44:08 +0100 Subject: [PATCH 19/78] Remove old now unused screen class --- .../coinwatch/navigation/{ScreenOld.kt => Screen.kt} | 6 ------ 1 file changed, 6 deletions(-) rename app/src/main/java/dev/shorthouse/coinwatch/navigation/{ScreenOld.kt => Screen.kt} (87%) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/ScreenOld.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/Screen.kt similarity index 87% rename from app/src/main/java/dev/shorthouse/coinwatch/navigation/ScreenOld.kt rename to app/src/main/java/dev/shorthouse/coinwatch/navigation/Screen.kt index 0cebcfa9..f43391e3 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/navigation/ScreenOld.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/navigation/Screen.kt @@ -10,12 +10,6 @@ import androidx.compose.material.icons.rounded.StarOutline import androidx.compose.ui.graphics.vector.ImageVector import dev.shorthouse.coinwatch.R -sealed class ScreenOld(val route: String) { - object CoinList : ScreenOld("coin_list_screen") - object CoinDetail : ScreenOld("coin_detail_screen") - object CoinSearch : ScreenOld("coin_search_screen") -} - sealed class NavigationBarScreen( val route: String, @StringRes val nameResourceId: Int, From dfa7f3707c1a7a61035ee6ad2152d4181c08ee47 Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Tue, 24 Oct 2023 18:44:23 +0100 Subject: [PATCH 20/78] Rename detail -> details --- .../DetailsScreen.kt} | 22 +++++++++---------- .../DetailsUiState.kt} | 10 ++++----- .../DetailsViewModel.kt} | 18 +++++++-------- .../component/ChartPeriodSelector.kt | 2 +- .../component/ChartRangeLine.kt | 2 +- .../component/CoinChartCard.kt | 2 +- .../component/CoinChartRangeCard.kt | 2 +- .../component/DetailsSkeletonLoader.kt} | 10 ++++----- .../component/MarketStatsCard.kt | 2 +- 9 files changed, 35 insertions(+), 35 deletions(-) rename app/src/main/java/dev/shorthouse/coinwatch/ui/screen/{detail/CoinDetailScreen.kt => details/DetailsScreen.kt} (93%) rename app/src/main/java/dev/shorthouse/coinwatch/ui/screen/{detail/CoinDetailUiState.kt => details/DetailsUiState.kt} (60%) rename app/src/main/java/dev/shorthouse/coinwatch/ui/screen/{detail/CoinDetailViewModel.kt => details/DetailsViewModel.kt} (86%) rename app/src/main/java/dev/shorthouse/coinwatch/ui/screen/{detail => details}/component/ChartPeriodSelector.kt (98%) rename app/src/main/java/dev/shorthouse/coinwatch/ui/screen/{detail => details}/component/ChartRangeLine.kt (97%) rename app/src/main/java/dev/shorthouse/coinwatch/ui/screen/{detail => details}/component/CoinChartCard.kt (99%) rename app/src/main/java/dev/shorthouse/coinwatch/ui/screen/{detail => details}/component/CoinChartRangeCard.kt (98%) rename app/src/main/java/dev/shorthouse/coinwatch/ui/screen/{detail/component/CoinDetailSkeletonLoader.kt => details/component/DetailsSkeletonLoader.kt} (91%) rename app/src/main/java/dev/shorthouse/coinwatch/ui/screen/{detail => details}/component/MarketStatsCard.kt (98%) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/CoinDetailScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt similarity index 93% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/CoinDetailScreen.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt index 0f1f35c3..a248e0ac 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/CoinDetailScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt @@ -1,4 +1,4 @@ -package dev.shorthouse.coinwatch.ui.screen.detail +package dev.shorthouse.coinwatch.ui.screen.details import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -47,16 +47,16 @@ import dev.shorthouse.coinwatch.model.CoinDetail import dev.shorthouse.coinwatch.ui.component.ErrorState import dev.shorthouse.coinwatch.ui.model.ChartPeriod import dev.shorthouse.coinwatch.ui.previewdata.CoinDetailUiStatePreviewProvider -import dev.shorthouse.coinwatch.ui.screen.detail.component.CoinChartCard -import dev.shorthouse.coinwatch.ui.screen.detail.component.CoinChartRangeCard -import dev.shorthouse.coinwatch.ui.screen.detail.component.CoinDetailSkeletonLoader -import dev.shorthouse.coinwatch.ui.screen.detail.component.MarketStatsCard +import dev.shorthouse.coinwatch.ui.screen.details.component.CoinChartCard +import dev.shorthouse.coinwatch.ui.screen.details.component.CoinChartRangeCard +import dev.shorthouse.coinwatch.ui.screen.details.component.CoinDetailSkeletonLoader +import dev.shorthouse.coinwatch.ui.screen.details.component.MarketStatsCard import dev.shorthouse.coinwatch.ui.theme.AppTheme @Composable fun CoinDetailScreen( navController: NavController, - viewModel: CoinDetailViewModel = hiltViewModel() + viewModel: DetailsViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -72,7 +72,7 @@ fun CoinDetailScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable fun CoinDetailScreen( - uiState: CoinDetailUiState, + uiState: DetailsUiState, onNavigateUp: () -> Unit, onClickFavouriteCoin: () -> Unit, onClickChartPeriod: (ChartPeriod) -> Unit, @@ -82,7 +82,7 @@ fun CoinDetailScreen( val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() when (uiState) { - is CoinDetailUiState.Success -> { + is DetailsUiState.Success -> { Scaffold( topBar = { CoinDetailTopBar( @@ -106,11 +106,11 @@ fun CoinDetailScreen( ) } - is CoinDetailUiState.Loading -> { + is DetailsUiState.Loading -> { CoinDetailSkeletonLoader() } - is CoinDetailUiState.Error -> { + is DetailsUiState.Error -> { ErrorState( message = uiState.message, onRetry = onErrorRetry, @@ -252,7 +252,7 @@ private fun CoinDetailContent( @Composable @Preview private fun CoinDetailScreenPreview( - @PreviewParameter(CoinDetailUiStatePreviewProvider::class) uiState: CoinDetailUiState + @PreviewParameter(CoinDetailUiStatePreviewProvider::class) uiState: DetailsUiState ) { AppTheme { CoinDetailScreen( diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/CoinDetailUiState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsUiState.kt similarity index 60% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/CoinDetailUiState.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsUiState.kt index c35bc4ac..cd729255 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/CoinDetailUiState.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsUiState.kt @@ -1,17 +1,17 @@ -package dev.shorthouse.coinwatch.ui.screen.detail +package dev.shorthouse.coinwatch.ui.screen.details import dev.shorthouse.coinwatch.model.CoinChart import dev.shorthouse.coinwatch.model.CoinDetail import dev.shorthouse.coinwatch.ui.model.ChartPeriod -sealed interface CoinDetailUiState { - object Loading : CoinDetailUiState +sealed interface DetailsUiState { + object Loading : DetailsUiState data class Success( val coinDetail: CoinDetail, val coinChart: CoinChart, val chartPeriod: ChartPeriod, val isCoinFavourite: Boolean - ) : CoinDetailUiState + ) : DetailsUiState - data class Error(val message: String?) : CoinDetailUiState + data class Error(val message: String?) : DetailsUiState } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/CoinDetailViewModel.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsViewModel.kt similarity index 86% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/CoinDetailViewModel.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsViewModel.kt index 7e128949..888ee149 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/CoinDetailViewModel.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsViewModel.kt @@ -1,4 +1,4 @@ -package dev.shorthouse.coinwatch.ui.screen.detail +package dev.shorthouse.coinwatch.ui.screen.details import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel @@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @HiltViewModel -class CoinDetailViewModel @Inject constructor( +class DetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val getCoinDetailUseCase: GetCoinDetailUseCase, private val getCoinChartUseCase: GetCoinChartUseCase, @@ -33,7 +33,7 @@ class CoinDetailViewModel @Inject constructor( private val insertFavouriteCoinUseCase: InsertFavouriteCoinUseCase, private val deleteFavouriteCoinUseCase: DeleteFavouriteCoinUseCase ) : ViewModel() { - private val _uiState = MutableStateFlow(CoinDetailUiState.Loading) + private val _uiState = MutableStateFlow(DetailsUiState.Loading) val uiState = _uiState.asStateFlow() private val chartPeriodFlow = MutableStateFlow(ChartPeriod.Day) @@ -45,10 +45,10 @@ class CoinDetailViewModel @Inject constructor( @OptIn(ExperimentalCoroutinesApi::class) fun initialiseUiState() { - _uiState.update { CoinDetailUiState.Loading } + _uiState.update { DetailsUiState.Loading } if (coinId == null) { - _uiState.update { CoinDetailUiState.Error("Invalid coin ID") } + _uiState.update { DetailsUiState.Error("Invalid coin ID") } return } @@ -68,22 +68,22 @@ class CoinDetailViewModel @Inject constructor( ) { coinDetailResult, coinChartResult, isCoinFavouriteResult -> when { coinDetailResult is Result.Error -> { - _uiState.update { CoinDetailUiState.Error(coinDetailResult.message) } + _uiState.update { DetailsUiState.Error(coinDetailResult.message) } } coinChartResult is Result.Error -> { - _uiState.update { CoinDetailUiState.Error(coinChartResult.message) } + _uiState.update { DetailsUiState.Error(coinChartResult.message) } } isCoinFavouriteResult is Result.Error -> { - _uiState.update { CoinDetailUiState.Error(isCoinFavouriteResult.message) } + _uiState.update { DetailsUiState.Error(isCoinFavouriteResult.message) } } coinDetailResult is Result.Success && coinChartResult is Result.Success && isCoinFavouriteResult is Result.Success -> { _uiState.update { - CoinDetailUiState.Success( + DetailsUiState.Success( coinDetail = coinDetailResult.data, coinChart = coinChartResult.data, chartPeriod = chartPeriodFlow.value, diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/component/ChartPeriodSelector.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/ChartPeriodSelector.kt similarity index 98% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/component/ChartPeriodSelector.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/ChartPeriodSelector.kt index b566de86..d480ba2e 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/component/ChartPeriodSelector.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/ChartPeriodSelector.kt @@ -1,4 +1,4 @@ -package dev.shorthouse.coinwatch.ui.screen.detail.component +package dev.shorthouse.coinwatch.ui.screen.details.component import androidx.compose.foundation.background import androidx.compose.foundation.clickable diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/component/ChartRangeLine.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/ChartRangeLine.kt similarity index 97% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/component/ChartRangeLine.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/ChartRangeLine.kt index f07ce5f4..44fc001d 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/component/ChartRangeLine.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/ChartRangeLine.kt @@ -1,4 +1,4 @@ -package dev.shorthouse.coinwatch.ui.screen.detail.component +package dev.shorthouse.coinwatch.ui.screen.details.component import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.fillMaxWidth diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/component/CoinChartCard.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/CoinChartCard.kt similarity index 99% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/component/CoinChartCard.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/CoinChartCard.kt index f04b45c1..4f719e34 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/component/CoinChartCard.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/CoinChartCard.kt @@ -1,4 +1,4 @@ -package dev.shorthouse.coinwatch.ui.screen.detail.component +package dev.shorthouse.coinwatch.ui.screen.details.component import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/component/CoinChartRangeCard.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/CoinChartRangeCard.kt similarity index 98% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/component/CoinChartRangeCard.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/CoinChartRangeCard.kt index c3453c83..6ef5bbd2 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/component/CoinChartRangeCard.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/CoinChartRangeCard.kt @@ -1,4 +1,4 @@ -package dev.shorthouse.coinwatch.ui.screen.detail.component +package dev.shorthouse.coinwatch.ui.screen.details.component import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/component/CoinDetailSkeletonLoader.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/DetailsSkeletonLoader.kt similarity index 91% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/component/CoinDetailSkeletonLoader.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/DetailsSkeletonLoader.kt index 4ede1f76..88f81b9d 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/component/CoinDetailSkeletonLoader.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/DetailsSkeletonLoader.kt @@ -1,4 +1,4 @@ -package dev.shorthouse.coinwatch.ui.screen.detail.component +package dev.shorthouse.coinwatch.ui.screen.details.component import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -31,10 +31,10 @@ import dev.shorthouse.coinwatch.ui.theme.AppTheme fun CoinDetailSkeletonLoader(modifier: Modifier = Modifier) { Scaffold( topBar = { - SkeletonTopAppBar() + DetailsSkeletonTopBar() }, content = { scaffoldPadding -> - SkeletonContent(modifier = Modifier.padding(scaffoldPadding)) + DetailsSkeletonContent(modifier = Modifier.padding(scaffoldPadding)) }, modifier = modifier ) @@ -42,7 +42,7 @@ fun CoinDetailSkeletonLoader(modifier: Modifier = Modifier) { @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun SkeletonTopAppBar(modifier: Modifier = Modifier) { +private fun DetailsSkeletonTopBar(modifier: Modifier = Modifier) { LargeTopAppBar( navigationIcon = { IconButton(onClick = {}) { @@ -70,7 +70,7 @@ private fun SkeletonTopAppBar(modifier: Modifier = Modifier) { } @Composable -private fun SkeletonContent(modifier: Modifier = Modifier) { +private fun DetailsSkeletonContent(modifier: Modifier = Modifier) { Column(modifier = modifier.padding(horizontal = 12.dp)) { SkeletonSurface( modifier = Modifier diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/component/MarketStatsCard.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/MarketStatsCard.kt similarity index 98% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/component/MarketStatsCard.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/MarketStatsCard.kt index 6ec6d13d..da0ffbca 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/detail/component/MarketStatsCard.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/MarketStatsCard.kt @@ -1,4 +1,4 @@ -package dev.shorthouse.coinwatch.ui.screen.detail.component +package dev.shorthouse.coinwatch.ui.screen.details.component import androidx.annotation.StringRes import androidx.compose.foundation.layout.Arrangement From 5ee8d9dca7522ad31c940b527f41d82d369baaff Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Tue, 24 Oct 2023 18:44:32 +0100 Subject: [PATCH 21/78] Rename detail -> details --- .../CoinDetailViewModelTest.kt | 162 +++++++++--------- 1 file changed, 83 insertions(+), 79 deletions(-) rename app/src/test/java/dev/shorthouse/coinwatch/ui/screen/{detail => details}/CoinDetailViewModelTest.kt (65%) diff --git a/app/src/test/java/dev/shorthouse/coinwatch/ui/screen/detail/CoinDetailViewModelTest.kt b/app/src/test/java/dev/shorthouse/coinwatch/ui/screen/details/CoinDetailViewModelTest.kt similarity index 65% rename from app/src/test/java/dev/shorthouse/coinwatch/ui/screen/detail/CoinDetailViewModelTest.kt rename to app/src/test/java/dev/shorthouse/coinwatch/ui/screen/details/CoinDetailViewModelTest.kt index 8b371b6c..1c7be939 100644 --- a/app/src/test/java/dev/shorthouse/coinwatch/ui/screen/detail/CoinDetailViewModelTest.kt +++ b/app/src/test/java/dev/shorthouse/coinwatch/ui/screen/details/CoinDetailViewModelTest.kt @@ -1,4 +1,4 @@ -package dev.shorthouse.coinwatch.ui.screen.detail +package dev.shorthouse.coinwatch.ui.screen.details import androidx.lifecycle.SavedStateHandle import com.google.common.truth.Truth.assertThat @@ -36,7 +36,7 @@ class CoinDetailViewModelTest { val mainDispatcherRule = MainDispatcherRule() // Class under test - private lateinit var viewModel: CoinDetailViewModel + private lateinit var viewModel: DetailsViewModel @RelaxedMockK private lateinit var getCoinDetailUseCase: GetCoinDetailUseCase @@ -62,7 +62,7 @@ class CoinDetailViewModelTest { every { savedStateHandle.get(Constants.PARAM_COIN_ID) } returns "Qwsogvtv82FCd" - viewModel = CoinDetailViewModel( + viewModel = DetailsViewModel( savedStateHandle = savedStateHandle, getCoinDetailUseCase = getCoinDetailUseCase, getCoinChartUseCase = getCoinChartUseCase, @@ -80,7 +80,7 @@ class CoinDetailViewModelTest { @Test fun `When ViewModel is initialised should have loading UI state`() = runTest { // Arrange - val expectedUiState = CoinDetailUiState.Loading + val expectedUiState = DetailsUiState.Loading // Act @@ -93,7 +93,7 @@ class CoinDetailViewModelTest { // Arrange val coinChart = mockkClass(CoinChart::class) val errorMessage = "Coin detail error" - val expectedUiState = CoinDetailUiState.Error(errorMessage) + val expectedUiState = DetailsUiState.Error(errorMessage) every { getCoinDetailUseCase(any()) } returns flowOf(Result.Error(errorMessage)) every { getCoinChartUseCase(any(), any()) } returns flowOf(Result.Success(coinChart)) @@ -111,7 +111,7 @@ class CoinDetailViewModelTest { // Arrange val errorMessage = "Coin chart error" val coinDetail = mockkClass(CoinDetail::class) - val expectedUiState = CoinDetailUiState.Error(errorMessage) + val expectedUiState = DetailsUiState.Error(errorMessage) every { getCoinDetailUseCase(any()) } returns flowOf(Result.Success(coinDetail)) every { getCoinChartUseCase(any(), any()) } returns flowOf(Result.Error(errorMessage)) @@ -130,7 +130,7 @@ class CoinDetailViewModelTest { val errorMessage = "Coin favourite error" val coinDetail = mockkClass(CoinDetail::class) val coinChart = mockkClass(CoinChart::class) - val expectedUiState = CoinDetailUiState.Error(errorMessage) + val expectedUiState = DetailsUiState.Error(errorMessage) every { getCoinDetailUseCase(any()) } returns flowOf(Result.Success(coinDetail)) every { getCoinChartUseCase(any(), any()) } returns flowOf(Result.Success(coinChart)) @@ -150,7 +150,7 @@ class CoinDetailViewModelTest { val coinChart = mockkClass(CoinChart::class) val isCoinFavourite = false - val expectedUiState = CoinDetailUiState.Success( + val expectedUiState = DetailsUiState.Success( coinDetail = coinDetail, coinChart = coinChart, chartPeriod = ChartPeriod.Day, @@ -169,89 +169,93 @@ class CoinDetailViewModelTest { } @Test - fun `When updating chart period UI state should update with new chart period value`() = runTest { - // Arrange - val coinDetail = mockkClass(CoinDetail::class) - val coinChart = mockkClass(CoinChart::class) - val isCoinFavourite = false + fun `When updating chart period UI state should update with new chart period value`() = + runTest { + // Arrange + val coinDetail = mockkClass(CoinDetail::class) + val coinChart = mockkClass(CoinChart::class) + val isCoinFavourite = false + + val expectedUiState = DetailsUiState.Success( + coinDetail = coinDetail, + coinChart = coinChart, + chartPeriod = ChartPeriod.Week, + isCoinFavourite = isCoinFavourite + ) - val expectedUiState = CoinDetailUiState.Success( - coinDetail = coinDetail, - coinChart = coinChart, - chartPeriod = ChartPeriod.Week, - isCoinFavourite = isCoinFavourite - ) + every { getCoinDetailUseCase(any()) } returns flowOf(Result.Success(coinDetail)) + every { getCoinChartUseCase(any(), any()) } returns flowOf(Result.Success(coinChart)) + every { isCoinFavouriteUseCase(any()) } returns flowOf(Result.Success(isCoinFavourite)) - every { getCoinDetailUseCase(any()) } returns flowOf(Result.Success(coinDetail)) - every { getCoinChartUseCase(any(), any()) } returns flowOf(Result.Success(coinChart)) - every { isCoinFavouriteUseCase(any()) } returns flowOf(Result.Success(isCoinFavourite)) + // Act + viewModel.initialiseUiState() + viewModel.updateChartPeriod(ChartPeriod.Week) - // Act - viewModel.initialiseUiState() - viewModel.updateChartPeriod(ChartPeriod.Week) - - // Assert - assertThat(viewModel.uiState.value).isEqualTo(expectedUiState) - } + // Assert + assertThat(viewModel.uiState.value).isEqualTo(expectedUiState) + } @Test - fun `When toggle coin favourite returns success with un-favourited coin should favourite coin`() = runTest { - // Arrange - val coinId = "Qwsogvtv82FCd" - every { isCoinFavouriteUseCase(coinId = coinId) } returns flowOf(Result.Success(false)) - coEvery { insertFavouriteCoinUseCase(any()) } just Runs - - // Act - viewModel.toggleIsCoinFavourite() - - // Assert - coVerify { - insertFavouriteCoinUseCase( - FavouriteCoin( - id = coinId + fun `When toggle coin favourite returns success with un-favourited coin should favourite coin`() = + runTest { + // Arrange + val coinId = "Qwsogvtv82FCd" + every { isCoinFavouriteUseCase(coinId = coinId) } returns flowOf(Result.Success(false)) + coEvery { insertFavouriteCoinUseCase(any()) } just Runs + + // Act + viewModel.toggleIsCoinFavourite() + + // Assert + coVerify { + insertFavouriteCoinUseCase( + FavouriteCoin( + id = coinId + ) ) - ) + } } - } @Test - fun `When toggle coin favourite returns success with favourited coin should un-favourite coin`() = runTest { - // Arrange - val coinId = "Qwsogvtv82FCd" - every { isCoinFavouriteUseCase(coinId = coinId) } returns flowOf(Result.Success(true)) - coEvery { deleteFavouriteCoinUseCase(any()) } just Runs - - // Act - viewModel.toggleIsCoinFavourite() - - // Assert - coVerify { - deleteFavouriteCoinUseCase( - FavouriteCoin( - id = coinId + fun `When toggle coin favourite returns success with favourited coin should un-favourite coin`() = + runTest { + // Arrange + val coinId = "Qwsogvtv82FCd" + every { isCoinFavouriteUseCase(coinId = coinId) } returns flowOf(Result.Success(true)) + coEvery { deleteFavouriteCoinUseCase(any()) } just Runs + + // Act + viewModel.toggleIsCoinFavourite() + + // Assert + coVerify { + deleteFavouriteCoinUseCase( + FavouriteCoin( + id = coinId + ) ) - ) + } } - } @Test - fun `When toggle coin favourite returns error should not attempt to favourite or un-favourite coin`() = runTest { - // Arrange - every { isCoinFavouriteUseCase(any()) } returns flowOf(Result.Error("Error")) - coEvery { insertFavouriteCoinUseCase(any()) } just Runs - coEvery { deleteFavouriteCoinUseCase(any()) } just Runs - - // Act - viewModel.toggleIsCoinFavourite() - - // Assert - coVerify { - isCoinFavouriteUseCase(any()) + fun `When toggle coin favourite returns error should not attempt to favourite or un-favourite coin`() = + runTest { + // Arrange + every { isCoinFavouriteUseCase(any()) } returns flowOf(Result.Error("Error")) + coEvery { insertFavouriteCoinUseCase(any()) } just Runs + coEvery { deleteFavouriteCoinUseCase(any()) } just Runs + + // Act + viewModel.toggleIsCoinFavourite() + + // Assert + coVerify { + isCoinFavouriteUseCase(any()) + } + + coVerify(exactly = 0) { + insertFavouriteCoinUseCase(any()) + deleteFavouriteCoinUseCase(any()) + } } - - coVerify(exactly = 0) { - insertFavouriteCoinUseCase(any()) - deleteFavouriteCoinUseCase(any()) - } - } } From c03c6a04329a6acd6e019f7b57fc6f1a95fff255 Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Tue, 24 Oct 2023 18:44:38 +0100 Subject: [PATCH 22/78] Remove old unused nav host --- .../coinwatch/navigation/AppNavHost.kt | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt deleted file mode 100644 index 4fceaf7c..00000000 --- a/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt +++ /dev/null @@ -1,34 +0,0 @@ -package dev.shorthouse.coinwatch.navigation - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import dev.shorthouse.coinwatch.ui.screen.detail.CoinDetailScreen -import dev.shorthouse.coinwatch.ui.screen.list.CoinListScreen -import dev.shorthouse.coinwatch.ui.screen.search.CoinSearchScreen - -@Composable -fun AppNavHost( - modifier: Modifier = Modifier, - navController: NavHostController = rememberNavController(), - startDestination: String = ScreenOld.CoinList.route -) { - NavHost( - navController = navController, - startDestination = startDestination, - modifier = modifier - ) { - composable(route = ScreenOld.CoinList.route) { - CoinListScreen(navController = navController) - } - composable(route = ScreenOld.CoinDetail.route + "/{coinId}") { - CoinDetailScreen(navController = navController) - } - composable(route = ScreenOld.CoinSearch.route) { - CoinSearchScreen(navController = navController) - } - } -} From 0c3818469161c51c89deb5c7783186507b9a431e Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Tue, 24 Oct 2023 18:44:47 +0100 Subject: [PATCH 23/78] Add new bottom screen nav host --- .../shorthouse/coinwatch/navigation/BottomNavigationScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/BottomNavigationScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/BottomNavigationScreen.kt index 45249a15..41faef00 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/navigation/BottomNavigationScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/navigation/BottomNavigationScreen.kt @@ -19,7 +19,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController -import dev.shorthouse.coinwatch.ui.screen.detail.CoinDetailScreen +import dev.shorthouse.coinwatch.ui.screen.details.CoinDetailScreen import dev.shorthouse.coinwatch.ui.screen.favourites.FavouritesScreen import dev.shorthouse.coinwatch.ui.screen.list.CoinListScreen import dev.shorthouse.coinwatch.ui.screen.search.CoinSearchScreen From d4f69fc9b74ad7d8555443b9e259ce95bc67629b Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Tue, 24 Oct 2023 18:44:57 +0100 Subject: [PATCH 24/78] Add new coins empty state --- .../ui/screen/list/CoinListScreen.kt | 60 +++++-------------- 1 file changed, 15 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt index 8ae371ab..58801303 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt @@ -3,12 +3,7 @@ package dev.shorthouse.coinwatch.ui.screen.list import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -32,11 +27,9 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -51,6 +44,7 @@ import dev.shorthouse.coinwatch.ui.model.TimeOfDay import dev.shorthouse.coinwatch.ui.previewdata.CoinListUiStatePreviewProvider import dev.shorthouse.coinwatch.ui.screen.list.component.CoinListItem import dev.shorthouse.coinwatch.ui.screen.list.component.CoinListSkeletonLoader +import dev.shorthouse.coinwatch.ui.screen.list.component.CoinSearchPrompt import dev.shorthouse.coinwatch.ui.screen.list.component.CoinsEmptyState import dev.shorthouse.coinwatch.ui.theme.AppTheme import kotlinx.collections.immutable.ImmutableList @@ -68,7 +62,7 @@ fun CoinListScreen( onCoinClick = { coin -> navController.navigate(Screen.CoinDetail.route + "/${coin.id}") }, - onErrorRetry = { viewModel.initialiseUiState() } + onRefresh = { viewModel.initialiseUiState() } ) } @@ -77,7 +71,7 @@ fun CoinListScreen( fun CoinListScreen( uiState: CoinListUiState, onCoinClick: (Coin) -> Unit, - onErrorRetry: () -> Unit, + onRefresh: () -> Unit, modifier: Modifier = Modifier ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() @@ -142,7 +136,7 @@ fun CoinListScreen( is CoinListUiState.Error -> { ErrorState( message = uiState.message, - onRetry = onErrorRetry + onRetry = onRefresh ) } } @@ -184,16 +178,14 @@ private fun CoinListContent( lazyListState: LazyListState, modifier: Modifier = Modifier ) { - LazyColumn( - state = lazyListState, - contentPadding = PaddingValues(horizontal = 12.dp), - modifier = modifier - ) { - if (coins.isEmpty()) { - item { - CoinsEmptyState() - } - } else { + if (coins.isEmpty()) { + CoinsEmptyState() + } else { + LazyColumn( + state = lazyListState, + contentPadding = PaddingValues(horizontal = 12.dp), + modifier = modifier + ) { items( count = coins.size, key = { coins[it].id }, @@ -221,31 +213,9 @@ private fun CoinListContent( ) } ) - } - item { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxWidth() - ) { - Spacer(Modifier.height(12.dp)) - - Text( - text = stringResource(R.string.cant_find_coin_prompt), - style = MaterialTheme.typography.bodySmall, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Text( - text = stringResource(R.string.search_coin_prompt), - style = MaterialTheme.typography.bodySmall, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Spacer(Modifier.height(12.dp)) + item { + CoinSearchPrompt(modifier = Modifier.padding(vertical = 12.dp)) } } } @@ -260,7 +230,7 @@ private fun CoinListScreenPreview( CoinListScreen( uiState = uiState, onCoinClick = {}, - onErrorRetry = {} + onRefresh = {} ) } } From b9e280f4d7fd52cbef22caebb32f926f03704f68 Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Tue, 24 Oct 2023 18:45:03 +0100 Subject: [PATCH 25/78] ktlint formatting --- .../coinwatch/ui/screen/search/CoinSearchScreen.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt index b3c464fc..0409535c 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt @@ -31,7 +31,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import dev.shorthouse.coinwatch.R import dev.shorthouse.coinwatch.model.SearchCoin -import dev.shorthouse.coinwatch.navigation.ScreenOld +import dev.shorthouse.coinwatch.navigation.Screen import dev.shorthouse.coinwatch.ui.component.ErrorState import dev.shorthouse.coinwatch.ui.previewdata.CoinSearchUiStatePreviewProvider import dev.shorthouse.coinwatch.ui.screen.search.component.CoinSearchListItem @@ -51,9 +51,7 @@ fun CoinSearchScreen( searchQuery = viewModel.searchQuery, onSearchQueryChange = { viewModel.updateSearchQuery(it) }, onCoinClick = { coin -> - navController.navigate(ScreenOld.CoinDetail.route + "/${coin.id}") { - popUpTo(ScreenOld.CoinSearch.route) { inclusive = true } - } + navController.navigate(Screen.CoinDetail.route + "/${coin.id}") }, onErrorRetry = { viewModel.initialiseUiState() } ) @@ -152,6 +150,7 @@ fun CoinSearchContent( val cardShape = when { searchResults.size == 1 -> MaterialTheme.shapes.medium + index == 0 -> MaterialTheme.shapes.medium.copy( bottomStart = CornerSize(0.dp), bottomEnd = CornerSize(0.dp) From 7d0fd97127f0755864b76c593d13b7bc7c84cec4 Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Tue, 24 Oct 2023 18:45:14 +0100 Subject: [PATCH 26/78] use new empty state --- .../coinwatch/ui/screen/favourites/FavouritesScreen.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt index c1e1361a..5585d3b7 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt @@ -2,6 +2,7 @@ package dev.shorthouse.coinwatch.ui.screen.favourites import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyRow @@ -28,8 +29,8 @@ import dev.shorthouse.coinwatch.model.Coin import dev.shorthouse.coinwatch.navigation.Screen import dev.shorthouse.coinwatch.ui.component.ErrorState import dev.shorthouse.coinwatch.ui.previewdata.FavouritesUiStatePreviewProvider +import dev.shorthouse.coinwatch.ui.screen.favourites.component.FavouritesEmptyState import dev.shorthouse.coinwatch.ui.screen.list.component.CoinFavouriteItem -import dev.shorthouse.coinwatch.ui.screen.list.component.FavouriteCoinsEmptyState import dev.shorthouse.coinwatch.ui.theme.AppTheme import kotlinx.collections.immutable.ImmutableList @@ -122,9 +123,7 @@ fun FavouritesContent( modifier: Modifier = Modifier ) { if (favouriteCoins.isEmpty()) { - FavouriteCoinsEmptyState( - modifier = Modifier.padding(end = 12.dp) - ) + FavouritesEmptyState(modifier = Modifier.fillMaxSize()) } else { LazyRow( horizontalArrangement = Arrangement.spacedBy(12.dp), From 28feca30139e8472671414ff84289bb78ee2e727 Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Tue, 24 Oct 2023 18:45:27 +0100 Subject: [PATCH 27/78] Rename detail -> details --- .../ui/previewdata/CoinDetailUiStatePreviewProvider.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinDetailUiStatePreviewProvider.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinDetailUiStatePreviewProvider.kt index 21dc6331..629c5d98 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinDetailUiStatePreviewProvider.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinDetailUiStatePreviewProvider.kt @@ -6,13 +6,13 @@ import dev.shorthouse.coinwatch.model.CoinDetail import dev.shorthouse.coinwatch.model.Percentage import dev.shorthouse.coinwatch.model.Price import dev.shorthouse.coinwatch.ui.model.ChartPeriod -import dev.shorthouse.coinwatch.ui.screen.detail.CoinDetailUiState +import dev.shorthouse.coinwatch.ui.screen.details.DetailsUiState import java.math.BigDecimal import kotlinx.collections.immutable.persistentListOf -class CoinDetailUiStatePreviewProvider : PreviewParameterProvider { +class CoinDetailUiStatePreviewProvider : PreviewParameterProvider { override val values = sequenceOf( - CoinDetailUiState.Success( + DetailsUiState.Success( CoinDetail( id = "ethereum", name = "Ethereum", @@ -38,7 +38,7 @@ class CoinDetailUiStatePreviewProvider : PreviewParameterProvider Date: Tue, 24 Oct 2023 18:45:38 +0100 Subject: [PATCH 28/78] Add new empty search ui state to preview provider --- .../ui/previewdata/CoinSearchUiStatePreviewProvider.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinSearchUiStatePreviewProvider.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinSearchUiStatePreviewProvider.kt index 779a9028..d5c2404b 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinSearchUiStatePreviewProvider.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinSearchUiStatePreviewProvider.kt @@ -12,6 +12,10 @@ class CoinSearchUiStatePreviewProvider : PreviewParameterProvider Date: Tue, 24 Oct 2023 18:49:24 +0100 Subject: [PATCH 29/78] Streamline names of nav hosts --- app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt | 4 ++-- .../navigation/{BottomNavigationScreen.kt => AppNavHost.kt} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename app/src/main/java/dev/shorthouse/coinwatch/navigation/{BottomNavigationScreen.kt => AppNavHost.kt} (99%) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt b/app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt index e98ed3b5..f59fa9a5 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt @@ -5,7 +5,7 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import dagger.hilt.android.AndroidEntryPoint -import dev.shorthouse.coinwatch.navigation.BottomNavigationScreen +import dev.shorthouse.coinwatch.navigation.AppNavHost import dev.shorthouse.coinwatch.ui.theme.AppTheme @AndroidEntryPoint @@ -16,7 +16,7 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { AppTheme { - BottomNavigationScreen() + AppNavHost() } } } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/BottomNavigationScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt similarity index 99% rename from app/src/main/java/dev/shorthouse/coinwatch/navigation/BottomNavigationScreen.kt rename to app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt index 41faef00..36c47c29 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/navigation/BottomNavigationScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt @@ -26,7 +26,7 @@ import dev.shorthouse.coinwatch.ui.screen.search.CoinSearchScreen import kotlinx.collections.immutable.persistentListOf @Composable -fun BottomNavigationScreen( +fun AppNavHost( modifier: Modifier = Modifier ) { val navController = rememberNavController() From 87322b44eb7ef0c4cd7a18756f443ef5bf641ec0 Mon Sep 17 00:00:00 2001 From: Harry S Date: Tue, 24 Oct 2023 18:49:29 +0100 Subject: [PATCH 30/78] Add search skeleton loader --- .../search/component/SearchSkeletonLoader.kt | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchSkeletonLoader.kt diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchSkeletonLoader.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchSkeletonLoader.kt new file mode 100644 index 00000000..0df4bb4a --- /dev/null +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchSkeletonLoader.kt @@ -0,0 +1,63 @@ +package dev.shorthouse.coinwatch.ui.screen.search.component + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import dev.shorthouse.coinwatch.R +import dev.shorthouse.coinwatch.ui.theme.AppTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchSkeletonLoader(modifier: Modifier = Modifier) { + SearchBar( + query = "", + onQueryChange = {}, + onSearch = {}, + placeholder = { + Text( + text = stringResource(R.string.search_coins_hint), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + leadingIcon = { + IconButton(onClick = {}) { + Icon( + imageVector = Icons.Rounded.Search, + tint = MaterialTheme.colorScheme.onSurface, + contentDescription = stringResource(R.string.cd_top_bar_back) + ) + } + }, + content = {}, + colors = SearchBarDefaults.colors( + containerColor = MaterialTheme.colorScheme.background, + dividerColor = MaterialTheme.colorScheme.surface, + inputFieldColors = TextFieldDefaults.colors( + cursorColor = MaterialTheme.colorScheme.onSurface + ) + ), + active = true, + onActiveChange = {}, + modifier = modifier + ) +} + +@Composable +@Preview +fun SearchSkeletonLoaderPreview() { + AppTheme { + SearchSkeletonLoader() + } +} From 0b055c053b19da980975c445bdbbbddceddaabf5 Mon Sep 17 00:00:00 2001 From: Harry S Date: Tue, 24 Oct 2023 18:49:44 +0100 Subject: [PATCH 31/78] Change favourites from .map{} to .forEach{} --- .../coinwatch/ui/screen/favourites/FavouritesViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesViewModel.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesViewModel.kt index 0abf8f4e..29fa9231 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesViewModel.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesViewModel.kt @@ -10,7 +10,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update @HiltViewModel @@ -29,7 +29,7 @@ class FavouritesViewModel @Inject constructor( val favouriteCoinsFlow = getFavouriteCoinsUseCase() - favouriteCoinsFlow.map { favouriteCoinsResult -> + favouriteCoinsFlow.onEach { favouriteCoinsResult -> when (favouriteCoinsResult) { is Result.Error -> { _uiState.update { FavouritesUiState.Error(favouriteCoinsResult.message) } From dfbc639192225e0daa2106100206274790409d94 Mon Sep 17 00:00:00 2001 From: Harry S Date: Tue, 24 Oct 2023 18:49:56 +0100 Subject: [PATCH 32/78] Refactor favourites to use lazy grid --- .../coinwatch/ui/screen/favourites/FavouritesScreen.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt index 5585d3b7..172d7630 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt @@ -5,7 +5,8 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -125,9 +126,11 @@ fun FavouritesContent( if (favouriteCoins.isEmpty()) { FavouritesEmptyState(modifier = Modifier.fillMaxSize()) } else { - LazyRow( + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 140.dp), + contentPadding = PaddingValues(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), - contentPadding = PaddingValues(end = 12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), modifier = modifier ) { items( From 1d7d3eaaa75d3b47da37f07234cb94a46a6a9311 Mon Sep 17 00:00:00 2001 From: Harry S Date: Tue, 24 Oct 2023 18:50:05 +0100 Subject: [PATCH 33/78] Refactor favourites to use lazy grid --- .../coinwatch/ui/screen/list/component/CoinFavouriteItem.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinFavouriteItem.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinFavouriteItem.kt index 1da17034..adfd77c0 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinFavouriteItem.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinFavouriteItem.kt @@ -50,7 +50,7 @@ fun CoinFavouriteItem( Surface( shape = MaterialTheme.shapes.medium, modifier = modifier - .size(width = 140.dp, height = 200.dp) + .height(200.dp) .clickable { onCoinClick(coin) } ) { Column { From 1bff5967cf61070446b14d918d42092e1b4fdfc3 Mon Sep 17 00:00:00 2001 From: Harry S Date: Tue, 24 Oct 2023 18:50:12 +0100 Subject: [PATCH 34/78] Add favourites skeleton loader --- .../component/FavouritesSkeletonLoader.kt | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesSkeletonLoader.kt diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesSkeletonLoader.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesSkeletonLoader.kt new file mode 100644 index 00000000..fc86ac3a --- /dev/null +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesSkeletonLoader.kt @@ -0,0 +1,84 @@ +package dev.shorthouse.coinwatch.ui.screen.favourites.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import dev.shorthouse.coinwatch.R +import dev.shorthouse.coinwatch.ui.component.SkeletonSurface +import dev.shorthouse.coinwatch.ui.theme.AppTheme + +@Composable +fun FavouritesSkeletonLoader(modifier: Modifier = Modifier) { + Scaffold( + topBar = { + FavouritesSkeletonTopBar() + }, + content = { scaffoldPadding -> + FavouritesSkeletonContent(modifier = Modifier.padding(scaffoldPadding)) + }, + modifier = modifier + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FavouritesSkeletonTopBar(modifier: Modifier = Modifier) { + TopAppBar( + title = { + Text( + text = stringResource(R.string.favourites_screen), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.offset(x = (-4).dp) + ) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.background, + scrolledContainerColor = MaterialTheme.colorScheme.background + ), + modifier = modifier + ) +} + +@Composable +fun FavouritesSkeletonContent(modifier: Modifier = Modifier) { + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 140.dp), + contentPadding = PaddingValues(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + userScrollEnabled = false, + modifier = modifier + ) { + repeat(20) { + item { + SkeletonSurface( + modifier = Modifier.height(200.dp) + ) + } + } + } +} + +@Composable +@Preview +fun FavouritesSkeletonLoaderPreview() { + AppTheme { + FavouritesSkeletonLoader() + } +} From 46343c75ae025a9029c6e27863b5fedb98218cf5 Mon Sep 17 00:00:00 2001 From: Harry S Date: Tue, 24 Oct 2023 19:13:18 +0100 Subject: [PATCH 35/78] Rename detail -> details --- ...ScreenTest.kt => CoinDetailsScreenTest.kt} | 32 ++-- ...inDetailMapper.kt => CoinDetailsMapper.kt} | 38 ++-- .../repository/detail/CoinDetailRepository.kt | 9 - .../detail/CoinDetailRepositoryImpl.kt | 36 ---- .../details/CoinDetailsRepository.kt | 9 + .../details/CoinDetailsRepositoryImpl.kt | 36 ++++ .../coinwatch/data/source/remote/CoinApi.kt | 6 +- .../source/remote/CoinNetworkDataSource.kt | 4 +- .../remote/CoinNetworkDataSourceImpl.kt | 6 +- ...tailApiModel.kt => CoinDetailsApiModel.kt} | 10 +- .../shorthouse/coinwatch/di/MapperModule.kt | 6 +- .../coinwatch/di/NetworkDataModule.kt | 16 +- .../coinwatch/domain/GetCoinDetailUseCase.kt | 19 -- .../coinwatch/domain/GetCoinDetailsUseCase.kt | 19 ++ .../model/{CoinDetail.kt => CoinDetails.kt} | 2 +- .../coinwatch/navigation/AppNavHost.kt | 6 +- .../shorthouse/coinwatch/navigation/Screen.kt | 2 +- .../CoinDetailUiStatePreviewProvider.kt | 44 ----- .../CoinDetailsUiStatePreviewProvider.kt | 174 ++++++++++++++++++ .../ui/screen/details/DetailsScreen.kt | 48 ++--- .../ui/screen/details/DetailsUiState.kt | 4 +- .../ui/screen/details/DetailsViewModel.kt | 18 +- .../component/DetailsSkeletonLoader.kt | 4 +- .../details/component/MarketStatsCard.kt | 46 ++--- .../ui/screen/favourites/FavouritesScreen.kt | 2 +- .../ui/screen/list/CoinListScreen.kt | 2 +- .../ui/screen/search/CoinSearchScreen.kt | 2 +- ...MapperTest.kt => CoinDetailsMapperTest.kt} | 90 ++++----- .../data/source/remote/FakeCoinApi.kt | 20 +- .../remote/FakeCoinNetworkDataSource.kt | 6 +- ...delTest.kt => CoinDetailsViewModelTest.kt} | 36 ++-- 31 files changed, 441 insertions(+), 311 deletions(-) rename app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/{CoinDetailScreenTest.kt => CoinDetailsScreenTest.kt} (95%) rename app/src/main/java/dev/shorthouse/coinwatch/data/mapper/{CoinDetailMapper.kt => CoinDetailsMapper.kt} (53%) delete mode 100644 app/src/main/java/dev/shorthouse/coinwatch/data/repository/detail/CoinDetailRepository.kt delete mode 100644 app/src/main/java/dev/shorthouse/coinwatch/data/repository/detail/CoinDetailRepositoryImpl.kt create mode 100644 app/src/main/java/dev/shorthouse/coinwatch/data/repository/details/CoinDetailsRepository.kt create mode 100644 app/src/main/java/dev/shorthouse/coinwatch/data/repository/details/CoinDetailsRepositoryImpl.kt rename app/src/main/java/dev/shorthouse/coinwatch/data/source/remote/model/{CoinDetailApiModel.kt => CoinDetailsApiModel.kt} (84%) delete mode 100644 app/src/main/java/dev/shorthouse/coinwatch/domain/GetCoinDetailUseCase.kt create mode 100644 app/src/main/java/dev/shorthouse/coinwatch/domain/GetCoinDetailsUseCase.kt rename app/src/main/java/dev/shorthouse/coinwatch/model/{CoinDetail.kt => CoinDetails.kt} (93%) delete mode 100644 app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinDetailUiStatePreviewProvider.kt create mode 100644 app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinDetailsUiStatePreviewProvider.kt rename app/src/test/java/dev/shorthouse/coinwatch/data/mapper/{CoinDetailMapperTest.kt => CoinDetailsMapperTest.kt} (72%) rename app/src/test/java/dev/shorthouse/coinwatch/ui/screen/details/{CoinDetailViewModelTest.kt => CoinDetailsViewModelTest.kt} (87%) diff --git a/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinDetailScreenTest.kt b/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinDetailsScreenTest.kt similarity index 95% rename from app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinDetailScreenTest.kt rename to app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinDetailsScreenTest.kt index 6db6babf..837389dc 100644 --- a/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinDetailScreenTest.kt +++ b/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinDetailsScreenTest.kt @@ -11,11 +11,11 @@ import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeUp import com.google.common.truth.Truth.assertThat import dev.shorthouse.coinwatch.model.CoinChart -import dev.shorthouse.coinwatch.model.CoinDetail +import dev.shorthouse.coinwatch.model.CoinDetails import dev.shorthouse.coinwatch.model.Percentage import dev.shorthouse.coinwatch.model.Price import dev.shorthouse.coinwatch.ui.model.ChartPeriod -import dev.shorthouse.coinwatch.ui.screen.details.CoinDetailScreen +import dev.shorthouse.coinwatch.ui.screen.details.CoinDetailsScreen import dev.shorthouse.coinwatch.ui.screen.details.DetailsUiState import dev.shorthouse.coinwatch.ui.theme.AppTheme import java.math.BigDecimal @@ -23,7 +23,7 @@ import kotlinx.collections.immutable.persistentListOf import org.junit.Rule import org.junit.Test -class CoinDetailScreenTest { +class CoinDetailsScreenTest { @get:Rule val composeTestRule = createComposeRule() @@ -34,7 +34,7 @@ class CoinDetailScreenTest { composeTestRule.setContent { AppTheme { - CoinDetailScreen( + CoinDetailsScreen( uiState = uiStateLoading, onNavigateUp = {}, onClickFavouriteCoin = {}, @@ -58,7 +58,7 @@ class CoinDetailScreenTest { composeTestRule.setContent { AppTheme { - CoinDetailScreen( + CoinDetailsScreen( uiState = uiStateError, onNavigateUp = {}, onClickFavouriteCoin = {}, @@ -83,7 +83,7 @@ class CoinDetailScreenTest { composeTestRule.setContent { AppTheme { - CoinDetailScreen( + CoinDetailsScreen( uiState = uiStateError, onNavigateUp = {}, onClickFavouriteCoin = {}, @@ -107,7 +107,7 @@ class CoinDetailScreenTest { composeTestRule.setContent { AppTheme { - CoinDetailScreen( + CoinDetailsScreen( uiState = uiStateError, onNavigateUp = { onNavigateUpCalled = true }, onClickFavouriteCoin = {}, @@ -127,7 +127,7 @@ class CoinDetailScreenTest { @Test fun when_uiStateSuccess_should_showExpectedContent() { val uiStateSuccess = DetailsUiState.Success( - CoinDetail( + CoinDetails( id = "ethereum", name = "Ethereum", symbol = "ETH", @@ -160,7 +160,7 @@ class CoinDetailScreenTest { composeTestRule.setContent { AppTheme { - CoinDetailScreen( + CoinDetailsScreen( uiState = uiStateSuccess, onNavigateUp = {}, onClickFavouriteCoin = {}, @@ -194,7 +194,7 @@ class CoinDetailScreenTest { onNodeWithText("High").assertIsDisplayed() onNodeWithText("$1,922.83").assertIsDisplayed() - onNodeWithTag("coin_detail_content") + onNodeWithTag("coin_details_content") .performTouchInput { swipeUp(durationMillis = 500) } onNodeWithText("Market Stats").assertIsDisplayed() @@ -220,7 +220,7 @@ class CoinDetailScreenTest { var onNavigateUpCalled = false val uiStateSuccess = DetailsUiState.Success( - CoinDetail( + CoinDetails( id = "ethereum", name = "Ethereum", symbol = "ETH", @@ -253,7 +253,7 @@ class CoinDetailScreenTest { composeTestRule.setContent { AppTheme { - CoinDetailScreen( + CoinDetailsScreen( uiState = uiStateSuccess, onNavigateUp = { onNavigateUpCalled = true }, onClickFavouriteCoin = {}, @@ -275,7 +275,7 @@ class CoinDetailScreenTest { var onClickFavouriteCoinCalled = false val uiStateSuccess = DetailsUiState.Success( - CoinDetail( + CoinDetails( id = "ethereum", name = "Ethereum", symbol = "ETH", @@ -308,7 +308,7 @@ class CoinDetailScreenTest { composeTestRule.setContent { AppTheme { - CoinDetailScreen( + CoinDetailsScreen( uiState = uiStateSuccess, onNavigateUp = {}, onClickFavouriteCoin = { onClickFavouriteCoinCalled = true }, @@ -332,7 +332,7 @@ class CoinDetailScreenTest { .toMutableMap() val uiStateSuccess = DetailsUiState.Success( - CoinDetail( + CoinDetails( id = "ethereum", name = "Ethereum", symbol = "ETH", @@ -365,7 +365,7 @@ class CoinDetailScreenTest { composeTestRule.setContent { AppTheme { - CoinDetailScreen( + CoinDetailsScreen( uiState = uiStateSuccess, onNavigateUp = {}, onClickFavouriteCoin = {}, diff --git a/app/src/main/java/dev/shorthouse/coinwatch/data/mapper/CoinDetailMapper.kt b/app/src/main/java/dev/shorthouse/coinwatch/data/mapper/CoinDetailsMapper.kt similarity index 53% rename from app/src/main/java/dev/shorthouse/coinwatch/data/mapper/CoinDetailMapper.kt rename to app/src/main/java/dev/shorthouse/coinwatch/data/mapper/CoinDetailsMapper.kt index b6a34312..040771d2 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/data/mapper/CoinDetailMapper.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/data/mapper/CoinDetailsMapper.kt @@ -1,8 +1,8 @@ package dev.shorthouse.coinwatch.data.mapper import dev.shorthouse.coinwatch.common.Mapper -import dev.shorthouse.coinwatch.data.source.remote.model.CoinDetailApiModel -import dev.shorthouse.coinwatch.model.CoinDetail +import dev.shorthouse.coinwatch.data.source.remote.model.CoinDetailsApiModel +import dev.shorthouse.coinwatch.model.CoinDetails import dev.shorthouse.coinwatch.model.Price import java.text.NumberFormat import java.time.DateTimeException @@ -12,7 +12,7 @@ import java.time.format.DateTimeFormatter import java.util.Locale import javax.inject.Inject -class CoinDetailMapper @Inject constructor() : Mapper { +class CoinDetailsMapper @Inject constructor() : Mapper { companion object { private val dateFormatter = DateTimeFormatter.ofPattern("d MMM yyyy", Locale.US) @@ -21,22 +21,22 @@ class CoinDetailMapper @Inject constructor() : Mapper> -} diff --git a/app/src/main/java/dev/shorthouse/coinwatch/data/repository/detail/CoinDetailRepositoryImpl.kt b/app/src/main/java/dev/shorthouse/coinwatch/data/repository/detail/CoinDetailRepositoryImpl.kt deleted file mode 100644 index 40675bd3..00000000 --- a/app/src/main/java/dev/shorthouse/coinwatch/data/repository/detail/CoinDetailRepositoryImpl.kt +++ /dev/null @@ -1,36 +0,0 @@ -package dev.shorthouse.coinwatch.data.repository.detail - -import dev.shorthouse.coinwatch.common.Result -import dev.shorthouse.coinwatch.data.mapper.CoinDetailMapper -import dev.shorthouse.coinwatch.data.source.remote.CoinNetworkDataSourceImpl -import dev.shorthouse.coinwatch.di.IoDispatcher -import dev.shorthouse.coinwatch.model.CoinDetail -import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn -import timber.log.Timber - -class CoinDetailRepositoryImpl @Inject constructor( - private val coinNetworkDataSource: CoinNetworkDataSourceImpl, - private val coinDetailMapper: CoinDetailMapper, - @IoDispatcher private val ioDispatcher: CoroutineDispatcher -) : CoinDetailRepository { - override fun getCoinDetail(coinId: String): Flow> = flow { - val response = coinNetworkDataSource.getCoinDetail(coinId = coinId) - val body = response.body() - - if (response.isSuccessful && body?.coinDetailDataHolder?.coinDetailData != null) { - val coinDetail = coinDetailMapper.mapApiModelToModel(body) - emit(Result.Success(coinDetail)) - } else { - Timber.e("getCoinDetail unsuccessful retrofit response ${response.message()}") - emit(Result.Error("Unable to fetch coin details")) - } - }.catch { e -> - Timber.e("getCoinDetail exception ${e.message}") - emit(Result.Error("Unable to fetch coin details")) - }.flowOn(ioDispatcher) -} diff --git a/app/src/main/java/dev/shorthouse/coinwatch/data/repository/details/CoinDetailsRepository.kt b/app/src/main/java/dev/shorthouse/coinwatch/data/repository/details/CoinDetailsRepository.kt new file mode 100644 index 00000000..3975ff42 --- /dev/null +++ b/app/src/main/java/dev/shorthouse/coinwatch/data/repository/details/CoinDetailsRepository.kt @@ -0,0 +1,9 @@ +package dev.shorthouse.coinwatch.data.repository.details + +import dev.shorthouse.coinwatch.common.Result +import dev.shorthouse.coinwatch.model.CoinDetails +import kotlinx.coroutines.flow.Flow + +interface CoinDetailsRepository { + fun getCoinDetails(coinId: String): Flow> +} diff --git a/app/src/main/java/dev/shorthouse/coinwatch/data/repository/details/CoinDetailsRepositoryImpl.kt b/app/src/main/java/dev/shorthouse/coinwatch/data/repository/details/CoinDetailsRepositoryImpl.kt new file mode 100644 index 00000000..74ab9e59 --- /dev/null +++ b/app/src/main/java/dev/shorthouse/coinwatch/data/repository/details/CoinDetailsRepositoryImpl.kt @@ -0,0 +1,36 @@ +package dev.shorthouse.coinwatch.data.repository.details + +import dev.shorthouse.coinwatch.common.Result +import dev.shorthouse.coinwatch.data.mapper.CoinDetailsMapper +import dev.shorthouse.coinwatch.data.source.remote.CoinNetworkDataSourceImpl +import dev.shorthouse.coinwatch.di.IoDispatcher +import dev.shorthouse.coinwatch.model.CoinDetails +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import timber.log.Timber + +class CoinDetailsRepositoryImpl @Inject constructor( + private val coinNetworkDataSource: CoinNetworkDataSourceImpl, + private val coinDetailsMapper: CoinDetailsMapper, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher +) : CoinDetailsRepository { + override fun getCoinDetails(coinId: String): Flow> = flow { + val response = coinNetworkDataSource.getCoinDetails(coinId = coinId) + val body = response.body() + + if (response.isSuccessful && body?.coinDetailsDataHolder?.coinDetailsData != null) { + val coinDetails = coinDetailsMapper.mapApiModelToModel(body) + emit(Result.Success(coinDetails)) + } else { + Timber.e("getCoinDetails unsuccessful retrofit response ${response.message()}") + emit(Result.Error("Unable to fetch coin details")) + } + }.catch { e -> + Timber.e("getCoinDetails exception ${e.message}") + emit(Result.Error("Unable to fetch coin details")) + }.flowOn(ioDispatcher) +} diff --git a/app/src/main/java/dev/shorthouse/coinwatch/data/source/remote/CoinApi.kt b/app/src/main/java/dev/shorthouse/coinwatch/data/source/remote/CoinApi.kt index 8790e0c1..ed4029a2 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/data/source/remote/CoinApi.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/data/source/remote/CoinApi.kt @@ -1,7 +1,7 @@ package dev.shorthouse.coinwatch.data.source.remote import dev.shorthouse.coinwatch.data.source.remote.model.CoinChartApiModel -import dev.shorthouse.coinwatch.data.source.remote.model.CoinDetailApiModel +import dev.shorthouse.coinwatch.data.source.remote.model.CoinDetailsApiModel import dev.shorthouse.coinwatch.data.source.remote.model.CoinSearchResultsApiModel import dev.shorthouse.coinwatch.data.source.remote.model.CoinsApiModel import retrofit2.Response @@ -21,10 +21,10 @@ interface CoinApi { ): Response @GET("coin/{coinId}") - suspend fun getCoinDetail( + suspend fun getCoinDetails( @Path("coinId") coinId: String, @Query("referenceCurrencyUuid") currencyUUID: String = "yhjMzLPhuIDl" - ): Response + ): Response @GET("coin/{coinId}/history") suspend fun getCoinChart( diff --git a/app/src/main/java/dev/shorthouse/coinwatch/data/source/remote/CoinNetworkDataSource.kt b/app/src/main/java/dev/shorthouse/coinwatch/data/source/remote/CoinNetworkDataSource.kt index 9da75a41..60d87f0f 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/data/source/remote/CoinNetworkDataSource.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/data/source/remote/CoinNetworkDataSource.kt @@ -1,7 +1,7 @@ package dev.shorthouse.coinwatch.data.source.remote import dev.shorthouse.coinwatch.data.source.remote.model.CoinChartApiModel -import dev.shorthouse.coinwatch.data.source.remote.model.CoinDetailApiModel +import dev.shorthouse.coinwatch.data.source.remote.model.CoinDetailsApiModel import dev.shorthouse.coinwatch.data.source.remote.model.CoinSearchResultsApiModel import dev.shorthouse.coinwatch.data.source.remote.model.CoinsApiModel import retrofit2.Response @@ -9,7 +9,7 @@ import retrofit2.Response interface CoinNetworkDataSource { suspend fun getCoins(coinIds: List): Response - suspend fun getCoinDetail(coinId: String): Response + suspend fun getCoinDetails(coinId: String): Response suspend fun getCoinChart( coinId: String, diff --git a/app/src/main/java/dev/shorthouse/coinwatch/data/source/remote/CoinNetworkDataSourceImpl.kt b/app/src/main/java/dev/shorthouse/coinwatch/data/source/remote/CoinNetworkDataSourceImpl.kt index 7753e2a7..d12b3c56 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/data/source/remote/CoinNetworkDataSourceImpl.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/data/source/remote/CoinNetworkDataSourceImpl.kt @@ -1,7 +1,7 @@ package dev.shorthouse.coinwatch.data.source.remote import dev.shorthouse.coinwatch.data.source.remote.model.CoinChartApiModel -import dev.shorthouse.coinwatch.data.source.remote.model.CoinDetailApiModel +import dev.shorthouse.coinwatch.data.source.remote.model.CoinDetailsApiModel import dev.shorthouse.coinwatch.data.source.remote.model.CoinSearchResultsApiModel import dev.shorthouse.coinwatch.data.source.remote.model.CoinsApiModel import javax.inject.Inject @@ -13,8 +13,8 @@ class CoinNetworkDataSourceImpl @Inject constructor(private val coinApi: CoinApi return coinApi.getCoins(coinIds = coinIds) } - override suspend fun getCoinDetail(coinId: String): Response { - return coinApi.getCoinDetail(coinId = coinId) + override suspend fun getCoinDetails(coinId: String): Response { + return coinApi.getCoinDetails(coinId = coinId) } override suspend fun getCoinChart( diff --git a/app/src/main/java/dev/shorthouse/coinwatch/data/source/remote/model/CoinDetailApiModel.kt b/app/src/main/java/dev/shorthouse/coinwatch/data/source/remote/model/CoinDetailsApiModel.kt similarity index 84% rename from app/src/main/java/dev/shorthouse/coinwatch/data/source/remote/model/CoinDetailApiModel.kt rename to app/src/main/java/dev/shorthouse/coinwatch/data/source/remote/model/CoinDetailsApiModel.kt index 5a2b962a..50483136 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/data/source/remote/model/CoinDetailApiModel.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/data/source/remote/model/CoinDetailsApiModel.kt @@ -2,17 +2,17 @@ package dev.shorthouse.coinwatch.data.source.remote.model import com.google.gson.annotations.SerializedName -data class CoinDetailApiModel( +data class CoinDetailsApiModel( @SerializedName("data") - val coinDetailDataHolder: CoinDetailDataHolder? + val coinDetailsDataHolder: CoinDetailsDataHolder? ) -data class CoinDetailDataHolder( +data class CoinDetailsDataHolder( @SerializedName("coin") - val coinDetailData: CoinDetailData? + val coinDetailsData: CoinDetailsData? ) -data class CoinDetailData( +data class CoinDetailsData( @SerializedName("uuid") val id: String?, @SerializedName("name") diff --git a/app/src/main/java/dev/shorthouse/coinwatch/di/MapperModule.kt b/app/src/main/java/dev/shorthouse/coinwatch/di/MapperModule.kt index 3d9b647f..94d31d09 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/di/MapperModule.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/di/MapperModule.kt @@ -5,7 +5,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dev.shorthouse.coinwatch.data.mapper.CoinChartMapper -import dev.shorthouse.coinwatch.data.mapper.CoinDetailMapper +import dev.shorthouse.coinwatch.data.mapper.CoinDetailsMapper import dev.shorthouse.coinwatch.data.mapper.CoinMapper import dev.shorthouse.coinwatch.data.mapper.CoinSearchResultsMapper import javax.inject.Singleton @@ -28,8 +28,8 @@ object MapperModule { @Provides @Singleton - fun provideCoinDetailMapper(): CoinDetailMapper { - return CoinDetailMapper() + fun provideCoinDetailsMapper(): CoinDetailsMapper { + return CoinDetailsMapper() } @Provides diff --git a/app/src/main/java/dev/shorthouse/coinwatch/di/NetworkDataModule.kt b/app/src/main/java/dev/shorthouse/coinwatch/di/NetworkDataModule.kt index f68be21c..0ce5a880 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/di/NetworkDataModule.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/di/NetworkDataModule.kt @@ -5,15 +5,15 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dev.shorthouse.coinwatch.data.mapper.CoinChartMapper -import dev.shorthouse.coinwatch.data.mapper.CoinDetailMapper +import dev.shorthouse.coinwatch.data.mapper.CoinDetailsMapper import dev.shorthouse.coinwatch.data.mapper.CoinMapper import dev.shorthouse.coinwatch.data.mapper.CoinSearchResultsMapper import dev.shorthouse.coinwatch.data.repository.chart.CoinChartRepository import dev.shorthouse.coinwatch.data.repository.chart.CoinChartRepositoryImpl import dev.shorthouse.coinwatch.data.repository.coin.CoinRepository import dev.shorthouse.coinwatch.data.repository.coin.CoinRepositoryImpl -import dev.shorthouse.coinwatch.data.repository.detail.CoinDetailRepository -import dev.shorthouse.coinwatch.data.repository.detail.CoinDetailRepositoryImpl +import dev.shorthouse.coinwatch.data.repository.details.CoinDetailsRepository +import dev.shorthouse.coinwatch.data.repository.details.CoinDetailsRepositoryImpl import dev.shorthouse.coinwatch.data.repository.searchResults.CoinSearchResultsRepository import dev.shorthouse.coinwatch.data.repository.searchResults.CoinSearchResultsRepositoryImpl import dev.shorthouse.coinwatch.data.source.remote.CoinApi @@ -41,14 +41,14 @@ object NetworkDataModule { @Provides @Singleton - fun provideCoinDetailRepository( + fun provideCoinDetailsRepository( coinNetworkDataSource: CoinNetworkDataSourceImpl, - coinDetailMapper: CoinDetailMapper, + coinDetailsMapper: CoinDetailsMapper, @IoDispatcher ioDispatcher: CoroutineDispatcher - ): CoinDetailRepository { - return CoinDetailRepositoryImpl( + ): CoinDetailsRepository { + return CoinDetailsRepositoryImpl( coinNetworkDataSource = coinNetworkDataSource, - coinDetailMapper = coinDetailMapper, + coinDetailsMapper = coinDetailsMapper, ioDispatcher = ioDispatcher ) } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/domain/GetCoinDetailUseCase.kt b/app/src/main/java/dev/shorthouse/coinwatch/domain/GetCoinDetailUseCase.kt deleted file mode 100644 index 315a0352..00000000 --- a/app/src/main/java/dev/shorthouse/coinwatch/domain/GetCoinDetailUseCase.kt +++ /dev/null @@ -1,19 +0,0 @@ -package dev.shorthouse.coinwatch.domain - -import dev.shorthouse.coinwatch.common.Result -import dev.shorthouse.coinwatch.data.repository.detail.CoinDetailRepository -import dev.shorthouse.coinwatch.model.CoinDetail -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow - -class GetCoinDetailUseCase @Inject constructor( - private val coinDetailRepository: CoinDetailRepository -) { - operator fun invoke(coinId: String): Flow> { - return getCoinDetail(coinId = coinId) - } - - private fun getCoinDetail(coinId: String): Flow> { - return coinDetailRepository.getCoinDetail(coinId = coinId) - } -} diff --git a/app/src/main/java/dev/shorthouse/coinwatch/domain/GetCoinDetailsUseCase.kt b/app/src/main/java/dev/shorthouse/coinwatch/domain/GetCoinDetailsUseCase.kt new file mode 100644 index 00000000..ff9cabd1 --- /dev/null +++ b/app/src/main/java/dev/shorthouse/coinwatch/domain/GetCoinDetailsUseCase.kt @@ -0,0 +1,19 @@ +package dev.shorthouse.coinwatch.domain + +import dev.shorthouse.coinwatch.common.Result +import dev.shorthouse.coinwatch.data.repository.details.CoinDetailsRepository +import dev.shorthouse.coinwatch.model.CoinDetails +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +class GetCoinDetailsUseCase @Inject constructor( + private val coinDetailsRepository: CoinDetailsRepository +) { + operator fun invoke(coinId: String): Flow> { + return getCoinDetails(coinId = coinId) + } + + private fun getCoinDetails(coinId: String): Flow> { + return coinDetailsRepository.getCoinDetails(coinId = coinId) + } +} diff --git a/app/src/main/java/dev/shorthouse/coinwatch/model/CoinDetail.kt b/app/src/main/java/dev/shorthouse/coinwatch/model/CoinDetails.kt similarity index 93% rename from app/src/main/java/dev/shorthouse/coinwatch/model/CoinDetail.kt rename to app/src/main/java/dev/shorthouse/coinwatch/model/CoinDetails.kt index 4a80e089..ee33fef1 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/model/CoinDetail.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/model/CoinDetails.kt @@ -1,6 +1,6 @@ package dev.shorthouse.coinwatch.model -data class CoinDetail( +data class CoinDetails( val id: String, val name: String, val symbol: String, diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt index 36c47c29..e8264bc0 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt @@ -19,7 +19,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController -import dev.shorthouse.coinwatch.ui.screen.details.CoinDetailScreen +import dev.shorthouse.coinwatch.ui.screen.details.CoinDetailsScreen import dev.shorthouse.coinwatch.ui.screen.favourites.FavouritesScreen import dev.shorthouse.coinwatch.ui.screen.list.CoinListScreen import dev.shorthouse.coinwatch.ui.screen.search.CoinSearchScreen @@ -108,8 +108,8 @@ fun AppNavHost( composable(route = NavigationBarScreen.Search.route) { CoinSearchScreen(navController = navController) } - composable(route = Screen.CoinDetail.route + "/{coinId}") { - CoinDetailScreen(navController = navController) + composable(route = Screen.Details.route + "/{coinId}") { + CoinDetailsScreen(navController = navController) } } }, diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/Screen.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/Screen.kt index f43391e3..7e25a4e2 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/navigation/Screen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/navigation/Screen.kt @@ -39,5 +39,5 @@ sealed class NavigationBarScreen( } sealed class Screen(val route: String) { - object CoinDetail : Screen("detail_screen") + object Details : Screen("details_screen") } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinDetailUiStatePreviewProvider.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinDetailUiStatePreviewProvider.kt deleted file mode 100644 index 629c5d98..00000000 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinDetailUiStatePreviewProvider.kt +++ /dev/null @@ -1,44 +0,0 @@ -package dev.shorthouse.coinwatch.ui.previewdata - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import dev.shorthouse.coinwatch.model.CoinChart -import dev.shorthouse.coinwatch.model.CoinDetail -import dev.shorthouse.coinwatch.model.Percentage -import dev.shorthouse.coinwatch.model.Price -import dev.shorthouse.coinwatch.ui.model.ChartPeriod -import dev.shorthouse.coinwatch.ui.screen.details.DetailsUiState -import java.math.BigDecimal -import kotlinx.collections.immutable.persistentListOf - -class CoinDetailUiStatePreviewProvider : PreviewParameterProvider { - override val values = sequenceOf( - DetailsUiState.Success( - CoinDetail( - id = "ethereum", - name = "Ethereum", - symbol = "ETH", - imageUrl = "https://cdn.coinranking.com/rk4RKHOuW/eth.svg", - currentPrice = Price("1879.14"), - marketCap = Price("225722901094"), - marketCapRank = "2", - volume24h = "6,627,669,115", - circulatingSupply = "120,186,525", - allTimeHigh = Price("4878.26"), - allTimeHighDate = "10 Nov 2021", - listedDate = "7 Aug 2015" - ), - CoinChart( - prices = persistentListOf( - BigDecimal("1755.19"), BigDecimal("1749.71"), BigDecimal("1750.94"), BigDecimal("1748.44"), BigDecimal("1743.98"), BigDecimal("1740.25"), BigDecimal("1737.53"), BigDecimal("1730.56"), BigDecimal("1738.12"), BigDecimal("1736.10"), BigDecimal("1740.20"), BigDecimal("1740.64"), BigDecimal("1741.49"), BigDecimal("1738.87"), BigDecimal("1734.92"), BigDecimal("1736.79"), BigDecimal("1743.53"), BigDecimal("1743.21"), BigDecimal("1744.75"), BigDecimal("1744.85"), BigDecimal("1741.76"), BigDecimal("1741.46"), BigDecimal("1739.82"), BigDecimal("1740.15"), BigDecimal("1745.08"), BigDecimal("1743.29"), BigDecimal("1746.12"), BigDecimal("1745.99"), BigDecimal("1744.89"), BigDecimal("1741.10"), BigDecimal("1741.91"), BigDecimal("1738.47"), BigDecimal("1737.67"), BigDecimal("1741.82"), BigDecimal("1735.95"), BigDecimal("1728.11"), BigDecimal("1657.23"), BigDecimal("1649.89"), BigDecimal("1649.71"), BigDecimal("1650.68"), BigDecimal("1654.04"), BigDecimal("1648.55"), BigDecimal("1650.10"), BigDecimal("1651.87"), BigDecimal("1651.29"), BigDecimal("1642.75"), BigDecimal("1637.79"), BigDecimal("1635.80"), BigDecimal("1637.01"), BigDecimal("1632.46"), BigDecimal("1633.31"), BigDecimal("1640.08"), BigDecimal("1638.61"), BigDecimal("1645.47"), BigDecimal("1643.50"), BigDecimal("1640.57"), BigDecimal("1640.41"), BigDecimal("1641.38"), BigDecimal("1660.21"), BigDecimal("1665.73"), BigDecimal("1660.33"), BigDecimal("1665.65"), BigDecimal("1664.11"), BigDecimal("1665.71"), BigDecimal("1661.90"), BigDecimal("1661.17"), BigDecimal("1662.54"), BigDecimal("1665.58"), BigDecimal("1666.27"), BigDecimal("1669.82"), BigDecimal("1671.34"), BigDecimal("1669.87"), BigDecimal("1670.62"), BigDecimal("1668.97"), BigDecimal("1668.86"), BigDecimal("1664.58"), BigDecimal("1665.96"), BigDecimal("1664.53"), BigDecimal("1656.15"), BigDecimal("1670.91"), BigDecimal("1685.59"), BigDecimal("1693.69"), BigDecimal("1718.10"), BigDecimal("1719.56"), BigDecimal("1724.42"), BigDecimal("1717.22"), BigDecimal("1718.34"), BigDecimal("1716.38"), BigDecimal("1715.37"), BigDecimal("1716.46"), BigDecimal("1719.39"), BigDecimal("1717.94"), BigDecimal("1722.92"), BigDecimal("1755.97"), BigDecimal("1749.11"), BigDecimal("1742.58"), BigDecimal("1742.88"), BigDecimal("1743.36"), BigDecimal("1742.95"), BigDecimal("1739.68"), BigDecimal("1736.65"), BigDecimal("1739.88"), BigDecimal("1734.35"), BigDecimal("1727.31"), BigDecimal("1728.35"), BigDecimal("1724.05"), BigDecimal("1730.04"), BigDecimal("1726.87"), BigDecimal("1727.71"), BigDecimal("1728.49"), BigDecimal("1729.93"), BigDecimal("1726.37"), BigDecimal("1722.92"), BigDecimal("1726.67"), BigDecimal("1724.76"), BigDecimal("1728.41"), BigDecimal("1729.20"), BigDecimal("1728.20"), BigDecimal("1727.98"), BigDecimal("1729.96"), BigDecimal("1727.80"), BigDecimal("1732.04"), BigDecimal("1730.22"), BigDecimal("1733.16"), BigDecimal("1734.14"), BigDecimal("1734.31"), BigDecimal("1739.62"), BigDecimal("1737.76"), BigDecimal("1739.52"), BigDecimal("1742.98"), BigDecimal("1738.36") // ktlint-disable argument-list-wrapping - ), - minPrice = Price("1632.46"), - maxPrice = Price("1922.83"), - periodPriceChangePercentage = Percentage("7.06") - ), - chartPeriod = ChartPeriod.Week, - isCoinFavourite = true - ), - DetailsUiState.Loading, - DetailsUiState.Error("No internet connection") - ) -} diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinDetailsUiStatePreviewProvider.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinDetailsUiStatePreviewProvider.kt new file mode 100644 index 00000000..333ddb7d --- /dev/null +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinDetailsUiStatePreviewProvider.kt @@ -0,0 +1,174 @@ +package dev.shorthouse.coinwatch.ui.previewdata + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import dev.shorthouse.coinwatch.model.CoinChart +import dev.shorthouse.coinwatch.model.CoinDetails +import dev.shorthouse.coinwatch.model.Percentage +import dev.shorthouse.coinwatch.model.Price +import dev.shorthouse.coinwatch.ui.model.ChartPeriod +import dev.shorthouse.coinwatch.ui.screen.details.DetailsUiState +import java.math.BigDecimal +import kotlinx.collections.immutable.persistentListOf + +class CoinDetailsUiStatePreviewProvider : PreviewParameterProvider { + override val values = sequenceOf( + DetailsUiState.Success( + CoinDetails( + id = "ethereum", + name = "Ethereum", + symbol = "ETH", + imageUrl = "https://cdn.coinranking.com/rk4RKHOuW/eth.svg", + currentPrice = Price("1879.14"), + marketCap = Price("225722901094"), + marketCapRank = "2", + volume24h = "6,627,669,115", + circulatingSupply = "120,186,525", + allTimeHigh = Price("4878.26"), + allTimeHighDate = "10 Nov 2021", + listedDate = "7 Aug 2015" + ), + CoinChart( + prices = persistentListOf( + BigDecimal("1755.19"), + BigDecimal("1749.71"), + BigDecimal("1750.94"), + BigDecimal("1748.44"), + BigDecimal("1743.98"), + BigDecimal("1740.25"), + BigDecimal("1737.53"), + BigDecimal("1730.56"), + BigDecimal("1738.12"), + BigDecimal("1736.10"), + BigDecimal("1740.20"), + BigDecimal("1740.64"), + BigDecimal("1741.49"), + BigDecimal("1738.87"), + BigDecimal("1734.92"), + BigDecimal("1736.79"), + BigDecimal("1743.53"), + BigDecimal("1743.21"), + BigDecimal("1744.75"), + BigDecimal("1744.85"), + BigDecimal("1741.76"), + BigDecimal("1741.46"), + BigDecimal("1739.82"), + BigDecimal("1740.15"), + BigDecimal("1745.08"), + BigDecimal("1743.29"), + BigDecimal("1746.12"), + BigDecimal("1745.99"), + BigDecimal("1744.89"), + BigDecimal("1741.10"), + BigDecimal("1741.91"), + BigDecimal("1738.47"), + BigDecimal("1737.67"), + BigDecimal("1741.82"), + BigDecimal("1735.95"), + BigDecimal("1728.11"), + BigDecimal("1657.23"), + BigDecimal("1649.89"), + BigDecimal("1649.71"), + BigDecimal("1650.68"), + BigDecimal("1654.04"), + BigDecimal("1648.55"), + BigDecimal("1650.10"), + BigDecimal("1651.87"), + BigDecimal("1651.29"), + BigDecimal("1642.75"), + BigDecimal("1637.79"), + BigDecimal("1635.80"), + BigDecimal("1637.01"), + BigDecimal("1632.46"), + BigDecimal("1633.31"), + BigDecimal("1640.08"), + BigDecimal("1638.61"), + BigDecimal("1645.47"), + BigDecimal("1643.50"), + BigDecimal("1640.57"), + BigDecimal("1640.41"), + BigDecimal("1641.38"), + BigDecimal("1660.21"), + BigDecimal("1665.73"), + BigDecimal("1660.33"), + BigDecimal("1665.65"), + BigDecimal("1664.11"), + BigDecimal("1665.71"), + BigDecimal("1661.90"), + BigDecimal("1661.17"), + BigDecimal("1662.54"), + BigDecimal("1665.58"), + BigDecimal("1666.27"), + BigDecimal("1669.82"), + BigDecimal("1671.34"), + BigDecimal("1669.87"), + BigDecimal("1670.62"), + BigDecimal("1668.97"), + BigDecimal("1668.86"), + BigDecimal("1664.58"), + BigDecimal("1665.96"), + BigDecimal("1664.53"), + BigDecimal("1656.15"), + BigDecimal("1670.91"), + BigDecimal("1685.59"), + BigDecimal("1693.69"), + BigDecimal("1718.10"), + BigDecimal("1719.56"), + BigDecimal("1724.42"), + BigDecimal("1717.22"), + BigDecimal("1718.34"), + BigDecimal("1716.38"), + BigDecimal("1715.37"), + BigDecimal("1716.46"), + BigDecimal("1719.39"), + BigDecimal("1717.94"), + BigDecimal("1722.92"), + BigDecimal("1755.97"), + BigDecimal("1749.11"), + BigDecimal("1742.58"), + BigDecimal("1742.88"), + BigDecimal("1743.36"), + BigDecimal("1742.95"), + BigDecimal("1739.68"), + BigDecimal("1736.65"), + BigDecimal("1739.88"), + BigDecimal("1734.35"), + BigDecimal("1727.31"), + BigDecimal("1728.35"), + BigDecimal("1724.05"), + BigDecimal("1730.04"), + BigDecimal("1726.87"), + BigDecimal("1727.71"), + BigDecimal("1728.49"), + BigDecimal("1729.93"), + BigDecimal("1726.37"), + BigDecimal("1722.92"), + BigDecimal("1726.67"), + BigDecimal("1724.76"), + BigDecimal("1728.41"), + BigDecimal("1729.20"), + BigDecimal("1728.20"), + BigDecimal("1727.98"), + BigDecimal("1729.96"), + BigDecimal("1727.80"), + BigDecimal("1732.04"), + BigDecimal("1730.22"), + BigDecimal("1733.16"), + BigDecimal("1734.14"), + BigDecimal("1734.31"), + BigDecimal("1739.62"), + BigDecimal("1737.76"), + BigDecimal("1739.52"), + BigDecimal("1742.98"), + BigDecimal("1738.36") // ktlint-disable argument-list-wrapping + ), + minPrice = Price("1632.46"), + maxPrice = Price("1922.83"), + periodPriceChangePercentage = Percentage("7.06") + ), + chartPeriod = ChartPeriod.Week, + isCoinFavourite = true + ), + DetailsUiState.Loading, + DetailsUiState.Error("No internet connection") + ) +} diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt index a248e0ac..9c1b3d29 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt @@ -43,24 +43,24 @@ import coil.decode.SvgDecoder import coil.request.ImageRequest import dev.shorthouse.coinwatch.R import dev.shorthouse.coinwatch.model.CoinChart -import dev.shorthouse.coinwatch.model.CoinDetail +import dev.shorthouse.coinwatch.model.CoinDetails import dev.shorthouse.coinwatch.ui.component.ErrorState import dev.shorthouse.coinwatch.ui.model.ChartPeriod -import dev.shorthouse.coinwatch.ui.previewdata.CoinDetailUiStatePreviewProvider +import dev.shorthouse.coinwatch.ui.previewdata.CoinDetailsUiStatePreviewProvider import dev.shorthouse.coinwatch.ui.screen.details.component.CoinChartCard import dev.shorthouse.coinwatch.ui.screen.details.component.CoinChartRangeCard -import dev.shorthouse.coinwatch.ui.screen.details.component.CoinDetailSkeletonLoader +import dev.shorthouse.coinwatch.ui.screen.details.component.CoinDetailsSkeletonLoader import dev.shorthouse.coinwatch.ui.screen.details.component.MarketStatsCard import dev.shorthouse.coinwatch.ui.theme.AppTheme @Composable -fun CoinDetailScreen( +fun CoinDetailsScreen( navController: NavController, viewModel: DetailsViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - CoinDetailScreen( + CoinDetailsScreen( uiState = uiState, onNavigateUp = { navController.navigateUp() }, onClickFavouriteCoin = { viewModel.toggleIsCoinFavourite() }, @@ -71,7 +71,7 @@ fun CoinDetailScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun CoinDetailScreen( +fun CoinDetailsScreen( uiState: DetailsUiState, onNavigateUp: () -> Unit, onClickFavouriteCoin: () -> Unit, @@ -85,8 +85,8 @@ fun CoinDetailScreen( is DetailsUiState.Success -> { Scaffold( topBar = { - CoinDetailTopBar( - coinDetail = uiState.coinDetail, + CoinDetailsTopBar( + coinDetails = uiState.coinDetails, isCoinFavourite = uiState.isCoinFavourite, onNavigateUp = onNavigateUp, onClickFavouriteCoin = onClickFavouriteCoin, @@ -94,8 +94,8 @@ fun CoinDetailScreen( ) }, content = { scaffoldPadding -> - CoinDetailContent( - coinDetail = uiState.coinDetail, + CoinDetailsContent( + coinDetails = uiState.coinDetails, coinChart = uiState.coinChart, chartPeriod = uiState.chartPeriod, onClickChartPeriod = onClickChartPeriod, @@ -107,7 +107,7 @@ fun CoinDetailScreen( } is DetailsUiState.Loading -> { - CoinDetailSkeletonLoader() + CoinDetailsSkeletonLoader() } is DetailsUiState.Error -> { @@ -122,8 +122,8 @@ fun CoinDetailScreen( @Composable @OptIn(ExperimentalMaterial3Api::class) -private fun CoinDetailTopBar( - coinDetail: CoinDetail, +private fun CoinDetailsTopBar( + coinDetails: CoinDetails, isCoinFavourite: Boolean, onNavigateUp: () -> Unit, onClickFavouriteCoin: () -> Unit, @@ -150,13 +150,13 @@ private fun CoinDetailTopBar( Row(verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.weight(1f)) { Text( - text = coinDetail.name, + text = coinDetails.name, maxLines = 1, overflow = TextOverflow.Ellipsis ) Text( - text = coinDetail.symbol, + text = coinDetails.symbol, style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, @@ -166,7 +166,7 @@ private fun CoinDetailTopBar( AsyncImage( model = imageBuilder - .data(coinDetail.imageUrl) + .data(coinDetails.imageUrl) .build(), contentDescription = null, modifier = Modifier @@ -198,8 +198,8 @@ private fun CoinDetailTopBar( } @Composable -private fun CoinDetailContent( - coinDetail: CoinDetail, +private fun CoinDetailsContent( + coinDetails: CoinDetails, coinChart: CoinChart, chartPeriod: ChartPeriod, onClickChartPeriod: (ChartPeriod) -> Unit, @@ -210,10 +210,10 @@ private fun CoinDetailContent( .fillMaxSize() .verticalScroll(rememberScrollState()) .padding(start = 12.dp, end = 12.dp, bottom = 12.dp) - .testTag("coin_detail_content") + .testTag("coin_details_content") ) { CoinChartCard( - currentPrice = coinDetail.currentPrice, + currentPrice = coinDetails.currentPrice, prices = coinChart.prices, periodPriceChangePercentage = coinChart.periodPriceChangePercentage, chartPeriod = chartPeriod, @@ -230,7 +230,7 @@ private fun CoinDetailContent( Spacer(Modifier.height(8.dp)) CoinChartRangeCard( - currentPrice = coinDetail.currentPrice, + currentPrice = coinDetails.currentPrice, minPrice = coinChart.minPrice, maxPrice = coinChart.maxPrice, isPricesEmpty = coinChart.prices.isEmpty() @@ -245,17 +245,17 @@ private fun CoinDetailContent( Spacer(Modifier.height(8.dp)) - MarketStatsCard(coinDetail = coinDetail) + MarketStatsCard(coinDetails = coinDetails) } } @Composable @Preview private fun CoinDetailScreenPreview( - @PreviewParameter(CoinDetailUiStatePreviewProvider::class) uiState: DetailsUiState + @PreviewParameter(CoinDetailsUiStatePreviewProvider::class) uiState: DetailsUiState ) { AppTheme { - CoinDetailScreen( + CoinDetailsScreen( uiState = uiState, onNavigateUp = {}, onClickFavouriteCoin = {}, diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsUiState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsUiState.kt index cd729255..607b630f 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsUiState.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsUiState.kt @@ -1,13 +1,13 @@ package dev.shorthouse.coinwatch.ui.screen.details import dev.shorthouse.coinwatch.model.CoinChart -import dev.shorthouse.coinwatch.model.CoinDetail +import dev.shorthouse.coinwatch.model.CoinDetails import dev.shorthouse.coinwatch.ui.model.ChartPeriod sealed interface DetailsUiState { object Loading : DetailsUiState data class Success( - val coinDetail: CoinDetail, + val coinDetails: CoinDetails, val coinChart: CoinChart, val chartPeriod: ChartPeriod, val isCoinFavourite: Boolean diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsViewModel.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsViewModel.kt index 888ee149..73b3befc 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsViewModel.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsViewModel.kt @@ -9,7 +9,7 @@ import dev.shorthouse.coinwatch.common.Result import dev.shorthouse.coinwatch.data.source.local.model.FavouriteCoin import dev.shorthouse.coinwatch.domain.DeleteFavouriteCoinUseCase import dev.shorthouse.coinwatch.domain.GetCoinChartUseCase -import dev.shorthouse.coinwatch.domain.GetCoinDetailUseCase +import dev.shorthouse.coinwatch.domain.GetCoinDetailsUseCase import dev.shorthouse.coinwatch.domain.InsertFavouriteCoinUseCase import dev.shorthouse.coinwatch.domain.IsCoinFavouriteUseCase import dev.shorthouse.coinwatch.ui.model.ChartPeriod @@ -27,7 +27,7 @@ import kotlinx.coroutines.launch @HiltViewModel class DetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, - private val getCoinDetailUseCase: GetCoinDetailUseCase, + private val getCoinDetailsUseCase: GetCoinDetailsUseCase, private val getCoinChartUseCase: GetCoinChartUseCase, private val isCoinFavouriteUseCase: IsCoinFavouriteUseCase, private val insertFavouriteCoinUseCase: InsertFavouriteCoinUseCase, @@ -52,7 +52,7 @@ class DetailsViewModel @Inject constructor( return } - val coinDetailFlow = getCoinDetailUseCase(coinId = coinId) + val coinDetailsFlow = getCoinDetailsUseCase(coinId = coinId) val coinChartFlow = chartPeriodFlow.flatMapLatest { chartPeriod -> getCoinChartUseCase( coinId = coinId, @@ -62,13 +62,13 @@ class DetailsViewModel @Inject constructor( val isCoinFavouriteFlow = isCoinFavouriteUseCase(coinId = coinId) combine( - coinDetailFlow, + coinDetailsFlow, coinChartFlow, isCoinFavouriteFlow - ) { coinDetailResult, coinChartResult, isCoinFavouriteResult -> + ) { coinDetailsResult, coinChartResult, isCoinFavouriteResult -> when { - coinDetailResult is Result.Error -> { - _uiState.update { DetailsUiState.Error(coinDetailResult.message) } + coinDetailsResult is Result.Error -> { + _uiState.update { DetailsUiState.Error(coinDetailsResult.message) } } coinChartResult is Result.Error -> { @@ -79,12 +79,12 @@ class DetailsViewModel @Inject constructor( _uiState.update { DetailsUiState.Error(isCoinFavouriteResult.message) } } - coinDetailResult is Result.Success && + coinDetailsResult is Result.Success && coinChartResult is Result.Success && isCoinFavouriteResult is Result.Success -> { _uiState.update { DetailsUiState.Success( - coinDetail = coinDetailResult.data, + coinDetails = coinDetailsResult.data, coinChart = coinChartResult.data, chartPeriod = chartPeriodFlow.value, isCoinFavourite = isCoinFavouriteResult.data diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/DetailsSkeletonLoader.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/DetailsSkeletonLoader.kt index 88f81b9d..506680f0 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/DetailsSkeletonLoader.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/DetailsSkeletonLoader.kt @@ -28,7 +28,7 @@ import dev.shorthouse.coinwatch.ui.component.SkeletonSurface import dev.shorthouse.coinwatch.ui.theme.AppTheme @Composable -fun CoinDetailSkeletonLoader(modifier: Modifier = Modifier) { +fun CoinDetailsSkeletonLoader(modifier: Modifier = Modifier) { Scaffold( topBar = { DetailsSkeletonTopBar() @@ -116,6 +116,6 @@ private fun DetailsSkeletonContent(modifier: Modifier = Modifier) { @Composable private fun CoinDetailSkeletonLoaderPreview() { AppTheme { - CoinDetailSkeletonLoader() + CoinDetailsSkeletonLoader() } } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/MarketStatsCard.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/MarketStatsCard.kt index da0ffbca..3276a9f1 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/MarketStatsCard.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/MarketStatsCard.kt @@ -17,44 +17,44 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import dev.shorthouse.coinwatch.R -import dev.shorthouse.coinwatch.model.CoinDetail +import dev.shorthouse.coinwatch.model.CoinDetails import dev.shorthouse.coinwatch.model.Price import dev.shorthouse.coinwatch.ui.theme.AppTheme @Composable fun MarketStatsCard( - coinDetail: CoinDetail, + coinDetails: CoinDetails, modifier: Modifier = Modifier ) { - val coinDetailItems = remember(coinDetail) { + val coinDetailsItems = remember(coinDetails) { listOf( - CoinDetailListItem( + CoinDetailsListItem( nameId = R.string.list_item_market_cap_rank, - value = coinDetail.marketCapRank + value = coinDetails.marketCapRank ), - CoinDetailListItem( + CoinDetailsListItem( nameId = R.string.list_item_market_cap, - value = coinDetail.marketCap.formattedAmount + value = coinDetails.marketCap.formattedAmount ), - CoinDetailListItem( + CoinDetailsListItem( nameId = R.string.list_item_volume_24h, - value = coinDetail.volume24h + value = coinDetails.volume24h ), - CoinDetailListItem( + CoinDetailsListItem( nameId = R.string.list_item_circulating_supply, - value = coinDetail.circulatingSupply + value = coinDetails.circulatingSupply ), - CoinDetailListItem( + CoinDetailsListItem( nameId = R.string.list_item_ath, - value = coinDetail.allTimeHigh.formattedAmount + value = coinDetails.allTimeHigh.formattedAmount ), - CoinDetailListItem( + CoinDetailsListItem( nameId = R.string.list_item_ath_date, - value = coinDetail.allTimeHighDate + value = coinDetails.allTimeHighDate ), - CoinDetailListItem( + CoinDetailsListItem( nameId = R.string.list_item_listed_date, - value = coinDetail.listedDate + value = coinDetails.listedDate ) ) } @@ -65,8 +65,8 @@ fun MarketStatsCard( ) { Column(modifier = Modifier.padding(12.dp)) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - coinDetailItems.forEachIndexed { coinDetailIndex, coinDetailListItem -> - if (coinDetailIndex != 0) { + coinDetailsItems.forEachIndexed { coinDetailsIndex, coinDetailsListItem -> + if (coinDetailsIndex != 0) { Divider(color = MaterialTheme.colorScheme.background) } @@ -75,13 +75,13 @@ fun MarketStatsCard( modifier = Modifier.fillMaxWidth() ) { Text( - text = stringResource(coinDetailListItem.nameId), + text = stringResource(coinDetailsListItem.nameId), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) Text( - text = coinDetailListItem.value, + text = coinDetailsListItem.value, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurface ) @@ -92,7 +92,7 @@ fun MarketStatsCard( } } -private data class CoinDetailListItem( +private data class CoinDetailsListItem( @StringRes val nameId: Int, val value: String ) @@ -102,7 +102,7 @@ private data class CoinDetailListItem( private fun MarketStatsCardPreview() { AppTheme { MarketStatsCard( - coinDetail = CoinDetail( + coinDetails = CoinDetails( id = "ethereum", name = "Ethereum", symbol = "ETH", diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt index 172d7630..8718e09b 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt @@ -45,7 +45,7 @@ fun FavouritesScreen( FavouriteScreen( uiState = uiState, onCoinClick = { coin -> - navController.navigate(Screen.CoinDetail.route + "/${coin.id}") + navController.navigate(Screen.Details.route + "/${coin.id}") }, onErrorRetry = { viewModel.initialiseUiState() } ) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt index 58801303..35a81e2f 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt @@ -60,7 +60,7 @@ fun CoinListScreen( CoinListScreen( uiState = uiState, onCoinClick = { coin -> - navController.navigate(Screen.CoinDetail.route + "/${coin.id}") + navController.navigate(Screen.Details.route + "/${coin.id}") }, onRefresh = { viewModel.initialiseUiState() } ) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt index 0409535c..f163d27c 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt @@ -51,7 +51,7 @@ fun CoinSearchScreen( searchQuery = viewModel.searchQuery, onSearchQueryChange = { viewModel.updateSearchQuery(it) }, onCoinClick = { coin -> - navController.navigate(Screen.CoinDetail.route + "/${coin.id}") + navController.navigate(Screen.Details.route + "/${coin.id}") }, onErrorRetry = { viewModel.initialiseUiState() } ) diff --git a/app/src/test/java/dev/shorthouse/coinwatch/data/mapper/CoinDetailMapperTest.kt b/app/src/test/java/dev/shorthouse/coinwatch/data/mapper/CoinDetailsMapperTest.kt similarity index 72% rename from app/src/test/java/dev/shorthouse/coinwatch/data/mapper/CoinDetailMapperTest.kt rename to app/src/test/java/dev/shorthouse/coinwatch/data/mapper/CoinDetailsMapperTest.kt index 57612fb8..4cabae40 100644 --- a/app/src/test/java/dev/shorthouse/coinwatch/data/mapper/CoinDetailMapperTest.kt +++ b/app/src/test/java/dev/shorthouse/coinwatch/data/mapper/CoinDetailsMapperTest.kt @@ -2,27 +2,27 @@ package dev.shorthouse.coinwatch.data.mapper import com.google.common.truth.Truth.assertThat import dev.shorthouse.coinwatch.data.source.remote.model.AllTimeHigh -import dev.shorthouse.coinwatch.data.source.remote.model.CoinDetailApiModel -import dev.shorthouse.coinwatch.data.source.remote.model.CoinDetailData -import dev.shorthouse.coinwatch.data.source.remote.model.CoinDetailDataHolder +import dev.shorthouse.coinwatch.data.source.remote.model.CoinDetailsApiModel +import dev.shorthouse.coinwatch.data.source.remote.model.CoinDetailsData +import dev.shorthouse.coinwatch.data.source.remote.model.CoinDetailsDataHolder import dev.shorthouse.coinwatch.data.source.remote.model.Supply -import dev.shorthouse.coinwatch.model.CoinDetail +import dev.shorthouse.coinwatch.model.CoinDetails import dev.shorthouse.coinwatch.model.Price import org.junit.Test -class CoinDetailMapperTest { +class CoinDetailsMapperTest { // Class under test - private val coinDetailMapper = CoinDetailMapper() + private val coinDetailsMapper = CoinDetailsMapper() @Test - fun `When coin detail data holder is null should return default values`() { + fun `When coin details data holder is null should return default values`() { // Arrange - val coinDetailApiModel = CoinDetailApiModel( - coinDetailDataHolder = null + val coinDetailsApiModel = CoinDetailsApiModel( + coinDetailsDataHolder = null ) - val expectedCoinDetail = CoinDetail( + val expectedCoinDetails = CoinDetails( id = "", name = "", symbol = "", @@ -38,22 +38,22 @@ class CoinDetailMapperTest { ) // Act - val coinDetail = coinDetailMapper.mapApiModelToModel(coinDetailApiModel) + val coinDetails = coinDetailsMapper.mapApiModelToModel(coinDetailsApiModel) // Assert - assertThat(coinDetail).isEqualTo(expectedCoinDetail) + assertThat(coinDetails).isEqualTo(expectedCoinDetails) } @Test - fun `When coin detail data is null should return default values`() { + fun `When coin details data is null should return default values`() { // Arrange - val coinDetailApiModel = CoinDetailApiModel( - coinDetailDataHolder = CoinDetailDataHolder( - coinDetailData = null + val coinDetailsApiModel = CoinDetailsApiModel( + coinDetailsDataHolder = CoinDetailsDataHolder( + coinDetailsData = null ) ) - val expectedCoinDetail = CoinDetail( + val expectedCoinDetails = CoinDetails( id = "", name = "", symbol = "", @@ -69,18 +69,18 @@ class CoinDetailMapperTest { ) // Act - val coinDetail = coinDetailMapper.mapApiModelToModel(coinDetailApiModel) + val coinDetails = coinDetailsMapper.mapApiModelToModel(coinDetailsApiModel) // Assert - assertThat(coinDetail).isEqualTo(expectedCoinDetail) + assertThat(coinDetails).isEqualTo(expectedCoinDetails) } @Test - fun `When all coin detail values are null should return default values`() { + fun `When all coin details values are null should return default values`() { // Arrange - val coinDetailApiModel = CoinDetailApiModel( - coinDetailDataHolder = CoinDetailDataHolder( - coinDetailData = CoinDetailData( + val coinDetailsApiModel = CoinDetailsApiModel( + coinDetailsDataHolder = CoinDetailsDataHolder( + coinDetailsData = CoinDetailsData( id = null, name = null, symbol = null, @@ -96,7 +96,7 @@ class CoinDetailMapperTest { ) ) - val expectedCoinDetail = CoinDetail( + val expectedCoinDetails = CoinDetails( id = "", name = "", symbol = "", @@ -112,18 +112,18 @@ class CoinDetailMapperTest { ) // Act - val coinDetail = coinDetailMapper.mapApiModelToModel(coinDetailApiModel) + val coinDetails = coinDetailsMapper.mapApiModelToModel(coinDetailsApiModel) // Assert - assertThat(coinDetail).isEqualTo(expectedCoinDetail) + assertThat(coinDetails).isEqualTo(expectedCoinDetails) } @Test fun `When all numbers to format are invalid should return empty values`() { // Arrange - val coinDetailApiModel = CoinDetailApiModel( - coinDetailDataHolder = CoinDetailDataHolder( - coinDetailData = CoinDetailData( + val coinDetailsApiModel = CoinDetailsApiModel( + coinDetailsDataHolder = CoinDetailsDataHolder( + coinDetailsData = CoinDetailsData( id = "Qwsogvtv82FCd", name = "Bitcoin", symbol = "BTC", @@ -144,7 +144,7 @@ class CoinDetailMapperTest { ) ) - val expectedCoinDetail = CoinDetail( + val expectedCoinDetails = CoinDetails( id = "Qwsogvtv82FCd", name = "Bitcoin", symbol = "BTC", @@ -160,18 +160,18 @@ class CoinDetailMapperTest { ) // Act - val coinDetail = coinDetailMapper.mapApiModelToModel(coinDetailApiModel) + val coinDetails = coinDetailsMapper.mapApiModelToModel(coinDetailsApiModel) // Assert - assertThat(coinDetail).isEqualTo(expectedCoinDetail) + assertThat(coinDetails).isEqualTo(expectedCoinDetails) } @Test fun `When timestamps are invalid should return default values`() { // Arrange - val coinDetailApiModel = CoinDetailApiModel( - coinDetailDataHolder = CoinDetailDataHolder( - coinDetailData = CoinDetailData( + val coinDetailsApiModel = CoinDetailsApiModel( + coinDetailsDataHolder = CoinDetailsDataHolder( + coinDetailsData = CoinDetailsData( id = "Qwsogvtv82FCd", name = "Bitcoin", symbol = "BTC", @@ -192,7 +192,7 @@ class CoinDetailMapperTest { ) ) - val expectedCoinDetail = CoinDetail( + val expectedCoinDetails = CoinDetails( id = "Qwsogvtv82FCd", name = "Bitcoin", symbol = "BTC", @@ -208,18 +208,18 @@ class CoinDetailMapperTest { ) // Act - val coinDetail = coinDetailMapper.mapApiModelToModel(coinDetailApiModel) + val coinDetails = coinDetailsMapper.mapApiModelToModel(coinDetailsApiModel) // Assert - assertThat(coinDetail).isEqualTo(expectedCoinDetail) + assertThat(coinDetails).isEqualTo(expectedCoinDetails) } @Test - fun `When coin detail data has valid values should map as expected`() { + fun `When coin details data has valid values should map as expected`() { // Arrange - val coinDetailApiModel = CoinDetailApiModel( - coinDetailDataHolder = CoinDetailDataHolder( - coinDetailData = CoinDetailData( + val coinDetailsApiModel = CoinDetailsApiModel( + coinDetailsDataHolder = CoinDetailsDataHolder( + coinDetailsData = CoinDetailsData( id = "Qwsogvtv82FCd", name = "Bitcoin", symbol = "BTC", @@ -240,7 +240,7 @@ class CoinDetailMapperTest { ) ) - val expectedCoinDetail = CoinDetail( + val expectedCoinDetails = CoinDetails( id = "Qwsogvtv82FCd", name = "Bitcoin", symbol = "BTC", @@ -256,9 +256,9 @@ class CoinDetailMapperTest { ) // Act - val coinDetail = coinDetailMapper.mapApiModelToModel(coinDetailApiModel) + val coinDetails = coinDetailsMapper.mapApiModelToModel(coinDetailsApiModel) // Assert - assertThat(coinDetail).isEqualTo(expectedCoinDetail) + assertThat(coinDetails).isEqualTo(expectedCoinDetails) } } diff --git a/app/src/test/java/dev/shorthouse/coinwatch/data/source/remote/FakeCoinApi.kt b/app/src/test/java/dev/shorthouse/coinwatch/data/source/remote/FakeCoinApi.kt index b191b9df..43265b2d 100644 --- a/app/src/test/java/dev/shorthouse/coinwatch/data/source/remote/FakeCoinApi.kt +++ b/app/src/test/java/dev/shorthouse/coinwatch/data/source/remote/FakeCoinApi.kt @@ -4,9 +4,9 @@ import dev.shorthouse.coinwatch.data.source.remote.model.AllTimeHigh import dev.shorthouse.coinwatch.data.source.remote.model.CoinApiModel import dev.shorthouse.coinwatch.data.source.remote.model.CoinChartApiModel import dev.shorthouse.coinwatch.data.source.remote.model.CoinChartData -import dev.shorthouse.coinwatch.data.source.remote.model.CoinDetailApiModel -import dev.shorthouse.coinwatch.data.source.remote.model.CoinDetailData -import dev.shorthouse.coinwatch.data.source.remote.model.CoinDetailDataHolder +import dev.shorthouse.coinwatch.data.source.remote.model.CoinDetailsApiModel +import dev.shorthouse.coinwatch.data.source.remote.model.CoinDetailsData +import dev.shorthouse.coinwatch.data.source.remote.model.CoinDetailsDataHolder import dev.shorthouse.coinwatch.data.source.remote.model.CoinSearchResultsApiModel import dev.shorthouse.coinwatch.data.source.remote.model.CoinSearchResultsData import dev.shorthouse.coinwatch.data.source.remote.model.CoinsApiModel @@ -215,13 +215,13 @@ class FakeCoinApi : CoinApi { } } - override suspend fun getCoinDetail( + override suspend fun getCoinDetails( coinId: String, currencyUUID: String - ): Response { + ): Response { when (coinId) { "Qwsogvtv82FCd" -> { - val coinDetail = CoinDetailData( + val coinDetails = CoinDetailsData( id = "Qwsogvtv82FCd", name = "Bitcoin", symbol = "BTC", @@ -241,9 +241,9 @@ class FakeCoinApi : CoinApi { ) return Response.success( - CoinDetailApiModel( - coinDetailDataHolder = CoinDetailDataHolder( - coinDetailData = coinDetail + CoinDetailsApiModel( + coinDetailsDataHolder = CoinDetailsDataHolder( + coinDetailsData = coinDetails ) ) ) @@ -252,7 +252,7 @@ class FakeCoinApi : CoinApi { else -> { return Response.error( 404, - "Coin detail not found".toResponseBody(null) + "Coin details not found".toResponseBody(null) ) } } diff --git a/app/src/test/java/dev/shorthouse/coinwatch/data/source/remote/FakeCoinNetworkDataSource.kt b/app/src/test/java/dev/shorthouse/coinwatch/data/source/remote/FakeCoinNetworkDataSource.kt index a80beaaf..8b657e83 100644 --- a/app/src/test/java/dev/shorthouse/coinwatch/data/source/remote/FakeCoinNetworkDataSource.kt +++ b/app/src/test/java/dev/shorthouse/coinwatch/data/source/remote/FakeCoinNetworkDataSource.kt @@ -1,7 +1,7 @@ package dev.shorthouse.coinwatch.data.source.remote import dev.shorthouse.coinwatch.data.source.remote.model.CoinChartApiModel -import dev.shorthouse.coinwatch.data.source.remote.model.CoinDetailApiModel +import dev.shorthouse.coinwatch.data.source.remote.model.CoinDetailsApiModel import dev.shorthouse.coinwatch.data.source.remote.model.CoinSearchResultsApiModel import dev.shorthouse.coinwatch.data.source.remote.model.CoinsApiModel import retrofit2.Response @@ -13,8 +13,8 @@ class FakeCoinNetworkDataSource( return coinApi.getCoins(coinIds = coinIds) } - override suspend fun getCoinDetail(coinId: String): Response { - return coinApi.getCoinDetail(coinId = coinId) + override suspend fun getCoinDetails(coinId: String): Response { + return coinApi.getCoinDetails(coinId = coinId) } override suspend fun getCoinChart( diff --git a/app/src/test/java/dev/shorthouse/coinwatch/ui/screen/details/CoinDetailViewModelTest.kt b/app/src/test/java/dev/shorthouse/coinwatch/ui/screen/details/CoinDetailsViewModelTest.kt similarity index 87% rename from app/src/test/java/dev/shorthouse/coinwatch/ui/screen/details/CoinDetailViewModelTest.kt rename to app/src/test/java/dev/shorthouse/coinwatch/ui/screen/details/CoinDetailsViewModelTest.kt index 1c7be939..d9fe7034 100644 --- a/app/src/test/java/dev/shorthouse/coinwatch/ui/screen/details/CoinDetailViewModelTest.kt +++ b/app/src/test/java/dev/shorthouse/coinwatch/ui/screen/details/CoinDetailsViewModelTest.kt @@ -8,11 +8,11 @@ import dev.shorthouse.coinwatch.common.Result import dev.shorthouse.coinwatch.data.source.local.model.FavouriteCoin import dev.shorthouse.coinwatch.domain.DeleteFavouriteCoinUseCase import dev.shorthouse.coinwatch.domain.GetCoinChartUseCase -import dev.shorthouse.coinwatch.domain.GetCoinDetailUseCase +import dev.shorthouse.coinwatch.domain.GetCoinDetailsUseCase import dev.shorthouse.coinwatch.domain.InsertFavouriteCoinUseCase import dev.shorthouse.coinwatch.domain.IsCoinFavouriteUseCase import dev.shorthouse.coinwatch.model.CoinChart -import dev.shorthouse.coinwatch.model.CoinDetail +import dev.shorthouse.coinwatch.model.CoinDetails import dev.shorthouse.coinwatch.ui.model.ChartPeriod import io.mockk.MockKAnnotations import io.mockk.Runs @@ -30,7 +30,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test -class CoinDetailViewModelTest { +class CoinDetailsViewModelTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() @@ -39,7 +39,7 @@ class CoinDetailViewModelTest { private lateinit var viewModel: DetailsViewModel @RelaxedMockK - private lateinit var getCoinDetailUseCase: GetCoinDetailUseCase + private lateinit var getCoinDetailsUseCase: GetCoinDetailsUseCase @RelaxedMockK private lateinit var getCoinChartUseCase: GetCoinChartUseCase @@ -64,7 +64,7 @@ class CoinDetailViewModelTest { viewModel = DetailsViewModel( savedStateHandle = savedStateHandle, - getCoinDetailUseCase = getCoinDetailUseCase, + getCoinDetailsUseCase = getCoinDetailsUseCase, getCoinChartUseCase = getCoinChartUseCase, isCoinFavouriteUseCase = isCoinFavouriteUseCase, insertFavouriteCoinUseCase = insertFavouriteCoinUseCase, @@ -89,13 +89,13 @@ class CoinDetailViewModelTest { } @Test - fun `When coin detail returns error should have error UI state`() = runTest { + fun `When coin details returns error should have error UI state`() = runTest { // Arrange val coinChart = mockkClass(CoinChart::class) - val errorMessage = "Coin detail error" + val errorMessage = "Coin details error" val expectedUiState = DetailsUiState.Error(errorMessage) - every { getCoinDetailUseCase(any()) } returns flowOf(Result.Error(errorMessage)) + every { getCoinDetailsUseCase(any()) } returns flowOf(Result.Error(errorMessage)) every { getCoinChartUseCase(any(), any()) } returns flowOf(Result.Success(coinChart)) every { isCoinFavouriteUseCase(any()) } returns flowOf(Result.Success(false)) @@ -110,10 +110,10 @@ class CoinDetailViewModelTest { fun `When coin chart returns error should have error UI state`() = runTest { // Arrange val errorMessage = "Coin chart error" - val coinDetail = mockkClass(CoinDetail::class) + val coinDetails = mockkClass(CoinDetails::class) val expectedUiState = DetailsUiState.Error(errorMessage) - every { getCoinDetailUseCase(any()) } returns flowOf(Result.Success(coinDetail)) + every { getCoinDetailsUseCase(any()) } returns flowOf(Result.Success(coinDetails)) every { getCoinChartUseCase(any(), any()) } returns flowOf(Result.Error(errorMessage)) every { isCoinFavouriteUseCase(any()) } returns flowOf(Result.Success(false)) @@ -128,11 +128,11 @@ class CoinDetailViewModelTest { fun `When is coin favourite returns error should have error UI state`() = runTest { // Arrange val errorMessage = "Coin favourite error" - val coinDetail = mockkClass(CoinDetail::class) + val coinDetails = mockkClass(CoinDetails::class) val coinChart = mockkClass(CoinChart::class) val expectedUiState = DetailsUiState.Error(errorMessage) - every { getCoinDetailUseCase(any()) } returns flowOf(Result.Success(coinDetail)) + every { getCoinDetailsUseCase(any()) } returns flowOf(Result.Success(coinDetails)) every { getCoinChartUseCase(any(), any()) } returns flowOf(Result.Success(coinChart)) every { isCoinFavouriteUseCase(any()) } returns flowOf(Result.Error(errorMessage)) @@ -146,18 +146,18 @@ class CoinDetailViewModelTest { @Test fun `When all use cases return success should have success UI state`() = runTest { // Arrange - val coinDetail = mockkClass(CoinDetail::class) + val coinDetails = mockkClass(CoinDetails::class) val coinChart = mockkClass(CoinChart::class) val isCoinFavourite = false val expectedUiState = DetailsUiState.Success( - coinDetail = coinDetail, + coinDetails = coinDetails, coinChart = coinChart, chartPeriod = ChartPeriod.Day, isCoinFavourite = isCoinFavourite ) - every { getCoinDetailUseCase(any()) } returns flowOf(Result.Success(coinDetail)) + every { getCoinDetailsUseCase(any()) } returns flowOf(Result.Success(coinDetails)) every { getCoinChartUseCase(any(), any()) } returns flowOf(Result.Success(coinChart)) every { isCoinFavouriteUseCase(any()) } returns flowOf(Result.Success(isCoinFavourite)) @@ -172,18 +172,18 @@ class CoinDetailViewModelTest { fun `When updating chart period UI state should update with new chart period value`() = runTest { // Arrange - val coinDetail = mockkClass(CoinDetail::class) + val coinDetails = mockkClass(CoinDetails::class) val coinChart = mockkClass(CoinChart::class) val isCoinFavourite = false val expectedUiState = DetailsUiState.Success( - coinDetail = coinDetail, + coinDetails = coinDetails, coinChart = coinChart, chartPeriod = ChartPeriod.Week, isCoinFavourite = isCoinFavourite ) - every { getCoinDetailUseCase(any()) } returns flowOf(Result.Success(coinDetail)) + every { getCoinDetailsUseCase(any()) } returns flowOf(Result.Success(coinDetails)) every { getCoinChartUseCase(any(), any()) } returns flowOf(Result.Success(coinChart)) every { isCoinFavouriteUseCase(any()) } returns flowOf(Result.Success(isCoinFavourite)) From 5edcfc01eae90926ca39f8862771ceb7f405a6fb Mon Sep 17 00:00:00 2001 From: Harry S Date: Tue, 24 Oct 2023 19:25:08 +0100 Subject: [PATCH 36/78] Rename onErrorRetry -> onRefresh --- .../ui/screen/CoinDetailsScreenTest.kt | 22 ++++++++-------- .../coinwatch/ui/screen/CoinListScreenTest.kt | 8 +++--- .../ui/screen/CoinSearchScreenTest.kt | 26 +++++++++---------- .../ui/screen/details/DetailsScreen.kt | 8 +++--- .../ui/screen/favourites/FavouritesScreen.kt | 8 +++--- .../ui/screen/search/CoinSearchScreen.kt | 12 ++++----- 6 files changed, 42 insertions(+), 42 deletions(-) diff --git a/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinDetailsScreenTest.kt b/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinDetailsScreenTest.kt index 837389dc..7ff6afa4 100644 --- a/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinDetailsScreenTest.kt +++ b/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinDetailsScreenTest.kt @@ -39,7 +39,7 @@ class CoinDetailsScreenTest { onNavigateUp = {}, onClickFavouriteCoin = {}, onClickChartPeriod = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -63,7 +63,7 @@ class CoinDetailsScreenTest { onNavigateUp = {}, onClickFavouriteCoin = {}, onClickChartPeriod = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -77,8 +77,8 @@ class CoinDetailsScreenTest { } @Test - fun when_uiStateErrorRetryClicked_should_callOnErrorRetry() { - var onErrorRetryCalled = false + fun when_uiStateErrorRetryClicked_should_callOnRefresh() { + var onRefreshCalled = false val uiStateError = DetailsUiState.Error("Error message") composeTestRule.setContent { @@ -88,7 +88,7 @@ class CoinDetailsScreenTest { onNavigateUp = {}, onClickFavouriteCoin = {}, onClickChartPeriod = {}, - onErrorRetry = { onErrorRetryCalled = true } + onRefresh = { onRefreshCalled = true } ) } } @@ -97,7 +97,7 @@ class CoinDetailsScreenTest { onNodeWithText("Retry").performClick() } - assertThat(onErrorRetryCalled).isTrue() + assertThat(onRefreshCalled).isTrue() } @Test @@ -112,7 +112,7 @@ class CoinDetailsScreenTest { onNavigateUp = { onNavigateUpCalled = true }, onClickFavouriteCoin = {}, onClickChartPeriod = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -165,7 +165,7 @@ class CoinDetailsScreenTest { onNavigateUp = {}, onClickFavouriteCoin = {}, onClickChartPeriod = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -258,7 +258,7 @@ class CoinDetailsScreenTest { onNavigateUp = { onNavigateUpCalled = true }, onClickFavouriteCoin = {}, onClickChartPeriod = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -313,7 +313,7 @@ class CoinDetailsScreenTest { onNavigateUp = {}, onClickFavouriteCoin = { onClickFavouriteCoinCalled = true }, onClickChartPeriod = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -370,7 +370,7 @@ class CoinDetailsScreenTest { onNavigateUp = {}, onClickFavouriteCoin = {}, onClickChartPeriod = { onClickChartPeriodMap[it] = true }, - onErrorRetry = {} + onRefresh = {} ) } } diff --git a/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinListScreenTest.kt b/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinListScreenTest.kt index 5fb1c0ae..e9597ff7 100644 --- a/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinListScreenTest.kt +++ b/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinListScreenTest.kt @@ -68,8 +68,8 @@ class CoinListScreenTest { } @Test - fun when_uiStateErrorRetryClicked_should_callOnErrorRetry() { - var onErrorRetryCalled = false + fun when_uiStateErrorRetryClicked_should_callOnRefresh() { + var onRefreshCalled = false val uiStateError = CoinListUiState.Error("Error message") composeTestRule.setContent { @@ -77,7 +77,7 @@ class CoinListScreenTest { CoinListScreen( uiState = uiStateError, onCoinClick = {}, - onRefresh = { onErrorRetryCalled = true } + onRefresh = { onRefreshCalled = true } ) } } @@ -86,7 +86,7 @@ class CoinListScreenTest { onNodeWithText("Retry").performClick() } - assertThat(onErrorRetryCalled).isTrue() + assertThat(onRefreshCalled).isTrue() } @Test diff --git a/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinSearchScreenTest.kt b/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinSearchScreenTest.kt index 6955fc7e..ae4c845a 100644 --- a/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinSearchScreenTest.kt +++ b/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinSearchScreenTest.kt @@ -33,7 +33,7 @@ class CoinSearchScreenTest { searchQuery = "", onSearchQueryChange = {}, onCoinClick = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -47,8 +47,8 @@ class CoinSearchScreenTest { } @Test - fun when_uiStateErrorRetryClicked_should_callOnErrorRetry() { - var onErrorRetryCalled = false + fun when_uiStateErrorRetryClicked_should_callOnRefresh() { + var onRefreshCalled = false val uiStateError = CoinSearchUiState.Error("Error message") composeTestRule.setContent { @@ -58,7 +58,7 @@ class CoinSearchScreenTest { searchQuery = "", onSearchQueryChange = {}, onCoinClick = {}, - onErrorRetry = { onErrorRetryCalled = true } + onRefresh = { onRefreshCalled = true } ) } } @@ -67,7 +67,7 @@ class CoinSearchScreenTest { onNodeWithText("Retry").performClick() } - assertThat(onErrorRetryCalled).isTrue() + assertThat(onRefreshCalled).isTrue() } @Test @@ -84,7 +84,7 @@ class CoinSearchScreenTest { searchQuery = "", onSearchQueryChange = {}, onCoinClick = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -111,7 +111,7 @@ class CoinSearchScreenTest { searchQuery = searchQuery, onSearchQueryChange = {}, onCoinClick = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -137,7 +137,7 @@ class CoinSearchScreenTest { searchQuery = searchQuery, onSearchQueryChange = {}, onCoinClick = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -163,7 +163,7 @@ class CoinSearchScreenTest { searchQuery = searchQuery.value, onSearchQueryChange = { searchQuery.value = it }, onCoinClick = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -191,7 +191,7 @@ class CoinSearchScreenTest { searchQuery = searchQuery.value, onSearchQueryChange = { searchQuery.value = it }, onCoinClick = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -233,7 +233,7 @@ class CoinSearchScreenTest { searchQuery = "", onSearchQueryChange = {}, onCoinClick = {}, - onErrorRetry = {} + onRefresh = {} ) } } @@ -271,7 +271,7 @@ class CoinSearchScreenTest { searchQuery = "", onSearchQueryChange = {}, onCoinClick = { onCoinClickCalled = true }, - onErrorRetry = {} + onRefresh = {} ) } } @@ -297,7 +297,7 @@ class CoinSearchScreenTest { searchQuery = "abcdefghijk", onSearchQueryChange = {}, onCoinClick = {}, - onErrorRetry = {} + onRefresh = {} ) } } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt index 9c1b3d29..1ac5cefd 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt @@ -65,7 +65,7 @@ fun CoinDetailsScreen( onNavigateUp = { navController.navigateUp() }, onClickFavouriteCoin = { viewModel.toggleIsCoinFavourite() }, onClickChartPeriod = { viewModel.updateChartPeriod(it) }, - onErrorRetry = { viewModel.initialiseUiState() } + onRefresh = { viewModel.initialiseUiState() } ) } @@ -76,7 +76,7 @@ fun CoinDetailsScreen( onNavigateUp: () -> Unit, onClickFavouriteCoin: () -> Unit, onClickChartPeriod: (ChartPeriod) -> Unit, - onErrorRetry: () -> Unit, + onRefresh: () -> Unit, modifier: Modifier = Modifier ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() @@ -113,7 +113,7 @@ fun CoinDetailsScreen( is DetailsUiState.Error -> { ErrorState( message = uiState.message, - onRetry = onErrorRetry, + onRetry = onRefresh, onNavigateUp = onNavigateUp ) } @@ -260,7 +260,7 @@ private fun CoinDetailScreenPreview( onNavigateUp = {}, onClickFavouriteCoin = {}, onClickChartPeriod = {}, - onErrorRetry = {} + onRefresh = {} ) } } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt index 8718e09b..7a5610ec 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt @@ -47,7 +47,7 @@ fun FavouritesScreen( onCoinClick = { coin -> navController.navigate(Screen.Details.route + "/${coin.id}") }, - onErrorRetry = { viewModel.initialiseUiState() } + onRefresh = { viewModel.initialiseUiState() } ) } @@ -56,7 +56,7 @@ fun FavouritesScreen( fun FavouriteScreen( uiState: FavouritesUiState, onCoinClick: (Coin) -> Unit, - onErrorRetry: () -> Unit, + onRefresh: () -> Unit, modifier: Modifier = Modifier ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() @@ -87,7 +87,7 @@ fun FavouriteScreen( is FavouritesUiState.Error -> { ErrorState( message = uiState.message, - onRetry = onErrorRetry + onRetry = onRefresh ) } } @@ -158,7 +158,7 @@ private fun FavouritesScreenPreview( FavouriteScreen( uiState = uiState, onCoinClick = {}, - onErrorRetry = {} + onRefresh = {} ) } } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt index f163d27c..e4f776da 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt @@ -1,6 +1,5 @@ package dev.shorthouse.coinwatch.ui.screen.search -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn @@ -36,6 +35,7 @@ import dev.shorthouse.coinwatch.ui.component.ErrorState import dev.shorthouse.coinwatch.ui.previewdata.CoinSearchUiStatePreviewProvider import dev.shorthouse.coinwatch.ui.screen.search.component.CoinSearchListItem import dev.shorthouse.coinwatch.ui.screen.search.component.SearchEmptyState +import dev.shorthouse.coinwatch.ui.screen.search.component.SearchSkeletonLoader import dev.shorthouse.coinwatch.ui.theme.AppTheme import kotlinx.collections.immutable.ImmutableList @@ -53,7 +53,7 @@ fun CoinSearchScreen( onCoinClick = { coin -> navController.navigate(Screen.Details.route + "/${coin.id}") }, - onErrorRetry = { viewModel.initialiseUiState() } + onRefresh = { viewModel.initialiseUiState() } ) } @@ -63,7 +63,7 @@ fun CoinSearchScreen( searchQuery: String, onSearchQueryChange: (String) -> Unit, onCoinClick: (SearchCoin) -> Unit, - onErrorRetry: () -> Unit, + onRefresh: () -> Unit, modifier: Modifier = Modifier ) { when (uiState) { @@ -81,12 +81,12 @@ fun CoinSearchScreen( is CoinSearchUiState.Error -> { ErrorState( message = uiState.message, - onRetry = onErrorRetry + onRetry = onRefresh ) } is CoinSearchUiState.Loading -> { - Box(modifier = Modifier.fillMaxSize()) + SearchSkeletonLoader() } } } @@ -201,7 +201,7 @@ private fun CoinSearchScreenPreview( searchQuery = "", onSearchQueryChange = {}, onCoinClick = {}, - onErrorRetry = {} + onRefresh = {} ) } } From 5b13aa36d9018e7cf209228b7023e69190192ca6 Mon Sep 17 00:00:00 2001 From: Harry S Date: Tue, 24 Oct 2023 19:38:09 +0100 Subject: [PATCH 37/78] Make inner functions private --- .../screen/favourites/component/FavouritesSkeletonLoader.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesSkeletonLoader.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesSkeletonLoader.kt index fc86ac3a..0bdada8b 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesSkeletonLoader.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesSkeletonLoader.kt @@ -37,7 +37,7 @@ fun FavouritesSkeletonLoader(modifier: Modifier = Modifier) { @OptIn(ExperimentalMaterial3Api::class) @Composable -fun FavouritesSkeletonTopBar(modifier: Modifier = Modifier) { +private fun FavouritesSkeletonTopBar(modifier: Modifier = Modifier) { TopAppBar( title = { Text( @@ -56,7 +56,7 @@ fun FavouritesSkeletonTopBar(modifier: Modifier = Modifier) { } @Composable -fun FavouritesSkeletonContent(modifier: Modifier = Modifier) { +private fun FavouritesSkeletonContent(modifier: Modifier = Modifier) { LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 140.dp), contentPadding = PaddingValues(12.dp), @@ -77,7 +77,7 @@ fun FavouritesSkeletonContent(modifier: Modifier = Modifier) { @Composable @Preview -fun FavouritesSkeletonLoaderPreview() { +private fun FavouritesSkeletonLoaderPreview() { AppTheme { FavouritesSkeletonLoader() } From d9a05353e519643306548772bc3f96f46333db75 Mon Sep 17 00:00:00 2001 From: Harry S Date: Tue, 24 Oct 2023 19:38:19 +0100 Subject: [PATCH 38/78] Make inner functions private --- .../ui/screen/favourites/FavouritesScreen.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt index 7a5610ec..83d67885 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt @@ -31,6 +31,7 @@ import dev.shorthouse.coinwatch.navigation.Screen import dev.shorthouse.coinwatch.ui.component.ErrorState import dev.shorthouse.coinwatch.ui.previewdata.FavouritesUiStatePreviewProvider import dev.shorthouse.coinwatch.ui.screen.favourites.component.FavouritesEmptyState +import dev.shorthouse.coinwatch.ui.screen.favourites.component.FavouritesSkeletonLoader import dev.shorthouse.coinwatch.ui.screen.list.component.CoinFavouriteItem import dev.shorthouse.coinwatch.ui.theme.AppTheme import kotlinx.collections.immutable.ImmutableList @@ -53,7 +54,7 @@ fun FavouritesScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun FavouriteScreen( +private fun FavouriteScreen( uiState: FavouritesUiState, onCoinClick: (Coin) -> Unit, onRefresh: () -> Unit, @@ -80,8 +81,8 @@ fun FavouriteScreen( ) } - FavouritesUiState.Loading -> { - // TODO + is FavouritesUiState.Loading -> { + FavouritesSkeletonLoader() } is FavouritesUiState.Error -> { @@ -95,7 +96,7 @@ fun FavouriteScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun FavouritesTopBar( +private fun FavouritesTopBar( scrollBehavior: TopAppBarScrollBehavior, modifier: Modifier = Modifier ) { @@ -118,7 +119,7 @@ fun FavouritesTopBar( } @Composable -fun FavouritesContent( +private fun FavouritesContent( favouriteCoins: ImmutableList, onCoinClick: (Coin) -> Unit, modifier: Modifier = Modifier @@ -128,7 +129,7 @@ fun FavouritesContent( } else { LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 140.dp), - contentPadding = PaddingValues(12.dp), + contentPadding = PaddingValues(horizontal = 12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp), modifier = modifier From 1bcb21f80580e7e1de247a5767c62f8fc491a6cf Mon Sep 17 00:00:00 2001 From: Harry S Date: Tue, 24 Oct 2023 19:38:54 +0100 Subject: [PATCH 39/78] Add search skeleton loader --- .../coinwatch/ui/screen/search/CoinSearchScreen.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt index e4f776da..9684be45 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt @@ -58,7 +58,7 @@ fun CoinSearchScreen( } @Composable -fun CoinSearchScreen( +private fun CoinSearchScreen( uiState: CoinSearchUiState, searchQuery: String, onSearchQueryChange: (String) -> Unit, @@ -78,22 +78,22 @@ fun CoinSearchScreen( ) } + is CoinSearchUiState.Loading -> { + SearchSkeletonLoader() + } + is CoinSearchUiState.Error -> { ErrorState( message = uiState.message, onRetry = onRefresh ) } - - is CoinSearchUiState.Loading -> { - SearchSkeletonLoader() - } } } @OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @Composable -fun CoinSearchContent( +private fun CoinSearchContent( searchResults: ImmutableList, searchQuery: String, isSearchResultsEmpty: Boolean, From 40e3ae13b60e7a1d83b9a3e2900a6e121393ef6b Mon Sep 17 00:00:00 2001 From: Harry S Date: Tue, 24 Oct 2023 19:39:07 +0100 Subject: [PATCH 40/78] Make inner functions private --- .../dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt index 35a81e2f..f5fde66f 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt @@ -68,7 +68,7 @@ fun CoinListScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun CoinListScreen( +private fun CoinListScreen( uiState: CoinListUiState, onCoinClick: (Coin) -> Unit, onRefresh: () -> Unit, @@ -129,7 +129,7 @@ fun CoinListScreen( ) } - CoinListUiState.Loading -> { + is CoinListUiState.Loading -> { CoinListSkeletonLoader() } From 77ee0110293dcb547d8618e90e2ebd8e04d606b0 Mon Sep 17 00:00:00 2001 From: Harry S Date: Tue, 24 Oct 2023 19:39:20 +0100 Subject: [PATCH 41/78] Refactor list skeleton loader to match new screen --- .../list/component/CoinListSkeletonLoader.kt | 40 +------------------ 1 file changed, 2 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinListSkeletonLoader.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinListSkeletonLoader.kt index dbb3c45a..bf715175 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinListSkeletonLoader.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinListSkeletonLoader.kt @@ -1,26 +1,18 @@ package dev.shorthouse.coinwatch.ui.screen.list.component -import androidx.compose.foundation.layout.Arrangement 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.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CornerSize import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import dev.shorthouse.coinwatch.R import dev.shorthouse.coinwatch.ui.component.SkeletonSurface import dev.shorthouse.coinwatch.ui.theme.AppTheme @@ -41,9 +33,7 @@ fun CoinListSkeletonLoader(modifier: Modifier = Modifier) { @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun SkeletonTopAppBar( - modifier: Modifier = Modifier -) { +private fun SkeletonTopAppBar(modifier: Modifier = Modifier) { TopAppBar( title = {}, colors = TopAppBarDefaults.largeTopAppBarColors( @@ -54,34 +44,8 @@ private fun SkeletonTopAppBar( } @Composable -private fun SkeletonContent( - modifier: Modifier = Modifier -) { +private fun SkeletonContent(modifier: Modifier = Modifier) { Column(modifier = modifier.padding(start = 12.dp, top = 12.dp)) { - Text( - text = stringResource(R.string.header_favourites), - style = MaterialTheme.typography.titleMedium - ) - - Spacer(Modifier.height(8.dp)) - - Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - repeat(12) { - SkeletonSurface( - modifier = Modifier.size(width = 140.dp, height = 200.dp) - ) - } - } - - Spacer(Modifier.height(24.dp)) - - Text( - text = stringResource(R.string.header_coins), - style = MaterialTheme.typography.titleMedium - ) - - Spacer(Modifier.height(8.dp)) - SkeletonSurface( shape = MaterialTheme.shapes.medium.copy( bottomStart = CornerSize(0.dp), From 21285abeac3fe64bd67da211f25cc07291bdab54 Mon Sep 17 00:00:00 2001 From: Harry S Date: Tue, 24 Oct 2023 19:47:08 +0100 Subject: [PATCH 42/78] Standardise names and remove redundant 'coin' prefix --- .../{CoinSearchScreen.kt => SearchScreen.kt} | 32 +++++++++---------- ...{CoinSearchUiState.kt => SearchUiState.kt} | 8 ++--- ...nSearchViewModel.kt => SearchViewModel.kt} | 10 +++--- .../search/component/SearchEmptyState.kt | 4 +-- ...oinSearchListItem.kt => SearchListItem.kt} | 6 ++-- .../screen/search/CoinSearchViewModelTest.kt | 14 ++++---- 6 files changed, 36 insertions(+), 38 deletions(-) rename app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/{CoinSearchScreen.kt => SearchScreen.kt} (90%) rename app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/{CoinSearchUiState.kt => SearchUiState.kt} (62%) rename app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/{CoinSearchViewModel.kt => SearchViewModel.kt} (89%) rename app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/{CoinSearchListItem.kt => SearchListItem.kt} (97%) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchScreen.kt similarity index 90% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchScreen.kt index 9684be45..32f66ff3 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchScreen.kt @@ -32,21 +32,21 @@ import dev.shorthouse.coinwatch.R import dev.shorthouse.coinwatch.model.SearchCoin import dev.shorthouse.coinwatch.navigation.Screen import dev.shorthouse.coinwatch.ui.component.ErrorState -import dev.shorthouse.coinwatch.ui.previewdata.CoinSearchUiStatePreviewProvider -import dev.shorthouse.coinwatch.ui.screen.search.component.CoinSearchListItem +import dev.shorthouse.coinwatch.ui.previewdata.SearchUiStatePreviewProvider import dev.shorthouse.coinwatch.ui.screen.search.component.SearchEmptyState +import dev.shorthouse.coinwatch.ui.screen.search.component.SearchListItem import dev.shorthouse.coinwatch.ui.screen.search.component.SearchSkeletonLoader import dev.shorthouse.coinwatch.ui.theme.AppTheme import kotlinx.collections.immutable.ImmutableList @Composable -fun CoinSearchScreen( +fun SearchScreen( navController: NavController, - viewModel: CoinSearchViewModel = hiltViewModel() + viewModel: SearchViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - CoinSearchScreen( + SearchScreen( uiState = uiState, searchQuery = viewModel.searchQuery, onSearchQueryChange = { viewModel.updateSearchQuery(it) }, @@ -58,8 +58,8 @@ fun CoinSearchScreen( } @Composable -private fun CoinSearchScreen( - uiState: CoinSearchUiState, +private fun SearchScreen( + uiState: SearchUiState, searchQuery: String, onSearchQueryChange: (String) -> Unit, onCoinClick: (SearchCoin) -> Unit, @@ -67,8 +67,8 @@ private fun CoinSearchScreen( modifier: Modifier = Modifier ) { when (uiState) { - is CoinSearchUiState.Success -> { - CoinSearchContent( + is SearchUiState.Success -> { + SearchContent( searchResults = uiState.searchResults, searchQuery = searchQuery, isSearchResultsEmpty = uiState.queryHasNoResults, @@ -78,11 +78,11 @@ private fun CoinSearchScreen( ) } - is CoinSearchUiState.Loading -> { + is SearchUiState.Loading -> { SearchSkeletonLoader() } - is CoinSearchUiState.Error -> { + is SearchUiState.Error -> { ErrorState( message = uiState.message, onRetry = onRefresh @@ -93,7 +93,7 @@ private fun CoinSearchScreen( @OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @Composable -private fun CoinSearchContent( +private fun SearchContent( searchResults: ImmutableList, searchQuery: String, isSearchResultsEmpty: Boolean, @@ -165,7 +165,7 @@ private fun CoinSearchContent( else -> RoundedCornerShape(0.dp) } - CoinSearchListItem( + SearchListItem( searchCoin = searchCoin, onCoinClick = onCoinClick, cardShape = cardShape @@ -192,11 +192,11 @@ private fun CoinSearchContent( @Composable @Preview(showBackground = true) -private fun CoinSearchScreenPreview( - @PreviewParameter(CoinSearchUiStatePreviewProvider::class) uiState: CoinSearchUiState +private fun SearchScreenPreview( + @PreviewParameter(SearchUiStatePreviewProvider::class) uiState: SearchUiState ) { AppTheme { - CoinSearchScreen( + SearchScreen( uiState = uiState, searchQuery = "", onSearchQueryChange = {}, diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchUiState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchUiState.kt similarity index 62% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchUiState.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchUiState.kt index 63616c97..01a9f2a6 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchUiState.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchUiState.kt @@ -3,12 +3,12 @@ package dev.shorthouse.coinwatch.ui.screen.search import dev.shorthouse.coinwatch.model.SearchCoin import kotlinx.collections.immutable.ImmutableList -sealed interface CoinSearchUiState { - object Loading : CoinSearchUiState +sealed interface SearchUiState { + object Loading : SearchUiState data class Success( val searchResults: ImmutableList, val queryHasNoResults: Boolean - ) : CoinSearchUiState + ) : SearchUiState - data class Error(val message: String?) : CoinSearchUiState + data class Error(val message: String?) : SearchUiState } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchViewModel.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchViewModel.kt similarity index 89% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchViewModel.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchViewModel.kt index 23975185..d27cad88 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchViewModel.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchViewModel.kt @@ -22,10 +22,10 @@ import kotlinx.coroutines.flow.update @OptIn(FlowPreview::class) @HiltViewModel -class CoinSearchViewModel @Inject constructor( +class SearchViewModel @Inject constructor( private val getCoinSearchResultsUseCase: GetCoinSearchResultsUseCase ) : ViewModel() { - private val _uiState = MutableStateFlow(CoinSearchUiState.Loading) + private val _uiState = MutableStateFlow(SearchUiState.Loading) val uiState = _uiState.asStateFlow() var searchQuery by mutableStateOf("") @@ -45,7 +45,7 @@ class CoinSearchViewModel @Inject constructor( when (result) { is Result.Error -> { _uiState.update { - CoinSearchUiState.Error( + SearchUiState.Error( message = result.message ) } @@ -54,7 +54,7 @@ class CoinSearchViewModel @Inject constructor( val searchResults = result.data.toPersistentList() _uiState.update { - CoinSearchUiState.Success( + SearchUiState.Success( searchResults = searchResults, queryHasNoResults = searchResults.isEmpty() ) @@ -63,7 +63,7 @@ class CoinSearchViewModel @Inject constructor( } } else { _uiState.update { - CoinSearchUiState.Success( + SearchUiState.Success( searchResults = persistentListOf(), queryHasNoResults = false ) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchEmptyState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchEmptyState.kt index e56c24e9..7feb0582 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchEmptyState.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchEmptyState.kt @@ -12,9 +12,7 @@ import dev.shorthouse.coinwatch.ui.component.EmptyState import dev.shorthouse.coinwatch.ui.theme.AppTheme @Composable -fun SearchEmptyState( - modifier: Modifier = Modifier -) { +fun SearchEmptyState(modifier: Modifier = Modifier) { EmptyState( image = painterResource(R.drawable.empty_state_search), title = stringResource(R.string.empty_state_search_title), diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/CoinSearchListItem.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchListItem.kt similarity index 97% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/CoinSearchListItem.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchListItem.kt index ddfe03e2..97ae1fd9 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/CoinSearchListItem.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchListItem.kt @@ -29,7 +29,7 @@ import dev.shorthouse.coinwatch.ui.previewdata.SearchCoinPreviewProvider import dev.shorthouse.coinwatch.ui.theme.AppTheme @Composable -fun CoinSearchListItem( +fun SearchListItem( searchCoin: SearchCoin, onCoinClick: (SearchCoin) -> Unit, cardShape: Shape, @@ -84,11 +84,11 @@ fun CoinSearchListItem( @Composable @Preview -private fun CoinSearchListItemPreview( +private fun SearchListItemPreview( @PreviewParameter(SearchCoinPreviewProvider::class) searchCoin: SearchCoin ) { AppTheme { - CoinSearchListItem( + SearchListItem( searchCoin = searchCoin, onCoinClick = {}, cardShape = MaterialTheme.shapes.medium diff --git a/app/src/test/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchViewModelTest.kt b/app/src/test/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchViewModelTest.kt index a1540e53..c5327c98 100644 --- a/app/src/test/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchViewModelTest.kt +++ b/app/src/test/java/dev/shorthouse/coinwatch/ui/screen/search/CoinSearchViewModelTest.kt @@ -22,7 +22,7 @@ class CoinSearchViewModelTest { val mainDispatcherRule = MainDispatcherRule() // Class under test - private lateinit var viewModel: CoinSearchViewModel + private lateinit var viewModel: SearchViewModel @MockK private lateinit var getCoinSearchResultsUseCase: GetCoinSearchResultsUseCase @@ -31,7 +31,7 @@ class CoinSearchViewModelTest { fun setup() { MockKAnnotations.init(this) - viewModel = CoinSearchViewModel( + viewModel = SearchViewModel( getCoinSearchResultsUseCase = getCoinSearchResultsUseCase ) } @@ -44,7 +44,7 @@ class CoinSearchViewModelTest { @Test fun `When ViewModel is initialised should have loading UI state`() = runTest { // Arrange - val expectedUiState = CoinSearchUiState.Loading + val expectedUiState = SearchUiState.Loading // Act @@ -66,7 +66,7 @@ class CoinSearchViewModelTest { @Test fun `When search query is empty should return empty search results list`() = runTest { // Arrange - val expectedUiState = CoinSearchUiState.Success( + val expectedUiState = SearchUiState.Success( searchResults = persistentListOf(), queryHasNoResults = false ) @@ -86,7 +86,7 @@ class CoinSearchViewModelTest { val searchQuery = "bit" val errorMessage = "Unable to fetch coin search results" - val expectedUiState = CoinSearchUiState.Error( + val expectedUiState = SearchUiState.Error( message = errorMessage ) @@ -124,7 +124,7 @@ class CoinSearchViewModelTest { ) ) - val expectedUiState = CoinSearchUiState.Success( + val expectedUiState = SearchUiState.Success( searchResults = searchResults, queryHasNoResults = false ) @@ -164,7 +164,7 @@ class CoinSearchViewModelTest { val searchResults = persistentListOf() - val expectedUiState = CoinSearchUiState.Success( + val expectedUiState = SearchUiState.Success( searchResults = searchResults, queryHasNoResults = true ) From 2d784b5c398fc37ed6c5af1d376fb40a86a258be Mon Sep 17 00:00:00 2001 From: Harry S Date: Tue, 24 Oct 2023 19:47:11 +0100 Subject: [PATCH 43/78] Standardise names and remove redundant 'coin' prefix --- .../ui/screen/favourites/FavouritesScreen.kt | 2 +- .../ui/screen/list/CoinListScreen.kt | 20 +++++++++---------- .../{CoinsEmptyState.kt => ListEmptyState.kt} | 6 +++--- .../{CoinListItem.kt => ListItem.kt} | 7 +++---- ...keletonLoader.kt => ListSkeletonLoader.kt} | 14 ++++++------- .../{CoinSearchPrompt.kt => SearchPrompt.kt} | 6 +++--- 6 files changed, 27 insertions(+), 28 deletions(-) rename app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/{CoinsEmptyState.kt => ListEmptyState.kt} (89%) rename app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/{CoinListItem.kt => ListItem.kt} (96%) rename app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/{CoinListSkeletonLoader.kt => ListSkeletonLoader.kt} (84%) rename app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/{CoinSearchPrompt.kt => SearchPrompt.kt} (92%) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt index 83d67885..86950f41 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt @@ -32,7 +32,7 @@ import dev.shorthouse.coinwatch.ui.component.ErrorState import dev.shorthouse.coinwatch.ui.previewdata.FavouritesUiStatePreviewProvider import dev.shorthouse.coinwatch.ui.screen.favourites.component.FavouritesEmptyState import dev.shorthouse.coinwatch.ui.screen.favourites.component.FavouritesSkeletonLoader -import dev.shorthouse.coinwatch.ui.screen.list.component.CoinFavouriteItem +import dev.shorthouse.coinwatch.ui.screen.favourites.component.CoinFavouriteItem import dev.shorthouse.coinwatch.ui.theme.AppTheme import kotlinx.collections.immutable.ImmutableList diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt index f5fde66f..88602d22 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt @@ -41,11 +41,11 @@ import dev.shorthouse.coinwatch.model.Coin import dev.shorthouse.coinwatch.navigation.Screen import dev.shorthouse.coinwatch.ui.component.ErrorState import dev.shorthouse.coinwatch.ui.model.TimeOfDay -import dev.shorthouse.coinwatch.ui.previewdata.CoinListUiStatePreviewProvider -import dev.shorthouse.coinwatch.ui.screen.list.component.CoinListItem -import dev.shorthouse.coinwatch.ui.screen.list.component.CoinListSkeletonLoader -import dev.shorthouse.coinwatch.ui.screen.list.component.CoinSearchPrompt -import dev.shorthouse.coinwatch.ui.screen.list.component.CoinsEmptyState +import dev.shorthouse.coinwatch.ui.previewdata.ListUiStatePreviewProvider +import dev.shorthouse.coinwatch.ui.screen.list.component.ListEmptyState +import dev.shorthouse.coinwatch.ui.screen.list.component.ListItem +import dev.shorthouse.coinwatch.ui.screen.list.component.ListSkeletonLoader +import dev.shorthouse.coinwatch.ui.screen.list.component.SearchPrompt import dev.shorthouse.coinwatch.ui.theme.AppTheme import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.launch @@ -130,7 +130,7 @@ private fun CoinListScreen( } is CoinListUiState.Loading -> { - CoinListSkeletonLoader() + ListSkeletonLoader() } is CoinListUiState.Error -> { @@ -179,7 +179,7 @@ private fun CoinListContent( modifier: Modifier = Modifier ) { if (coins.isEmpty()) { - CoinsEmptyState() + ListEmptyState() } else { LazyColumn( state = lazyListState, @@ -206,7 +206,7 @@ private fun CoinListContent( else -> RoundedCornerShape(0.dp) } - CoinListItem( + ListItem( coin = coinListItem, onCoinClick = { onCoinClick(coinListItem) }, cardShape = cardShape @@ -215,7 +215,7 @@ private fun CoinListContent( ) item { - CoinSearchPrompt(modifier = Modifier.padding(vertical = 12.dp)) + SearchPrompt(modifier = Modifier.padding(vertical = 12.dp)) } } } @@ -224,7 +224,7 @@ private fun CoinListContent( @Composable @Preview(showBackground = true) private fun CoinListScreenPreview( - @PreviewParameter(CoinListUiStatePreviewProvider::class) uiState: CoinListUiState + @PreviewParameter(ListUiStatePreviewProvider::class) uiState: CoinListUiState ) { AppTheme { CoinListScreen( diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinsEmptyState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/ListEmptyState.kt similarity index 89% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinsEmptyState.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/ListEmptyState.kt index 17bac25f..33676fb5 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinsEmptyState.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/ListEmptyState.kt @@ -12,7 +12,7 @@ import dev.shorthouse.coinwatch.ui.component.EmptyState import dev.shorthouse.coinwatch.ui.theme.AppTheme @Composable -fun CoinsEmptyState(modifier: Modifier = Modifier) { +fun ListEmptyState(modifier: Modifier = Modifier) { EmptyState( image = painterResource(R.drawable.empty_state_coins), title = stringResource(R.string.empty_state_coins_title), @@ -29,8 +29,8 @@ fun CoinsEmptyState(modifier: Modifier = Modifier) { @Composable @Preview -fun CoinsEmptyStatePreview() { +private fun ListEmptyStatePreview() { AppTheme { - CoinsEmptyState() + ListEmptyState() } } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinListItem.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/ListItem.kt similarity index 96% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinListItem.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/ListItem.kt index bbaac2a3..ed1604db 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinListItem.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/ListItem.kt @@ -26,12 +26,11 @@ import coil.decode.SvgDecoder import coil.request.ImageRequest import dev.shorthouse.coinwatch.model.Coin import dev.shorthouse.coinwatch.ui.component.PercentageChange -import dev.shorthouse.coinwatch.ui.component.PercentageChangeChip import dev.shorthouse.coinwatch.ui.previewdata.CoinPreviewProvider import dev.shorthouse.coinwatch.ui.theme.AppTheme @Composable -fun CoinListItem( +fun ListItem( coin: Coin, onCoinClick: (Coin) -> Unit, cardShape: Shape, @@ -97,11 +96,11 @@ fun CoinListItem( @Composable @Preview -private fun CoinListItemPreview( +private fun ListItemPreview( @PreviewParameter(CoinPreviewProvider::class) coin: Coin ) { AppTheme { - CoinListItem( + ListItem( coin = coin, onCoinClick = {}, cardShape = MaterialTheme.shapes.medium diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinListSkeletonLoader.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/ListSkeletonLoader.kt similarity index 84% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinListSkeletonLoader.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/ListSkeletonLoader.kt index bf715175..62f3497e 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinListSkeletonLoader.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/ListSkeletonLoader.kt @@ -17,13 +17,13 @@ import dev.shorthouse.coinwatch.ui.component.SkeletonSurface import dev.shorthouse.coinwatch.ui.theme.AppTheme @Composable -fun CoinListSkeletonLoader(modifier: Modifier = Modifier) { +fun ListSkeletonLoader(modifier: Modifier = Modifier) { Scaffold( topBar = { - SkeletonTopAppBar() + ListSkeletonTopBar() }, content = { scaffoldPadding -> - SkeletonContent( + ListSkeletonContent( modifier = Modifier.padding(scaffoldPadding) ) }, @@ -33,7 +33,7 @@ fun CoinListSkeletonLoader(modifier: Modifier = Modifier) { @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun SkeletonTopAppBar(modifier: Modifier = Modifier) { +private fun ListSkeletonTopBar(modifier: Modifier = Modifier) { TopAppBar( title = {}, colors = TopAppBarDefaults.largeTopAppBarColors( @@ -44,7 +44,7 @@ private fun SkeletonTopAppBar(modifier: Modifier = Modifier) { } @Composable -private fun SkeletonContent(modifier: Modifier = Modifier) { +private fun ListSkeletonContent(modifier: Modifier = Modifier) { Column(modifier = modifier.padding(start = 12.dp, top = 12.dp)) { SkeletonSurface( shape = MaterialTheme.shapes.medium.copy( @@ -60,8 +60,8 @@ private fun SkeletonContent(modifier: Modifier = Modifier) { @Composable @Preview -fun CoinListSkeletonLoaderPreview() { +private fun ListSkeletonLoaderPreview() { AppTheme { - CoinListSkeletonLoader() + ListSkeletonLoader() } } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinSearchPrompt.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/SearchPrompt.kt similarity index 92% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinSearchPrompt.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/SearchPrompt.kt index 564bc2ab..faec9672 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinSearchPrompt.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/SearchPrompt.kt @@ -15,7 +15,7 @@ import dev.shorthouse.coinwatch.R import dev.shorthouse.coinwatch.ui.theme.AppTheme @Composable -fun CoinSearchPrompt(modifier: Modifier = Modifier) { +fun SearchPrompt(modifier: Modifier = Modifier) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, @@ -39,8 +39,8 @@ fun CoinSearchPrompt(modifier: Modifier = Modifier) { @Composable @Preview -fun CoinSearchPromptPreview() { +fun SearchPromptPreview() { AppTheme { - CoinSearchPrompt() + SearchPrompt() } } From dc9b20741fcf263d618c6b08ea8ee159366b7b3f Mon Sep 17 00:00:00 2001 From: Harry S Date: Tue, 24 Oct 2023 19:47:14 +0100 Subject: [PATCH 44/78] Standardise names and remove redundant 'coin' prefix --- ...viewProvider.kt => ListUiStatePreviewProvider.kt} | 2 +- ...ewProvider.kt => SearchUiStatePreviewProvider.kt} | 12 ++++++------ .../coinwatch/ui/screen/details/DetailsScreen.kt | 4 ++-- .../component/FavouriteItem.kt} | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) rename app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/{CoinListUiStatePreviewProvider.kt => ListUiStatePreviewProvider.kt} (99%) rename app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/{CoinSearchUiStatePreviewProvider.kt => SearchUiStatePreviewProvider.kt} (81%) rename app/src/main/java/dev/shorthouse/coinwatch/ui/screen/{list/component/CoinFavouriteItem.kt => favourites/component/FavouriteItem.kt} (98%) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinListUiStatePreviewProvider.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/ListUiStatePreviewProvider.kt similarity index 99% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinListUiStatePreviewProvider.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/ListUiStatePreviewProvider.kt index d7d0d016..60e09fdf 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinListUiStatePreviewProvider.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/ListUiStatePreviewProvider.kt @@ -10,7 +10,7 @@ import dev.shorthouse.coinwatch.ui.screen.list.CoinListUiState import java.math.BigDecimal import kotlinx.collections.immutable.persistentListOf -class CoinListUiStatePreviewProvider : PreviewParameterProvider { +class ListUiStatePreviewProvider : PreviewParameterProvider { override val values = sequenceOf( CoinListUiState.Success( coins = coins, diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinSearchUiStatePreviewProvider.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/SearchUiStatePreviewProvider.kt similarity index 81% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinSearchUiStatePreviewProvider.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/SearchUiStatePreviewProvider.kt index d5c2404b..b10df59e 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinSearchUiStatePreviewProvider.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/SearchUiStatePreviewProvider.kt @@ -3,21 +3,21 @@ package dev.shorthouse.coinwatch.ui.previewdata import androidx.compose.ui.tooling.preview.PreviewParameterProvider import dev.shorthouse.coinwatch.model.SearchCoin import dev.shorthouse.coinwatch.ui.previewdata.CoinSearchPreviewData.searchResults -import dev.shorthouse.coinwatch.ui.screen.search.CoinSearchUiState +import dev.shorthouse.coinwatch.ui.screen.search.SearchUiState import kotlinx.collections.immutable.persistentListOf -class CoinSearchUiStatePreviewProvider : PreviewParameterProvider { +class SearchUiStatePreviewProvider : PreviewParameterProvider { override val values = sequenceOf( - CoinSearchUiState.Success( + SearchUiState.Success( searchResults = searchResults, queryHasNoResults = false ), - CoinSearchUiState.Success( + SearchUiState.Success( searchResults = persistentListOf(), queryHasNoResults = true ), - CoinSearchUiState.Loading, - CoinSearchUiState.Error( + SearchUiState.Loading, + SearchUiState.Error( message = "Error searching coins" ) ) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt index 1ac5cefd..9d74aadc 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt @@ -46,7 +46,7 @@ import dev.shorthouse.coinwatch.model.CoinChart import dev.shorthouse.coinwatch.model.CoinDetails import dev.shorthouse.coinwatch.ui.component.ErrorState import dev.shorthouse.coinwatch.ui.model.ChartPeriod -import dev.shorthouse.coinwatch.ui.previewdata.CoinDetailsUiStatePreviewProvider +import dev.shorthouse.coinwatch.ui.previewdata.DetailsUiStatePreviewProvider import dev.shorthouse.coinwatch.ui.screen.details.component.CoinChartCard import dev.shorthouse.coinwatch.ui.screen.details.component.CoinChartRangeCard import dev.shorthouse.coinwatch.ui.screen.details.component.CoinDetailsSkeletonLoader @@ -252,7 +252,7 @@ private fun CoinDetailsContent( @Composable @Preview private fun CoinDetailScreenPreview( - @PreviewParameter(CoinDetailsUiStatePreviewProvider::class) uiState: DetailsUiState + @PreviewParameter(DetailsUiStatePreviewProvider::class) uiState: DetailsUiState ) { AppTheme { CoinDetailsScreen( diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinFavouriteItem.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouriteItem.kt similarity index 98% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinFavouriteItem.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouriteItem.kt index adfd77c0..030ab646 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/CoinFavouriteItem.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouriteItem.kt @@ -1,4 +1,4 @@ -package dev.shorthouse.coinwatch.ui.screen.list.component +package dev.shorthouse.coinwatch.ui.screen.favourites.component import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box From 879830de136e67138c162df1c555dacc0eab65c2 Mon Sep 17 00:00:00 2001 From: Harry S Date: Tue, 24 Oct 2023 19:47:18 +0100 Subject: [PATCH 45/78] Standardise names and remove redundant 'coin' prefix --- .../ui/screen/CoinSearchScreenTest.kt | 44 +++++++++---------- .../coinwatch/navigation/AppNavHost.kt | 4 +- ...er.kt => DetailsUiStatePreviewProvider.kt} | 2 +- 3 files changed, 25 insertions(+), 25 deletions(-) rename app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/{CoinDetailsUiStatePreviewProvider.kt => DetailsUiStatePreviewProvider.kt} (98%) diff --git a/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinSearchScreenTest.kt b/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinSearchScreenTest.kt index ae4c845a..dccf59ef 100644 --- a/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinSearchScreenTest.kt +++ b/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinSearchScreenTest.kt @@ -10,8 +10,8 @@ import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextInput import com.google.common.truth.Truth.assertThat import dev.shorthouse.coinwatch.model.SearchCoin -import dev.shorthouse.coinwatch.ui.screen.search.CoinSearchScreen -import dev.shorthouse.coinwatch.ui.screen.search.CoinSearchUiState +import dev.shorthouse.coinwatch.ui.screen.search.SearchScreen +import dev.shorthouse.coinwatch.ui.screen.search.SearchUiState import dev.shorthouse.coinwatch.ui.theme.AppTheme import kotlinx.collections.immutable.persistentListOf import org.junit.Rule @@ -24,11 +24,11 @@ class CoinSearchScreenTest { @Test fun when_uiStateLoading_should_showSkeletonLoader() { - val uiStateError = CoinSearchUiState.Error("Error message") + val uiStateError = SearchUiState.Error("Error message") composeTestRule.setContent { AppTheme { - CoinSearchScreen( + SearchScreen( uiState = uiStateError, searchQuery = "", onSearchQueryChange = {}, @@ -49,11 +49,11 @@ class CoinSearchScreenTest { @Test fun when_uiStateErrorRetryClicked_should_callOnRefresh() { var onRefreshCalled = false - val uiStateError = CoinSearchUiState.Error("Error message") + val uiStateError = SearchUiState.Error("Error message") composeTestRule.setContent { AppTheme { - CoinSearchScreen( + SearchScreen( uiState = uiStateError, searchQuery = "", onSearchQueryChange = {}, @@ -72,14 +72,14 @@ class CoinSearchScreenTest { @Test fun when_uiStateSuccess_should_showExpectedContent() { - val uiStateSuccess = CoinSearchUiState.Success( + val uiStateSuccess = SearchUiState.Success( searchResults = persistentListOf(), queryHasNoResults = false ) composeTestRule.setContent { AppTheme { - CoinSearchScreen( + SearchScreen( uiState = uiStateSuccess, searchQuery = "", onSearchQueryChange = {}, @@ -99,14 +99,14 @@ class CoinSearchScreenTest { fun when_searchQueryEntered_should_displaySearchQuery() { val searchQuery = "Bitcoin" - val uiStateSuccess = CoinSearchUiState.Success( + val uiStateSuccess = SearchUiState.Success( searchResults = persistentListOf(), queryHasNoResults = false ) composeTestRule.setContent { AppTheme { - CoinSearchScreen( + SearchScreen( uiState = uiStateSuccess, searchQuery = searchQuery, onSearchQueryChange = {}, @@ -125,14 +125,14 @@ class CoinSearchScreenTest { fun when_searchQueryEntered_should_displayClearSearchButton() { val searchQuery = "Bitcoin" - val uiStateSuccess = CoinSearchUiState.Success( + val uiStateSuccess = SearchUiState.Success( searchResults = persistentListOf(), queryHasNoResults = false ) composeTestRule.setContent { AppTheme { - CoinSearchScreen( + SearchScreen( uiState = uiStateSuccess, searchQuery = searchQuery, onSearchQueryChange = {}, @@ -151,14 +151,14 @@ class CoinSearchScreenTest { fun when_clearSearchClicked_should_clearSearchQuery() { val searchQuery = mutableStateOf("Bitcoin") - val uiStateSuccess = CoinSearchUiState.Success( + val uiStateSuccess = SearchUiState.Success( searchResults = persistentListOf(), queryHasNoResults = false ) composeTestRule.setContent { AppTheme { - CoinSearchScreen( + SearchScreen( uiState = uiStateSuccess, searchQuery = searchQuery.value, onSearchQueryChange = { searchQuery.value = it }, @@ -179,14 +179,14 @@ class CoinSearchScreenTest { fun when_typingInSearchBar_should_updateSearchQuery() { val searchQuery = mutableStateOf("") - val uiStateSuccess = CoinSearchUiState.Success( + val uiStateSuccess = SearchUiState.Success( searchResults = persistentListOf(), queryHasNoResults = false ) composeTestRule.setContent { AppTheme { - CoinSearchScreen( + SearchScreen( uiState = uiStateSuccess, searchQuery = searchQuery.value, onSearchQueryChange = { searchQuery.value = it }, @@ -221,14 +221,14 @@ class CoinSearchScreenTest { ) ) - val uiStateSuccess = CoinSearchUiState.Success( + val uiStateSuccess = SearchUiState.Success( searchResults = searchResults, queryHasNoResults = false ) composeTestRule.setContent { AppTheme { - CoinSearchScreen( + SearchScreen( uiState = uiStateSuccess, searchQuery = "", onSearchQueryChange = {}, @@ -259,14 +259,14 @@ class CoinSearchScreenTest { ) ) - val uiStateSuccess = CoinSearchUiState.Success( + val uiStateSuccess = SearchUiState.Success( searchResults = searchResults, queryHasNoResults = false ) composeTestRule.setContent { AppTheme { - CoinSearchScreen( + SearchScreen( uiState = uiStateSuccess, searchQuery = "", onSearchQueryChange = {}, @@ -285,14 +285,14 @@ class CoinSearchScreenTest { @Test fun when_searchQueryHasNoResults_should_displaySearchEmptyState() { - val uiStateSuccess = CoinSearchUiState.Success( + val uiStateSuccess = SearchUiState.Success( searchResults = persistentListOf(), queryHasNoResults = true ) composeTestRule.setContent { AppTheme { - CoinSearchScreen( + SearchScreen( uiState = uiStateSuccess, searchQuery = "abcdefghijk", onSearchQueryChange = {}, diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt index e8264bc0..f71cfa21 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt @@ -22,7 +22,7 @@ import androidx.navigation.compose.rememberNavController import dev.shorthouse.coinwatch.ui.screen.details.CoinDetailsScreen import dev.shorthouse.coinwatch.ui.screen.favourites.FavouritesScreen import dev.shorthouse.coinwatch.ui.screen.list.CoinListScreen -import dev.shorthouse.coinwatch.ui.screen.search.CoinSearchScreen +import dev.shorthouse.coinwatch.ui.screen.search.SearchScreen import kotlinx.collections.immutable.persistentListOf @Composable @@ -106,7 +106,7 @@ fun AppNavHost( FavouritesScreen(navController = navController) } composable(route = NavigationBarScreen.Search.route) { - CoinSearchScreen(navController = navController) + SearchScreen(navController = navController) } composable(route = Screen.Details.route + "/{coinId}") { CoinDetailsScreen(navController = navController) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinDetailsUiStatePreviewProvider.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/DetailsUiStatePreviewProvider.kt similarity index 98% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinDetailsUiStatePreviewProvider.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/DetailsUiStatePreviewProvider.kt index 333ddb7d..1718e549 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/CoinDetailsUiStatePreviewProvider.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/DetailsUiStatePreviewProvider.kt @@ -10,7 +10,7 @@ import dev.shorthouse.coinwatch.ui.screen.details.DetailsUiState import java.math.BigDecimal import kotlinx.collections.immutable.persistentListOf -class CoinDetailsUiStatePreviewProvider : PreviewParameterProvider { +class DetailsUiStatePreviewProvider : PreviewParameterProvider { override val values = sequenceOf( DetailsUiState.Success( CoinDetails( From 1087c53dc29752545d666178be67d105faaf2e21 Mon Sep 17 00:00:00 2001 From: Harry S Date: Wed, 25 Oct 2023 18:40:47 +0100 Subject: [PATCH 46/78] Add favourites error string --- app/src/main/res/values/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f6efed84..8423989d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -54,4 +54,5 @@ Market Search Favourites + Unable to fetch favourite coins From 9509b7165c07c0cccb0d38d2d9fcb9b1ecf029fd Mon Sep 17 00:00:00 2001 From: Harry S Date: Wed, 25 Oct 2023 18:41:18 +0100 Subject: [PATCH 47/78] Standardise and capture SSOT for colours indrawable resources --- .../main/res/drawable/empty_state_coins.xml | 216 +++++++++--------- .../drawable/empty_state_favourite_coins.xml | 147 ++++++------ .../main/res/drawable/empty_state_search.xml | 120 +++++----- app/src/main/res/drawable/error_state.xml | 102 ++++----- app/src/main/res/values/colors.xml | 8 + 5 files changed, 293 insertions(+), 300 deletions(-) diff --git a/app/src/main/res/drawable/empty_state_coins.xml b/app/src/main/res/drawable/empty_state_coins.xml index e0025b7f..50a9e523 100644 --- a/app/src/main/res/drawable/empty_state_coins.xml +++ b/app/src/main/res/drawable/empty_state_coins.xml @@ -3,112 +3,112 @@ android:height="532.8dp" android:viewportWidth="423.88" android:viewportHeight="532.8"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/empty_state_favourite_coins.xml b/app/src/main/res/drawable/empty_state_favourite_coins.xml index 2883495c..28958d32 100644 --- a/app/src/main/res/drawable/empty_state_favourite_coins.xml +++ b/app/src/main/res/drawable/empty_state_favourite_coins.xml @@ -3,85 +3,70 @@ android:height="483.5dp" android:viewportWidth="485.83" android:viewportHeight="483.5"> - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/empty_state_search.xml b/app/src/main/res/drawable/empty_state_search.xml index 9fc1e873..9844c3c1 100644 --- a/app/src/main/res/drawable/empty_state_search.xml +++ b/app/src/main/res/drawable/empty_state_search.xml @@ -3,64 +3,64 @@ android:height="515.46dp" android:viewportWidth="552.81" android:viewportHeight="515.46"> - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/error_state.xml b/app/src/main/res/drawable/error_state.xml index eb71c8c2..c3e04db8 100644 --- a/app/src/main/res/drawable/error_state.xml +++ b/app/src/main/res/drawable/error_state.xml @@ -3,55 +3,55 @@ android:height="658dp" android:viewportWidth="729" android:viewportHeight="658"> - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index b6b6a853..142575ee 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -2,4 +2,12 @@ #19284D #19284D + #253667 + #6F6C88 + #3F3d56 + #FFFFFF + #F2F2F2 + #97636b + #FFB6B6 + #A0616A From 6599afd2bea10679076815f9c9cb806164e468ba Mon Sep 17 00:00:00 2001 From: Harry S Date: Wed, 25 Oct 2023 18:41:32 +0100 Subject: [PATCH 48/78] Remove unneeded test components --- .../coinwatch/ui/component/ErrorStateTest.kt | 37 ------------------- .../coinwatch/ui/screen/CoinListScreenTest.kt | 31 +++++----------- 2 files changed, 10 insertions(+), 58 deletions(-) diff --git a/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/component/ErrorStateTest.kt b/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/component/ErrorStateTest.kt index 6dcb4bfa..8f7bdc2e 100644 --- a/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/component/ErrorStateTest.kt +++ b/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/component/ErrorStateTest.kt @@ -68,41 +68,4 @@ class ErrorStateTest { assertThat(onRetryCalled).isTrue() } } - - @Test - fun when_navigateUpProvided_should_displayBackButton() { - composeTestRule.setContent { - AppTheme { - ErrorState( - message = null, - onRetry = {}, - onNavigateUp = {} - ) - } - } - - composeTestRule.apply { - onNodeWithContentDescription("Back").assertIsDisplayed().assertHasClickAction() - } - } - - @Test - fun when_navigateUpClicked_should_callOnNavigateUp() { - var onNavigateUpCalled = false - - composeTestRule.setContent { - AppTheme { - ErrorState( - message = null, - onRetry = {}, - onNavigateUp = { onNavigateUpCalled = true } - ) - } - } - - composeTestRule.apply { - onNodeWithContentDescription("Back").performClick() - assertThat(onNavigateUpCalled).isTrue() - } - } } diff --git a/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinListScreenTest.kt b/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinListScreenTest.kt index e9597ff7..269d3e55 100644 --- a/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinListScreenTest.kt +++ b/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinListScreenTest.kt @@ -11,7 +11,6 @@ import com.google.common.truth.Truth.assertThat import dev.shorthouse.coinwatch.model.Coin import dev.shorthouse.coinwatch.model.Percentage import dev.shorthouse.coinwatch.model.Price -import dev.shorthouse.coinwatch.ui.model.TimeOfDay import dev.shorthouse.coinwatch.ui.screen.list.CoinListScreen import dev.shorthouse.coinwatch.ui.screen.list.CoinListUiState import dev.shorthouse.coinwatch.ui.theme.AppTheme @@ -92,8 +91,7 @@ class CoinListScreenTest { @Test fun when_uiStateSuccess_should_showExpectedContent() { val uiStateSuccess = CoinListUiState.Success( - coins = persistentListOf(), - timeOfDay = TimeOfDay.Morning + coins = persistentListOf() ) composeTestRule.setContent { @@ -115,8 +113,7 @@ class CoinListScreenTest { @Test fun when_uiStateSuccess_favouriteCoinsEmpty_should_showEmptyState() { val uiStateSuccess = CoinListUiState.Success( - coins = persistentListOf(), - timeOfDay = TimeOfDay.Morning + coins = persistentListOf() ) composeTestRule.setContent { @@ -140,8 +137,7 @@ class CoinListScreenTest { @Test fun when_uiStateSuccess_favouriteCoinsList_should_showExpectedContent() { val uiStateSuccess = CoinListUiState.Success( - coins = persistentListOf(), - timeOfDay = TimeOfDay.Morning + coins = persistentListOf() ) composeTestRule.setContent { @@ -181,8 +177,7 @@ class CoinListScreenTest { @Test fun when_uiStateSuccess_coinsEmpty_should_showEmptyState() { val uiStateSuccess = CoinListUiState.Success( - coins = persistentListOf(), - timeOfDay = TimeOfDay.Morning + coins = persistentListOf() ) composeTestRule.setContent { @@ -247,8 +242,7 @@ class CoinListScreenTest { BigDecimal("1.00") ) ) - ), - timeOfDay = TimeOfDay.Morning + ) ) composeTestRule.setContent { @@ -302,8 +296,7 @@ class CoinListScreenTest { BigDecimal("29471.20179209623") ) ) - ), - timeOfDay = TimeOfDay.Morning + ) ) composeTestRule.setContent { @@ -328,8 +321,7 @@ class CoinListScreenTest { var onCoinClickCalled = false val uiStateSuccess = CoinListUiState.Success( - coins = persistentListOf(), - timeOfDay = TimeOfDay.Morning + coins = persistentListOf() ) composeTestRule.setContent { @@ -352,8 +344,7 @@ class CoinListScreenTest { @Test fun when_timeOfDayMorning_should_showMorningGreeting() { val uiStateSuccess = CoinListUiState.Success( - coins = persistentListOf(), - timeOfDay = TimeOfDay.Morning + coins = persistentListOf() ) composeTestRule.setContent { @@ -374,8 +365,7 @@ class CoinListScreenTest { @Test fun when_timeOfDayAfternoon_should_showAfternoonGreeting() { val uiStateSuccess = CoinListUiState.Success( - coins = persistentListOf(), - timeOfDay = TimeOfDay.Afternoon + coins = persistentListOf() ) composeTestRule.setContent { @@ -396,8 +386,7 @@ class CoinListScreenTest { @Test fun when_timeOfDayEvening_should_showEveningGreeting() { val uiStateSuccess = CoinListUiState.Success( - coins = persistentListOf(), - timeOfDay = TimeOfDay.Evening + coins = persistentListOf() ) composeTestRule.setContent { From b1ff464dd85d7da286ae398368b92a0dae8d1586 Mon Sep 17 00:00:00 2001 From: Harry S Date: Wed, 25 Oct 2023 18:41:44 +0100 Subject: [PATCH 49/78] ktlint formatting --- .../java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt index f71cfa21..301bc095 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt @@ -26,9 +26,7 @@ import dev.shorthouse.coinwatch.ui.screen.search.SearchScreen import kotlinx.collections.immutable.persistentListOf @Composable -fun AppNavHost( - modifier: Modifier = Modifier -) { +fun AppNavHost(modifier: Modifier = Modifier) { val navController = rememberNavController() val navigationBarScreens = remember { From f356895dcc75ae7363c19fee7f930bddd9c4c4e1 Mon Sep 17 00:00:00 2001 From: Harry S Date: Wed, 25 Oct 2023 18:41:55 +0100 Subject: [PATCH 50/78] Steamline empty and error states --- .../coinwatch/ui/component/EmptyState.kt | 26 +--- .../coinwatch/ui/component/ErrorState.kt | 123 ++++++------------ 2 files changed, 49 insertions(+), 100 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/component/EmptyState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/component/EmptyState.kt index ef9f925c..30e876c0 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/component/EmptyState.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/component/EmptyState.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import dev.shorthouse.coinwatch.R @@ -27,7 +28,6 @@ fun EmptyState( title: String, subtitle: @Composable () -> Unit, modifier: Modifier = Modifier - // onRetry: (() -> Unit)? = null, ) { Box( modifier = modifier.background(MaterialTheme.colorScheme.background) @@ -56,34 +56,22 @@ fun EmptyState( Spacer(Modifier.height(4.dp)) subtitle() - - Spacer(Modifier.height(24.dp)) - -// Button( -// onClick = onRetry, -// shape = MaterialTheme.shapes.small, -// colors = ButtonDefaults.buttonColors( -// containerColor = MaterialTheme.colorScheme.surface, -// contentColor = MaterialTheme.colorScheme.onSurface -// ) -// ) { -// Text( -// text = stringResource(R.string.button_retry), -// style = MaterialTheme.typography.titleSmall -// ) -// } } } } @Composable -@Preview(showBackground = true) +@Preview private fun EmptyStatePreview() { EmptyState( image = painterResource(R.drawable.empty_state_coins), title = "No coins", subtitle = { - Text(text = "Please try again later") + Text( + text = stringResource(R.string.empty_state_coins_subtitle), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) } ) } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/component/ErrorState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/component/ErrorState.kt index 5c49e2bb..a1f30408 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/component/ErrorState.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/component/ErrorState.kt @@ -3,25 +3,16 @@ package dev.shorthouse.coinwatch.ui.component import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ArrowBack import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -32,90 +23,60 @@ import androidx.compose.ui.unit.dp import dev.shorthouse.coinwatch.R import dev.shorthouse.coinwatch.ui.theme.AppTheme -@OptIn(ExperimentalMaterial3Api::class) @Composable fun ErrorState( message: String?, onRetry: () -> Unit, - modifier: Modifier = Modifier, - onNavigateUp: (() -> Unit)? = null + modifier: Modifier = Modifier ) { - Scaffold( - topBar = { - TopAppBar( - title = {}, - navigationIcon = { - onNavigateUp?.let { - IconButton(onClick = onNavigateUp) { - Icon( - imageVector = Icons.Rounded.ArrowBack, - tint = MaterialTheme.colorScheme.onBackground, - contentDescription = stringResource(R.string.cd_top_bar_back) - ) - } - } - }, - colors = TopAppBarDefaults.largeTopAppBarColors( - containerColor = MaterialTheme.colorScheme.background - ) - ) - }, - content = { scaffoldPadding -> - Box( - modifier = modifier - .padding(scaffoldPadding) - .background(MaterialTheme.colorScheme.background) - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier - .fillMaxSize() - .padding(12.dp) - ) { - Image( - painter = painterResource(R.drawable.error_state), - contentDescription = stringResource(R.string.cd_error_state), - modifier = Modifier.size(250.dp) - ) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = modifier + .fillMaxSize() + .padding(12.dp) + .background(MaterialTheme.colorScheme.background) + ) { + Image( + painter = painterResource(R.drawable.error_state), + contentDescription = stringResource(R.string.cd_error_state), + modifier = Modifier.size(250.dp) + ) - Spacer(Modifier.height(12.dp)) + Spacer(Modifier.height(12.dp)) - Text( - text = stringResource(R.string.error_occurred), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onBackground - ) + Text( + text = stringResource(R.string.error_occurred), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onBackground + ) - Spacer(Modifier.height(4.dp)) + Spacer(Modifier.height(4.dp)) - message?.let { - Text( - text = it, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } + message?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } - Spacer(Modifier.height(24.dp)) + Spacer(Modifier.height(24.dp)) - Button( - onClick = onRetry, - shape = MaterialTheme.shapes.small, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface - ) - ) { - Text( - text = stringResource(R.string.button_retry), - style = MaterialTheme.typography.titleSmall - ) - } - } - } + Button( + onClick = onRetry, + shape = MaterialTheme.shapes.small, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface + ) + ) { + Text( + text = stringResource(R.string.button_retry), + style = MaterialTheme.typography.titleSmall + ) } - ) + } } @Composable From d402ca712bea25a3db05d753088191f25018c440 Mon Sep 17 00:00:00 2001 From: Harry S Date: Wed, 25 Oct 2023 18:42:08 +0100 Subject: [PATCH 51/78] Standardise UI state orders in preview providers --- .../ui/previewdata/DetailsUiStatePreviewProvider.kt | 4 ++-- .../previewdata/FavouritesUiStatePreviewProvider.kt | 4 ++-- .../ui/previewdata/ListUiStatePreviewProvider.kt | 11 ++++------- .../ui/previewdata/SearchUiStatePreviewProvider.kt | 4 ++-- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/DetailsUiStatePreviewProvider.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/DetailsUiStatePreviewProvider.kt index 1718e549..65228cd6 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/DetailsUiStatePreviewProvider.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/DetailsUiStatePreviewProvider.kt @@ -168,7 +168,7 @@ class DetailsUiStatePreviewProvider : PreviewParameterProvider { chartPeriod = ChartPeriod.Week, isCoinFavourite = true ), - DetailsUiState.Loading, - DetailsUiState.Error("No internet connection") + DetailsUiState.Error("No internet connection"), + DetailsUiState.Loading ) } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/FavouritesUiStatePreviewProvider.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/FavouritesUiStatePreviewProvider.kt index 9aa45399..b12719ad 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/FavouritesUiStatePreviewProvider.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/FavouritesUiStatePreviewProvider.kt @@ -17,8 +17,8 @@ class FavouritesUiStatePreviewProvider : PreviewParameterProvider { override val values = sequenceOf( CoinListUiState.Success( - coins = coins, - timeOfDay = TimeOfDay.Evening + coins = coins ), CoinListUiState.Success( - coins = persistentListOf(), - timeOfDay = TimeOfDay.Morning + coins = persistentListOf() ), - CoinListUiState.Loading, - CoinListUiState.Error("No internet connection") + CoinListUiState.Error("No internet connection"), + CoinListUiState.Loading ) } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/SearchUiStatePreviewProvider.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/SearchUiStatePreviewProvider.kt index b10df59e..4571df7e 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/SearchUiStatePreviewProvider.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/SearchUiStatePreviewProvider.kt @@ -16,10 +16,10 @@ class SearchUiStatePreviewProvider : PreviewParameterProvider { searchResults = persistentListOf(), queryHasNoResults = true ), - SearchUiState.Loading, SearchUiState.Error( message = "Error searching coins" - ) + ), + SearchUiState.Loading ) } From bc0eaad296544e6bf0afd48b78b7dc672c9cd62d Mon Sep 17 00:00:00 2001 From: Harry S Date: Wed, 25 Oct 2023 18:42:26 +0100 Subject: [PATCH 52/78] Standardise skeleton loaders to match new container functionality --- .../component/FavouritesSkeletonLoader.kt | 45 +------------------ .../list/component/ListSkeletonLoader.kt | 37 +-------------- 2 files changed, 3 insertions(+), 79 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesSkeletonLoader.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesSkeletonLoader.kt index 0bdada8b..e5c6b25b 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesSkeletonLoader.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesSkeletonLoader.kt @@ -3,63 +3,20 @@ package dev.shorthouse.coinwatch.ui.screen.favourites.component import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import dev.shorthouse.coinwatch.R import dev.shorthouse.coinwatch.ui.component.SkeletonSurface import dev.shorthouse.coinwatch.ui.theme.AppTheme @Composable fun FavouritesSkeletonLoader(modifier: Modifier = Modifier) { - Scaffold( - topBar = { - FavouritesSkeletonTopBar() - }, - content = { scaffoldPadding -> - FavouritesSkeletonContent(modifier = Modifier.padding(scaffoldPadding)) - }, - modifier = modifier - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun FavouritesSkeletonTopBar(modifier: Modifier = Modifier) { - TopAppBar( - title = { - Text( - text = stringResource(R.string.favourites_screen), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.offset(x = (-4).dp) - ) - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.background, - scrolledContainerColor = MaterialTheme.colorScheme.background - ), - modifier = modifier - ) -} - -@Composable -private fun FavouritesSkeletonContent(modifier: Modifier = Modifier) { LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 140.dp), - contentPadding = PaddingValues(12.dp), + contentPadding = PaddingValues(horizontal = 12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp), userScrollEnabled = false, diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/ListSkeletonLoader.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/ListSkeletonLoader.kt index 62f3497e..9196dfb0 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/ListSkeletonLoader.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/ListSkeletonLoader.kt @@ -4,11 +4,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CornerSize -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @@ -18,42 +14,13 @@ import dev.shorthouse.coinwatch.ui.theme.AppTheme @Composable fun ListSkeletonLoader(modifier: Modifier = Modifier) { - Scaffold( - topBar = { - ListSkeletonTopBar() - }, - content = { scaffoldPadding -> - ListSkeletonContent( - modifier = Modifier.padding(scaffoldPadding) - ) - }, - modifier = modifier - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun ListSkeletonTopBar(modifier: Modifier = Modifier) { - TopAppBar( - title = {}, - colors = TopAppBarDefaults.largeTopAppBarColors( - containerColor = MaterialTheme.colorScheme.background - ), - modifier = modifier - ) -} - -@Composable -private fun ListSkeletonContent(modifier: Modifier = Modifier) { - Column(modifier = modifier.padding(start = 12.dp, top = 12.dp)) { + Column(modifier = modifier.padding(horizontal = 12.dp)) { SkeletonSurface( shape = MaterialTheme.shapes.medium.copy( bottomStart = CornerSize(0.dp), bottomEnd = CornerSize(0.dp) ), - modifier = Modifier - .fillMaxSize() - .padding(end = 12.dp) + modifier = Modifier.fillMaxSize() ) } } From 4d47a806d85a3a2310afb23529eedacd7e07408c Mon Sep 17 00:00:00 2001 From: Harry S Date: Wed, 25 Oct 2023 18:42:47 +0100 Subject: [PATCH 53/78] Standardise and reorder UI state classes / objects --- .../coinwatch/ui/screen/details/DetailsUiState.kt | 2 +- .../coinwatch/ui/screen/favourites/FavouritesUiState.kt | 2 +- .../shorthouse/coinwatch/ui/screen/list/CoinListUiState.kt | 6 ++---- .../shorthouse/coinwatch/ui/screen/search/SearchUiState.kt | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsUiState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsUiState.kt index 607b630f..436ea808 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsUiState.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsUiState.kt @@ -5,7 +5,6 @@ import dev.shorthouse.coinwatch.model.CoinDetails import dev.shorthouse.coinwatch.ui.model.ChartPeriod sealed interface DetailsUiState { - object Loading : DetailsUiState data class Success( val coinDetails: CoinDetails, val coinChart: CoinChart, @@ -14,4 +13,5 @@ sealed interface DetailsUiState { ) : DetailsUiState data class Error(val message: String?) : DetailsUiState + object Loading : DetailsUiState } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesUiState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesUiState.kt index 80d4f5a8..23e60bdf 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesUiState.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesUiState.kt @@ -4,10 +4,10 @@ import dev.shorthouse.coinwatch.model.Coin import kotlinx.collections.immutable.ImmutableList sealed interface FavouritesUiState { - object Loading : FavouritesUiState data class Success( val favouriteCoins: ImmutableList ) : FavouritesUiState data class Error(val message: String?) : FavouritesUiState + object Loading : FavouritesUiState } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListUiState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListUiState.kt index 388f5422..4f8ca1b9 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListUiState.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListUiState.kt @@ -1,15 +1,13 @@ package dev.shorthouse.coinwatch.ui.screen.list import dev.shorthouse.coinwatch.model.Coin -import dev.shorthouse.coinwatch.ui.model.TimeOfDay import kotlinx.collections.immutable.ImmutableList sealed interface CoinListUiState { - object Loading : CoinListUiState data class Success( - val coins: ImmutableList, - val timeOfDay: TimeOfDay + val coins: ImmutableList ) : CoinListUiState data class Error(val message: String?) : CoinListUiState + object Loading : CoinListUiState } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchUiState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchUiState.kt index 01a9f2a6..26ba53ae 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchUiState.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchUiState.kt @@ -4,11 +4,11 @@ import dev.shorthouse.coinwatch.model.SearchCoin import kotlinx.collections.immutable.ImmutableList sealed interface SearchUiState { - object Loading : SearchUiState data class Success( val searchResults: ImmutableList, val queryHasNoResults: Boolean ) : SearchUiState data class Error(val message: String?) : SearchUiState + object Loading : SearchUiState } From 3f7129ff37cd1d8178c85a089be782bf58ab4eb6 Mon Sep 17 00:00:00 2001 From: Harry S Date: Wed, 25 Oct 2023 18:42:59 +0100 Subject: [PATCH 54/78] ktlint formatting --- .../shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt index 9d74aadc..b7288573 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt @@ -113,8 +113,7 @@ fun CoinDetailsScreen( is DetailsUiState.Error -> { ErrorState( message = uiState.message, - onRetry = onRefresh, - onNavigateUp = onNavigateUp + onRetry = onRefresh ) } } From a6313d72f0733a65690c9d54944938d01557e90d Mon Sep 17 00:00:00 2001 From: Harry S Date: Wed, 25 Oct 2023 18:43:22 +0100 Subject: [PATCH 55/78] Refactor favourites screen to use new UI state content pattern --- .../ui/screen/favourites/FavouritesScreen.kt | 53 +++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt index 86950f41..293fc296 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt @@ -3,7 +3,6 @@ package dev.shorthouse.coinwatch.ui.screen.favourites import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid @@ -30,9 +29,9 @@ import dev.shorthouse.coinwatch.model.Coin import dev.shorthouse.coinwatch.navigation.Screen import dev.shorthouse.coinwatch.ui.component.ErrorState import dev.shorthouse.coinwatch.ui.previewdata.FavouritesUiStatePreviewProvider +import dev.shorthouse.coinwatch.ui.screen.favourites.component.CoinFavouriteItem import dev.shorthouse.coinwatch.ui.screen.favourites.component.FavouritesEmptyState import dev.shorthouse.coinwatch.ui.screen.favourites.component.FavouritesSkeletonLoader -import dev.shorthouse.coinwatch.ui.screen.favourites.component.CoinFavouriteItem import dev.shorthouse.coinwatch.ui.theme.AppTheme import kotlinx.collections.immutable.ImmutableList @@ -62,36 +61,37 @@ private fun FavouriteScreen( ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - when (uiState) { - is FavouritesUiState.Success -> { - Scaffold( - topBar = { - FavouritesTopBar( - scrollBehavior = scrollBehavior - ) - }, - content = { scaffoldPadding -> + Scaffold( + topBar = { + FavouritesTopBar( + scrollBehavior = scrollBehavior + ) + }, + content = { scaffoldPadding -> + when (uiState) { + is FavouritesUiState.Success -> { FavouritesContent( favouriteCoins = uiState.favouriteCoins, onCoinClick = onCoinClick, modifier = Modifier.padding(scaffoldPadding) ) - }, - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection) - ) - } + } - is FavouritesUiState.Loading -> { - FavouritesSkeletonLoader() - } + is FavouritesUiState.Error -> { + ErrorState( + message = stringResource(R.string.error_state_favourite_coins), + onRetry = onRefresh, + modifier = Modifier.padding(scaffoldPadding) + ) + } - is FavouritesUiState.Error -> { - ErrorState( - message = uiState.message, - onRetry = onRefresh - ) - } - } + is FavouritesUiState.Loading -> { + FavouritesSkeletonLoader(modifier = Modifier.padding(scaffoldPadding)) + } + } + }, + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection) + ) } @OptIn(ExperimentalMaterial3Api::class) @@ -105,8 +105,7 @@ private fun FavouritesTopBar( Text( text = stringResource(R.string.favourites_screen), style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.offset(x = (-4).dp) + color = MaterialTheme.colorScheme.onBackground ) }, colors = TopAppBarDefaults.topAppBarColors( From 2e8834ddea0e9bf03e4c006ef9f965bef256aaf0 Mon Sep 17 00:00:00 2001 From: Harry S Date: Wed, 25 Oct 2023 18:43:52 +0100 Subject: [PATCH 56/78] Remove private from top level search composable --- .../dev/shorthouse/coinwatch/ui/screen/search/SearchScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchScreen.kt index 32f66ff3..0f21c046 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchScreen.kt @@ -58,7 +58,7 @@ fun SearchScreen( } @Composable -private fun SearchScreen( +fun SearchScreen( uiState: SearchUiState, searchQuery: String, onSearchQueryChange: (String) -> Unit, From 1eeb172953df4f0d961407c090af8e223475d70e Mon Sep 17 00:00:00 2001 From: Harry S Date: Wed, 25 Oct 2023 18:44:08 +0100 Subject: [PATCH 57/78] Refactor list screen to use new UI state content pattern --- .../ui/screen/list/CoinListScreen.kt | 122 ++++++++---------- 1 file changed, 56 insertions(+), 66 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt index 88602d22..ffc8a60b 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt @@ -40,7 +40,6 @@ import dev.shorthouse.coinwatch.R import dev.shorthouse.coinwatch.model.Coin import dev.shorthouse.coinwatch.navigation.Screen import dev.shorthouse.coinwatch.ui.component.ErrorState -import dev.shorthouse.coinwatch.ui.model.TimeOfDay import dev.shorthouse.coinwatch.ui.previewdata.ListUiStatePreviewProvider import dev.shorthouse.coinwatch.ui.screen.list.component.ListEmptyState import dev.shorthouse.coinwatch.ui.screen.list.component.ListItem @@ -68,98 +67,89 @@ fun CoinListScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun CoinListScreen( +fun CoinListScreen( uiState: CoinListUiState, onCoinClick: (Coin) -> Unit, onRefresh: () -> Unit, modifier: Modifier = Modifier ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + val lazyListState = rememberLazyListState() + val scope = rememberCoroutineScope() + val showJumpToTopFab by remember { + derivedStateOf { + lazyListState.firstVisibleItemIndex > 0 + } + } - when (uiState) { - is CoinListUiState.Success -> { - val lazyListState = rememberLazyListState() - val scope = rememberCoroutineScope() - val showJumpToTopFab by remember { - derivedStateOf { - lazyListState.firstVisibleItemIndex > 0 - } - } - - Scaffold( - topBar = { - CoinListTopBar( - timeOfDay = uiState.timeOfDay, - scrollBehavior = scrollBehavior - ) - }, - content = { scaffoldPadding -> + Scaffold( + topBar = { + CoinListTopBar(scrollBehavior = scrollBehavior) + }, + content = { scaffoldPadding -> + when (uiState) { + is CoinListUiState.Success -> { CoinListContent( coins = uiState.coins, onCoinClick = onCoinClick, lazyListState = lazyListState, modifier = Modifier.padding(scaffoldPadding) ) - }, - floatingActionButton = { - AnimatedVisibility( - visible = showJumpToTopFab, - enter = scaleIn(), - exit = scaleOut() - ) { - SmallFloatingActionButton( - onClick = { - scope.launch { - scrollBehavior.state.heightOffset = 0f - lazyListState.animateScrollToItem(0) - } - }, - containerColor = MaterialTheme.colorScheme.background, - contentColor = MaterialTheme.colorScheme.onBackground, - content = { - Icon( - imageVector = Icons.Rounded.KeyboardDoubleArrowUp, - contentDescription = stringResource(R.string.cd_list_scroll_top) - ) - } - ) - } - }, - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection) - ) - } + } - is CoinListUiState.Loading -> { - ListSkeletonLoader() - } + is CoinListUiState.Error -> { + ErrorState( + message = uiState.message, + onRetry = onRefresh, + modifier = Modifier.padding(scaffoldPadding) + ) + } - is CoinListUiState.Error -> { - ErrorState( - message = uiState.message, - onRetry = onRefresh - ) - } - } + is CoinListUiState.Loading -> { + ListSkeletonLoader(modifier = Modifier.padding(scaffoldPadding)) + } + } + }, + floatingActionButton = { + AnimatedVisibility( + visible = showJumpToTopFab, + enter = scaleIn(), + exit = scaleOut() + ) { + SmallFloatingActionButton( + onClick = { + scope.launch { + scrollBehavior.state.heightOffset = 0f + lazyListState.animateScrollToItem(0) + } + }, + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground, + content = { + Icon( + imageVector = Icons.Rounded.KeyboardDoubleArrowUp, + contentDescription = stringResource(R.string.cd_list_scroll_top) + ) + } + ) + } + }, + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection) + ) } @Composable @OptIn(ExperimentalMaterial3Api::class) private fun CoinListTopBar( - timeOfDay: TimeOfDay, scrollBehavior: TopAppBarScrollBehavior, modifier: Modifier = Modifier ) { TopAppBar( title = { Text( - text = when (timeOfDay) { - TimeOfDay.Morning -> stringResource(R.string.good_morning) - TimeOfDay.Afternoon -> stringResource(R.string.good_afternoon) - TimeOfDay.Evening -> stringResource(R.string.good_evening) - }, + text = stringResource(R.string.market_screen), style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.offset(x = (-4).dp) + color = MaterialTheme.colorScheme.onBackground ) }, colors = TopAppBarDefaults.topAppBarColors( From 6214a4c021d170cab58f8852ee28c213073f8ece Mon Sep 17 00:00:00 2001 From: Harry S Date: Wed, 25 Oct 2023 18:44:23 +0100 Subject: [PATCH 58/78] Remove now defunct morning / afternoon / evening logic to match new patterns --- .../ui/screen/list/CoinListViewModel.kt | 30 ++----------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListViewModel.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListViewModel.kt index 6a9d69da..9060f4d2 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListViewModel.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListViewModel.kt @@ -1,22 +1,16 @@ package dev.shorthouse.coinwatch.ui.screen.list -import androidx.annotation.VisibleForTesting import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dev.shorthouse.coinwatch.common.Result import dev.shorthouse.coinwatch.domain.GetCoinsUseCase -import dev.shorthouse.coinwatch.ui.model.TimeOfDay -import java.time.LocalTime import javax.inject.Inject -import kotlin.time.Duration.Companion.minutes import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update @HiltViewModel @@ -34,15 +28,8 @@ class CoinListViewModel @Inject constructor( _uiState.update { CoinListUiState.Loading } val coinsFlow = getCoinsUseCase() - val currentHourFlow = flow { - emit(LocalTime.now().hour) - delay(5.minutes.inWholeMilliseconds) - } - combine( - coinsFlow, - currentHourFlow - ) { coinsResult, currentHour -> + coinsFlow.onEach { coinsResult -> when (coinsResult) { is Result.Error -> { _uiState.update { CoinListUiState.Error(coinsResult.message) } @@ -50,25 +37,14 @@ class CoinListViewModel @Inject constructor( is Result.Success -> { val coins = coinsResult.data.toImmutableList() - val timeOfDay = calculateTimeOfDay(currentHour) _uiState.update { CoinListUiState.Success( - coins = coins, - timeOfDay = timeOfDay + coins = coins ) } } } }.launchIn(viewModelScope) } - - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - fun calculateTimeOfDay(currentHour: Int): TimeOfDay { - return when (currentHour) { - in 0..11 -> TimeOfDay.Morning - in 12..17 -> TimeOfDay.Afternoon - else -> TimeOfDay.Evening - } - } } From 40f8695f87972f9a36d396115c2581830b81bc2b Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Thu, 26 Oct 2023 18:46:25 +0100 Subject: [PATCH 59/78] Standardise screen states --- .../ui/screen/favourites/FavouritesScreen.kt | 12 +- .../component/FavouritesEmptyState.kt | 56 +++---- .../component/FavouritesSkeletonLoader.kt | 3 +- .../screen/list/component/ListEmptyState.kt | 27 ++-- .../list/component/ListSkeletonLoader.kt | 6 +- .../ui/screen/search/SearchScreen.kt | 147 +++++++++--------- .../search/component/SearchEmptyState.kt | 26 ++-- .../search/component/SearchSkeletonLoader.kt | 50 +----- 8 files changed, 153 insertions(+), 174 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt index 293fc296..ee58475c 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt @@ -53,7 +53,7 @@ fun FavouritesScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun FavouriteScreen( +fun FavouriteScreen( uiState: FavouritesUiState, onCoinClick: (Coin) -> Unit, onRefresh: () -> Unit, @@ -96,7 +96,7 @@ private fun FavouriteScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun FavouritesTopBar( +fun FavouritesTopBar( scrollBehavior: TopAppBarScrollBehavior, modifier: Modifier = Modifier ) { @@ -118,13 +118,17 @@ private fun FavouritesTopBar( } @Composable -private fun FavouritesContent( +fun FavouritesContent( favouriteCoins: ImmutableList, onCoinClick: (Coin) -> Unit, modifier: Modifier = Modifier ) { if (favouriteCoins.isEmpty()) { - FavouritesEmptyState(modifier = Modifier.fillMaxSize()) + FavouritesEmptyState( + modifier = modifier + .fillMaxSize() + .padding(12.dp) + ) } else { LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 140.dp), diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesEmptyState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesEmptyState.kt index 53eee5e0..2ce0a2ca 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesEmptyState.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesEmptyState.kt @@ -1,5 +1,6 @@ package dev.shorthouse.coinwatch.ui.screen.favourites.component +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -21,40 +22,41 @@ import dev.shorthouse.coinwatch.ui.theme.AppTheme @Composable fun FavouritesEmptyState(modifier: Modifier = Modifier) { - EmptyState( - image = painterResource(R.drawable.empty_state_favourite_coins), - title = stringResource(R.string.empty_state_favourite_coins_title), - subtitle = { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = stringResource(R.string.empty_state_favourite_coins_subtitle_start), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + Column(modifier = modifier) { + EmptyState( + image = painterResource(R.drawable.empty_state_favourite_coins), + title = stringResource(R.string.empty_state_favourite_coins_title), + subtitle = { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResource(R.string.empty_state_favourite_coins_subtitle_start), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) - Icon( - imageVector = Icons.Rounded.StarOutline, - contentDescription = stringResource(R.string.cd_top_bar_favourite), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .size(22.dp) - .padding(start = 2.dp, top = 2.dp, end = 2.dp) - ) + Icon( + imageVector = Icons.Rounded.StarOutline, + contentDescription = stringResource(R.string.cd_top_bar_favourite), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .size(22.dp) + .padding(start = 2.dp, top = 2.dp, end = 2.dp) + ) - Text( - text = stringResource(R.string.empty_state_favourite_coins_subtitle_end), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + Text( + text = stringResource(R.string.empty_state_favourite_coins_subtitle_end), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } } - }, - modifier = modifier - ) + ) + } } @Composable @Preview -fun FavouritesEmptyStatePreview() { +private fun FavouritesEmptyStatePreview() { AppTheme { FavouritesEmptyState() } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesSkeletonLoader.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesSkeletonLoader.kt index e5c6b25b..fca070d7 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesSkeletonLoader.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesSkeletonLoader.kt @@ -2,6 +2,7 @@ package dev.shorthouse.coinwatch.ui.screen.favourites.component import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid @@ -20,7 +21,7 @@ fun FavouritesSkeletonLoader(modifier: Modifier = Modifier) { horizontalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp), userScrollEnabled = false, - modifier = modifier + modifier = modifier.fillMaxSize() ) { repeat(20) { item { diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/ListEmptyState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/ListEmptyState.kt index 33676fb5..d0b9bbcd 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/ListEmptyState.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/ListEmptyState.kt @@ -1,5 +1,6 @@ package dev.shorthouse.coinwatch.ui.screen.list.component +import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -13,18 +14,20 @@ import dev.shorthouse.coinwatch.ui.theme.AppTheme @Composable fun ListEmptyState(modifier: Modifier = Modifier) { - EmptyState( - image = painterResource(R.drawable.empty_state_coins), - title = stringResource(R.string.empty_state_coins_title), - subtitle = { - Text( - text = stringResource(R.string.empty_state_coins_subtitle), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - }, - modifier = modifier - ) + Column(modifier = modifier) { + EmptyState( + image = painterResource(R.drawable.empty_state_coins), + title = stringResource(R.string.empty_state_coins_title), + subtitle = { + Text( + text = stringResource(R.string.empty_state_coins_subtitle), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + modifier = Modifier + ) + } } @Composable diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/ListSkeletonLoader.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/ListSkeletonLoader.kt index 9196dfb0..dff928e6 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/ListSkeletonLoader.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/ListSkeletonLoader.kt @@ -14,7 +14,11 @@ import dev.shorthouse.coinwatch.ui.theme.AppTheme @Composable fun ListSkeletonLoader(modifier: Modifier = Modifier) { - Column(modifier = modifier.padding(horizontal = 12.dp)) { + Column( + modifier = modifier + .fillMaxSize() + .padding(horizontal = 12.dp) + ) { SkeletonSurface( shape = MaterialTheme.shapes.medium.copy( bottomStart = CornerSize(0.dp), diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchScreen.kt index 0f21c046..f782357f 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchScreen.kt @@ -2,6 +2,7 @@ package dev.shorthouse.coinwatch.ui.screen.search import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape @@ -57,6 +58,7 @@ fun SearchScreen( ) } +@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @Composable fun SearchScreen( uiState: SearchUiState, @@ -65,41 +67,6 @@ fun SearchScreen( onCoinClick: (SearchCoin) -> Unit, onRefresh: () -> Unit, modifier: Modifier = Modifier -) { - when (uiState) { - is SearchUiState.Success -> { - SearchContent( - searchResults = uiState.searchResults, - searchQuery = searchQuery, - isSearchResultsEmpty = uiState.queryHasNoResults, - onSearchQueryChange = onSearchQueryChange, - onCoinClick = onCoinClick, - modifier = modifier - ) - } - - is SearchUiState.Loading -> { - SearchSkeletonLoader() - } - - is SearchUiState.Error -> { - ErrorState( - message = uiState.message, - onRetry = onRefresh - ) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) -@Composable -private fun SearchContent( - searchResults: ImmutableList, - searchQuery: String, - isSearchResultsEmpty: Boolean, - onSearchQueryChange: (String) -> Unit, - onCoinClick: (SearchCoin) -> Unit, - modifier: Modifier = Modifier ) { val keyboardController = LocalSoftwareKeyboardController.current @@ -135,51 +102,37 @@ private fun SearchContent( } }, content = { - if (isSearchResultsEmpty) { - SearchEmptyState() - } else { - LazyColumn( - contentPadding = PaddingValues(12.dp), - modifier = Modifier.fillMaxSize() - ) { - items( - count = searchResults.size, - key = { index -> searchResults[index].id }, - itemContent = { index -> - val searchCoin = searchResults[index] - - val cardShape = when { - searchResults.size == 1 -> MaterialTheme.shapes.medium - - index == 0 -> MaterialTheme.shapes.medium.copy( - bottomStart = CornerSize(0.dp), - bottomEnd = CornerSize(0.dp) - ) - - index == searchResults.lastIndex -> - MaterialTheme.shapes.medium.copy( - topStart = CornerSize(0.dp), - topEnd = CornerSize(0.dp) - ) - - else -> RoundedCornerShape(0.dp) - } + when (uiState) { + is SearchUiState.Success -> { + SearchContent( + searchResults = uiState.searchResults, + queryHasNoResults = uiState.queryHasNoResults, + onCoinClick = onCoinClick + ) + } - SearchListItem( - searchCoin = searchCoin, - onCoinClick = onCoinClick, - cardShape = cardShape - ) - } + is SearchUiState.Error -> { + ErrorState( + message = uiState.message, + onRetry = onRefresh, + modifier = Modifier.padding(bottom = 10.dp) ) } + + is SearchUiState.Loading -> { + SearchSkeletonLoader() + } } }, colors = SearchBarDefaults.colors( containerColor = MaterialTheme.colorScheme.background, dividerColor = MaterialTheme.colorScheme.surface, inputFieldColors = TextFieldDefaults.colors( - cursorColor = MaterialTheme.colorScheme.onSurface + cursorColor = MaterialTheme.colorScheme.onSurface, + focusedIndicatorColor = MaterialTheme.colorScheme.surface, + unfocusedIndicatorColor = MaterialTheme.colorScheme.surface, + disabledIndicatorColor = MaterialTheme.colorScheme.surface, + errorIndicatorColor = MaterialTheme.colorScheme.surface ) ), enabled = true, @@ -190,6 +143,58 @@ private fun SearchContent( ) } +@Composable +fun SearchContent( + searchResults: ImmutableList, + queryHasNoResults: Boolean, + onCoinClick: (SearchCoin) -> Unit, + modifier: Modifier = Modifier +) { + if (queryHasNoResults) { + SearchEmptyState( + modifier = modifier + .fillMaxSize() + .padding(12.dp) + ) + } else { + LazyColumn( + contentPadding = PaddingValues(12.dp), + modifier = modifier.fillMaxSize() + ) { + items( + count = searchResults.size, + key = { index -> searchResults[index].id }, + itemContent = { index -> + val searchCoin = searchResults[index] + + val cardShape = when { + searchResults.size == 1 -> MaterialTheme.shapes.medium + + index == 0 -> MaterialTheme.shapes.medium.copy( + bottomStart = CornerSize(0.dp), + bottomEnd = CornerSize(0.dp) + ) + + index == searchResults.lastIndex -> + MaterialTheme.shapes.medium.copy( + topStart = CornerSize(0.dp), + topEnd = CornerSize(0.dp) + ) + + else -> RoundedCornerShape(0.dp) + } + + SearchListItem( + searchCoin = searchCoin, + onCoinClick = onCoinClick, + cardShape = cardShape + ) + } + ) + } + } +} + @Composable @Preview(showBackground = true) private fun SearchScreenPreview( diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchEmptyState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchEmptyState.kt index 7feb0582..e2a9bf63 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchEmptyState.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchEmptyState.kt @@ -1,5 +1,6 @@ package dev.shorthouse.coinwatch.ui.screen.search.component +import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -13,18 +14,19 @@ import dev.shorthouse.coinwatch.ui.theme.AppTheme @Composable fun SearchEmptyState(modifier: Modifier = Modifier) { - EmptyState( - image = painterResource(R.drawable.empty_state_search), - title = stringResource(R.string.empty_state_search_title), - subtitle = { - Text( - text = stringResource(R.string.empty_state_search_subtitle), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - }, - modifier = modifier - ) + Column(modifier = modifier) { + EmptyState( + image = painterResource(R.drawable.empty_state_search), + title = stringResource(R.string.empty_state_search_title), + subtitle = { + Text( + text = stringResource(R.string.empty_state_search_subtitle), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + ) + } } @Composable diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchSkeletonLoader.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchSkeletonLoader.kt index 0df4bb4a..96d4000b 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchSkeletonLoader.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/component/SearchSkeletonLoader.kt @@ -1,62 +1,20 @@ package dev.shorthouse.coinwatch.ui.screen.search.component -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Search -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SearchBar -import androidx.compose.material3.SearchBarDefaults -import androidx.compose.material3.Text -import androidx.compose.material3.TextFieldDefaults +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import dev.shorthouse.coinwatch.R import dev.shorthouse.coinwatch.ui.theme.AppTheme -@OptIn(ExperimentalMaterial3Api::class) @Composable fun SearchSkeletonLoader(modifier: Modifier = Modifier) { - SearchBar( - query = "", - onQueryChange = {}, - onSearch = {}, - placeholder = { - Text( - text = stringResource(R.string.search_coins_hint), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - }, - leadingIcon = { - IconButton(onClick = {}) { - Icon( - imageVector = Icons.Rounded.Search, - tint = MaterialTheme.colorScheme.onSurface, - contentDescription = stringResource(R.string.cd_top_bar_back) - ) - } - }, - content = {}, - colors = SearchBarDefaults.colors( - containerColor = MaterialTheme.colorScheme.background, - dividerColor = MaterialTheme.colorScheme.surface, - inputFieldColors = TextFieldDefaults.colors( - cursorColor = MaterialTheme.colorScheme.onSurface - ) - ), - active = true, - onActiveChange = {}, - modifier = modifier - ) + Box(modifier = modifier.fillMaxSize()) } @Composable @Preview -fun SearchSkeletonLoaderPreview() { +private fun SearchSkeletonLoaderPreview() { AppTheme { SearchSkeletonLoader() } From 127e5ddd4f020b3fcec51862b9f0327e9e91d75d Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Thu, 26 Oct 2023 18:46:36 +0100 Subject: [PATCH 60/78] Standardise screen states --- .../java/dev/shorthouse/coinwatch/ui/component/ErrorState.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/component/ErrorState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/component/ErrorState.kt index a1f30408..432888f2 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/component/ErrorState.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/component/ErrorState.kt @@ -81,7 +81,7 @@ fun ErrorState( @Composable @Preview -fun ErrorStatePreview() { +private fun ErrorStatePreview() { AppTheme { ErrorState( message = "No internet connection", From 80dcaabe715ca0238057218660483d08af897d7e Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Thu, 26 Oct 2023 18:46:44 +0100 Subject: [PATCH 61/78] Fix padding discrepancy --- .../coinwatch/ui/screen/favourites/component/FavouriteItem.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouriteItem.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouriteItem.kt index 030ab646..6186ff4b 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouriteItem.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouriteItem.kt @@ -65,7 +65,7 @@ fun CoinFavouriteItem( modifier = Modifier.size(32.dp) ) - Spacer(Modifier.width(8.dp)) + Spacer(Modifier.width(16.dp)) Column { Text( From 894b52b2cf4e30e550d4e01c3d9a4ca186c89dd4 Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Thu, 26 Oct 2023 18:47:02 +0100 Subject: [PATCH 62/78] Make previews private --- .../coinwatch/ui/screen/list/component/SearchPrompt.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/SearchPrompt.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/SearchPrompt.kt index faec9672..4a7eed1f 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/SearchPrompt.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/component/SearchPrompt.kt @@ -39,7 +39,7 @@ fun SearchPrompt(modifier: Modifier = Modifier) { @Composable @Preview -fun SearchPromptPreview() { +private fun SearchPromptPreview() { AppTheme { SearchPrompt() } From 8c96b3f3b81b1793d862c168dd949a4cf55e839e Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Thu, 26 Oct 2023 18:47:17 +0100 Subject: [PATCH 63/78] Make previews private --- .../coinwatch/ui/screen/details/component/CoinChartCard.kt | 2 +- .../coinwatch/ui/screen/details/component/CoinChartRangeCard.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/CoinChartCard.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/CoinChartCard.kt index 4f719e34..4641caa1 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/CoinChartCard.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/CoinChartCard.kt @@ -104,7 +104,7 @@ fun CoinChartCard( @Preview @Composable -fun CoinChartCardPreview() { +private fun CoinChartCardPreview() { AppTheme { CoinChartCard( currentPrice = Price("1000"), diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/CoinChartRangeCard.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/CoinChartRangeCard.kt index 6ef5bbd2..5895777f 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/CoinChartRangeCard.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/CoinChartRangeCard.kt @@ -97,7 +97,7 @@ fun CoinChartRangeCard( @Preview @Composable -fun ChartRangeCardPreview() { +private fun ChartRangeCardPreview() { AppTheme { CoinChartRangeCard( currentPrice = Price("80.0"), From f61d167906aa2e70c6e989ca3a282fc7a15686bd Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Thu, 26 Oct 2023 18:47:31 +0100 Subject: [PATCH 64/78] Standardise screen states --- .../ui/screen/details/DetailsScreen.kt | 59 +++++++++------- .../component/DetailsSkeletonLoader.kt | 67 +++---------------- 2 files changed, 47 insertions(+), 79 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt index b7288573..a6ef6e00 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt @@ -49,7 +49,8 @@ import dev.shorthouse.coinwatch.ui.model.ChartPeriod import dev.shorthouse.coinwatch.ui.previewdata.DetailsUiStatePreviewProvider import dev.shorthouse.coinwatch.ui.screen.details.component.CoinChartCard import dev.shorthouse.coinwatch.ui.screen.details.component.CoinChartRangeCard -import dev.shorthouse.coinwatch.ui.screen.details.component.CoinDetailsSkeletonLoader +import dev.shorthouse.coinwatch.ui.screen.details.component.DetailsEmptyTopBar +import dev.shorthouse.coinwatch.ui.screen.details.component.DetailsSkeletonLoader import dev.shorthouse.coinwatch.ui.screen.details.component.MarketStatsCard import dev.shorthouse.coinwatch.ui.theme.AppTheme @@ -81,10 +82,10 @@ fun CoinDetailsScreen( ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - when (uiState) { - is DetailsUiState.Success -> { - Scaffold( - topBar = { + Scaffold( + topBar = { + when (uiState) { + is DetailsUiState.Success -> { CoinDetailsTopBar( coinDetails = uiState.coinDetails, isCoinFavourite = uiState.isCoinFavourite, @@ -92,8 +93,19 @@ fun CoinDetailsScreen( onClickFavouriteCoin = onClickFavouriteCoin, scrollBehavior = scrollBehavior ) - }, - content = { scaffoldPadding -> + } + + else -> { + DetailsEmptyTopBar( + onNavigateUp = onNavigateUp, + showFavouriteAction = uiState == DetailsUiState.Loading + ) + } + } + }, + content = { scaffoldPadding -> + when (uiState) { + is DetailsUiState.Success -> { CoinDetailsContent( coinDetails = uiState.coinDetails, coinChart = uiState.coinChart, @@ -101,27 +113,28 @@ fun CoinDetailsScreen( onClickChartPeriod = onClickChartPeriod, modifier = Modifier.padding(scaffoldPadding) ) - }, - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection) - ) - } + } - is DetailsUiState.Loading -> { - CoinDetailsSkeletonLoader() - } + is DetailsUiState.Error -> { + ErrorState( + message = uiState.message, + onRetry = onRefresh, + modifier = Modifier.padding(scaffoldPadding) + ) + } - is DetailsUiState.Error -> { - ErrorState( - message = uiState.message, - onRetry = onRefresh - ) - } - } + is DetailsUiState.Loading -> { + DetailsSkeletonLoader(modifier = Modifier.padding(scaffoldPadding)) + } + } + }, + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection) + ) } @Composable @OptIn(ExperimentalMaterial3Api::class) -private fun CoinDetailsTopBar( +fun CoinDetailsTopBar( coinDetails: CoinDetails, isCoinFavourite: Boolean, onNavigateUp: () -> Unit, @@ -197,7 +210,7 @@ private fun CoinDetailsTopBar( } @Composable -private fun CoinDetailsContent( +fun CoinDetailsContent( coinDetails: CoinDetails, coinChart: CoinChart, chartPeriod: ChartPeriod, diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/DetailsSkeletonLoader.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/DetailsSkeletonLoader.kt index 506680f0..a6f5a117 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/DetailsSkeletonLoader.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/DetailsSkeletonLoader.kt @@ -7,17 +7,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CornerSize -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ArrowBack -import androidx.compose.material.icons.rounded.StarOutline -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -28,50 +19,12 @@ import dev.shorthouse.coinwatch.ui.component.SkeletonSurface import dev.shorthouse.coinwatch.ui.theme.AppTheme @Composable -fun CoinDetailsSkeletonLoader(modifier: Modifier = Modifier) { - Scaffold( - topBar = { - DetailsSkeletonTopBar() - }, - content = { scaffoldPadding -> - DetailsSkeletonContent(modifier = Modifier.padding(scaffoldPadding)) - }, +fun DetailsSkeletonLoader(modifier: Modifier = Modifier) { + Column( modifier = modifier - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun DetailsSkeletonTopBar(modifier: Modifier = Modifier) { - LargeTopAppBar( - navigationIcon = { - IconButton(onClick = {}) { - Icon( - imageVector = Icons.Rounded.ArrowBack, - contentDescription = stringResource(R.string.cd_top_bar_back) - ) - } - }, - title = {}, - actions = { - IconButton(onClick = {}) { - Icon( - imageVector = Icons.Rounded.StarOutline, - contentDescription = stringResource(R.string.cd_top_bar_favourite), - tint = MaterialTheme.colorScheme.onBackground - ) - } - }, - colors = TopAppBarDefaults.largeTopAppBarColors( - containerColor = MaterialTheme.colorScheme.background - ), - modifier = modifier - ) -} - -@Composable -private fun DetailsSkeletonContent(modifier: Modifier = Modifier) { - Column(modifier = modifier.padding(horizontal = 12.dp)) { + .fillMaxSize() + .padding(horizontal = 12.dp) + ) { SkeletonSurface( modifier = Modifier .fillMaxWidth() @@ -82,7 +35,8 @@ private fun DetailsSkeletonContent(modifier: Modifier = Modifier) { Text( text = stringResource(R.string.title_chart_range), - style = MaterialTheme.typography.titleMedium + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onBackground ) Spacer(Modifier.height(8.dp)) @@ -97,7 +51,8 @@ private fun DetailsSkeletonContent(modifier: Modifier = Modifier) { Text( text = stringResource(R.string.card_header_market_stats), - style = MaterialTheme.typography.titleMedium + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onBackground ) Spacer(Modifier.height(8.dp)) @@ -112,10 +67,10 @@ private fun DetailsSkeletonContent(modifier: Modifier = Modifier) { } } -@Preview @Composable +@Preview(showBackground = true) private fun CoinDetailSkeletonLoaderPreview() { AppTheme { - CoinDetailsSkeletonLoader() + DetailsSkeletonLoader() } } From 60d59d199400bf201d102f1bb64c6215d97237f8 Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Thu, 26 Oct 2023 18:47:44 +0100 Subject: [PATCH 65/78] Build out new nested navigation graph --- .../coinwatch/navigation/AppNavHost.kt | 97 ++++++----- .../shorthouse/coinwatch/navigation/Graph.kt | 7 + .../coinwatch/navigation/RootNavGraph.kt | 161 ++++++++++++++++++ 3 files changed, 218 insertions(+), 47 deletions(-) create mode 100644 app/src/main/java/dev/shorthouse/coinwatch/navigation/Graph.kt create mode 100644 app/src/main/java/dev/shorthouse/coinwatch/navigation/RootNavGraph.kt diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt index 301bc095..d591d0dc 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt @@ -21,7 +21,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import dev.shorthouse.coinwatch.ui.screen.details.CoinDetailsScreen import dev.shorthouse.coinwatch.ui.screen.favourites.FavouritesScreen -import dev.shorthouse.coinwatch.ui.screen.list.CoinListScreen +import dev.shorthouse.coinwatch.ui.screen.list.ListScreen import dev.shorthouse.coinwatch.ui.screen.search.SearchScreen import kotlinx.collections.immutable.persistentListOf @@ -37,57 +37,60 @@ fun AppNavHost(modifier: Modifier = Modifier) { ) } + val showNavigationBar = navController.currentBackStackEntryAsState() + .value?.destination?.route in navigationBarScreens.map { it.route } + Scaffold( bottomBar = { - NavigationBar( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface - ) { - val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentDestination = navBackStackEntry?.destination + if (showNavigationBar) { + NavigationBar( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface + ) { + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination - navigationBarScreens.forEach { screen -> - val selected = currentDestination?.hierarchy?.any { - it.route == screen.route - } == true + navigationBarScreens.forEach { screen -> + val selected = currentDestination?.hierarchy?.any { destination -> + destination.route == screen.route + } == true - NavigationBarItem( - icon = { - if (selected) { - Icon( - imageVector = screen.selectedIcon, - contentDescription = null - ) - } else { - Icon( - imageVector = screen.icon, - contentDescription = null - ) - } - }, - label = { - Text(text = stringResource(screen.nameResourceId)) - }, - selected = currentDestination?.hierarchy?.any { - it.route == screen.route - } == true, - onClick = { - navController.navigate(screen.route) { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true + NavigationBarItem( + icon = { + if (selected) { + Icon( + imageVector = screen.selectedIcon, + contentDescription = null + ) + } else { + Icon( + imageVector = screen.icon, + contentDescription = null + ) + } + }, + label = { + Text(text = stringResource(screen.nameResourceId)) + }, + selected = selected, + onClick = { + navController.navigate(screen.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true } - launchSingleTop = true - restoreState = true - } - }, - colors = NavigationBarItemDefaults.colors( - selectedIconColor = MaterialTheme.colorScheme.onSurface, - selectedTextColor = MaterialTheme.colorScheme.onSurface, - indicatorColor = MaterialTheme.colorScheme.background, - unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, - unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant + }, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = MaterialTheme.colorScheme.onSurface, + selectedTextColor = MaterialTheme.colorScheme.onSurface, + indicatorColor = MaterialTheme.colorScheme.background, + unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, + unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant + ) ) - ) + } } } }, @@ -98,7 +101,7 @@ fun AppNavHost(modifier: Modifier = Modifier) { modifier = Modifier.padding(scaffoldPadding) ) { composable(route = NavigationBarScreen.Market.route) { - CoinListScreen(navController = navController) + ListScreen(navController = navController) } composable(route = NavigationBarScreen.Favourites.route) { FavouritesScreen(navController = navController) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/Graph.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/Graph.kt new file mode 100644 index 00000000..b5a8e6ad --- /dev/null +++ b/app/src/main/java/dev/shorthouse/coinwatch/navigation/Graph.kt @@ -0,0 +1,7 @@ +package dev.shorthouse.coinwatch.navigation + +sealed class Graph(val route: String) { + object Root : Screen("root_graph") + object Home : Screen("home_graph") + object Details : Screen("details_graph") +} diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/RootNavGraph.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/RootNavGraph.kt new file mode 100644 index 00000000..e83c3c3a --- /dev/null +++ b/app/src/main/java/dev/shorthouse/coinwatch/navigation/RootNavGraph.kt @@ -0,0 +1,161 @@ +package dev.shorthouse.coinwatch.navigation + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavDestination +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHost +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.navigation +import androidx.navigation.compose.rememberNavController +import dev.shorthouse.coinwatch.ui.screen.details.CoinDetailsScreen +import dev.shorthouse.coinwatch.ui.screen.favourites.FavouritesScreen +import dev.shorthouse.coinwatch.ui.screen.list.ListScreen +import dev.shorthouse.coinwatch.ui.screen.search.SearchScreen +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun RootNavGraph( + modifier: Modifier = Modifier, + navController: NavHostController = rememberNavController() +) { + NavHost( + navController = navController, + route = Graph.Root.route, + startDestination = Graph.Home.route + ) { + composable(route = Graph.Home.route) { + Scaffold( + bottomBar = { AppNavigationBar(navController = navController) }, + content = { scaffoldPadding -> + HomeNavGraph( + navController = navController, + modifier = Modifier.padding(scaffoldPadding) + ) + }, + modifier = modifier + ) + } + } +} + +@Composable +fun HomeNavGraph( + navController: NavHostController, + modifier: Modifier = Modifier +) { + NavHost( + navController = navController, + route = Graph.Home.route, + startDestination = NavigationBarScreen.Market.route, + modifier = modifier + ) { + composable(route = NavigationBarScreen.Market.route) { + ListScreen(navController = navController) + } + composable(route = NavigationBarScreen.Favourites.route) { + FavouritesScreen(navController = navController) + } + composable(route = NavigationBarScreen.Search.route) { + SearchScreen(navController = navController) + } + detailsNavGraph(navController = navController) + } +} + +fun NavGraphBuilder.detailsNavGraph(navController: NavHostController) { + navigation( + route = Graph.Details.route, + startDestination = Screen.Details.route + ) { + composable(route = Screen.Details.route + "/{coinId}") { + CoinDetailsScreen(navController = navController) + } + } +} + +@Composable +fun AppNavigationBar(navController: NavHostController) { + val navigationBarScreens = remember { + persistentListOf( + NavigationBarScreen.Market, + NavigationBarScreen.Favourites, + NavigationBarScreen.Search + ) + } + + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + val showNavigationBar = navigationBarScreens.any { it.route == currentDestination?.route } + + if (showNavigationBar) { + NavigationBar { + navigationBarScreens.forEach { screen -> + AddItem( + screen = screen, + currentDestination = currentDestination, + navController = navController + ) + } + } + } +} + +@Composable +fun RowScope.AddItem( + screen: NavigationBarScreen, + currentDestination: NavDestination?, + navController: NavHostController, + modifier: Modifier = Modifier +) { + val selected = currentDestination?.hierarchy?.any { destination -> + destination.route == screen.route + } == true + + NavigationBarItem( + label = { + Text(text = stringResource(screen.nameResourceId)) + }, + icon = { + Icon( + imageVector = screen.icon, + contentDescription = null + ) + }, + selected = selected, + onClick = { + navController.navigate(screen.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + }, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = MaterialTheme.colorScheme.onSurface, + selectedTextColor = MaterialTheme.colorScheme.onSurface, + indicatorColor = MaterialTheme.colorScheme.background, + unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, + unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant + ), + modifier = modifier + ) +} From 7c3d83b198ca7135ece21f36d33689c0e004511a Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Thu, 26 Oct 2023 18:48:18 +0100 Subject: [PATCH 66/78] Remove unneeded 'coin' prefix from list screen and accompanying classes --- .../coinwatch/ui/screen/CoinListScreenTest.kt | 56 +++++++++---------- .../previewdata/ListUiStatePreviewProvider.kt | 12 ++-- .../list/{CoinListScreen.kt => ListScreen.kt} | 40 +++++++------ .../{CoinListUiState.kt => ListUiState.kt} | 8 +-- ...{CoinListViewModel.kt => ListViewModel.kt} | 10 ++-- .../ui/screen/list/CoinListViewModelTest.kt | 12 ++-- 6 files changed, 71 insertions(+), 67 deletions(-) rename app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/{CoinListScreen.kt => ListScreen.kt} (92%) rename app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/{CoinListUiState.kt => ListUiState.kt} (57%) rename app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/{CoinListViewModel.kt => ListViewModel.kt} (79%) diff --git a/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinListScreenTest.kt b/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinListScreenTest.kt index 269d3e55..97d87c45 100644 --- a/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinListScreenTest.kt +++ b/app/src/androidTest/java/dev/shorthouse/coinwatch/ui/screen/CoinListScreenTest.kt @@ -11,8 +11,8 @@ import com.google.common.truth.Truth.assertThat import dev.shorthouse.coinwatch.model.Coin import dev.shorthouse.coinwatch.model.Percentage import dev.shorthouse.coinwatch.model.Price -import dev.shorthouse.coinwatch.ui.screen.list.CoinListScreen -import dev.shorthouse.coinwatch.ui.screen.list.CoinListUiState +import dev.shorthouse.coinwatch.ui.screen.list.ListScreen +import dev.shorthouse.coinwatch.ui.screen.list.ListUiState import dev.shorthouse.coinwatch.ui.theme.AppTheme import java.math.BigDecimal import kotlinx.collections.immutable.persistentListOf @@ -26,11 +26,11 @@ class CoinListScreenTest { @Test fun when_uiStateLoading_should_showSkeletonLoader() { - val uiStateLoading = CoinListUiState.Loading + val uiStateLoading = ListUiState.Loading composeTestRule.setContent { AppTheme { - CoinListScreen( + ListScreen( uiState = uiStateLoading, onCoinClick = {}, onRefresh = {} @@ -46,11 +46,11 @@ class CoinListScreenTest { @Test fun when_uiStateError_should_showErrorState() { - val uiStateError = CoinListUiState.Error("Error message") + val uiStateError = ListUiState.Error("Error message") composeTestRule.setContent { AppTheme { - CoinListScreen( + ListScreen( uiState = uiStateError, onCoinClick = {}, onRefresh = {} @@ -69,11 +69,11 @@ class CoinListScreenTest { @Test fun when_uiStateErrorRetryClicked_should_callOnRefresh() { var onRefreshCalled = false - val uiStateError = CoinListUiState.Error("Error message") + val uiStateError = ListUiState.Error("Error message") composeTestRule.setContent { AppTheme { - CoinListScreen( + ListScreen( uiState = uiStateError, onCoinClick = {}, onRefresh = { onRefreshCalled = true } @@ -90,13 +90,13 @@ class CoinListScreenTest { @Test fun when_uiStateSuccess_should_showExpectedContent() { - val uiStateSuccess = CoinListUiState.Success( + val uiStateSuccess = ListUiState.Success( coins = persistentListOf() ) composeTestRule.setContent { AppTheme { - CoinListScreen( + ListScreen( uiState = uiStateSuccess, onCoinClick = {}, onRefresh = {} @@ -112,13 +112,13 @@ class CoinListScreenTest { @Test fun when_uiStateSuccess_favouriteCoinsEmpty_should_showEmptyState() { - val uiStateSuccess = CoinListUiState.Success( + val uiStateSuccess = ListUiState.Success( coins = persistentListOf() ) composeTestRule.setContent { AppTheme { - CoinListScreen( + ListScreen( uiState = uiStateSuccess, onCoinClick = {}, onRefresh = {} @@ -136,13 +136,13 @@ class CoinListScreenTest { @Test fun when_uiStateSuccess_favouriteCoinsList_should_showExpectedContent() { - val uiStateSuccess = CoinListUiState.Success( + val uiStateSuccess = ListUiState.Success( coins = persistentListOf() ) composeTestRule.setContent { AppTheme { - CoinListScreen( + ListScreen( uiState = uiStateSuccess, onCoinClick = {}, onRefresh = {} @@ -176,13 +176,13 @@ class CoinListScreenTest { @Test fun when_uiStateSuccess_coinsEmpty_should_showEmptyState() { - val uiStateSuccess = CoinListUiState.Success( + val uiStateSuccess = ListUiState.Success( coins = persistentListOf() ) composeTestRule.setContent { AppTheme { - CoinListScreen( + ListScreen( uiState = uiStateSuccess, onCoinClick = {}, onRefresh = {} @@ -198,7 +198,7 @@ class CoinListScreenTest { @Test fun when_uiStateSuccess_coinsList_should_showExpectedContent() { - val uiStateSuccess = CoinListUiState.Success( + val uiStateSuccess = ListUiState.Success( coins = persistentListOf( Coin( id = "bitcoin", @@ -247,7 +247,7 @@ class CoinListScreenTest { composeTestRule.setContent { AppTheme { - CoinListScreen( + ListScreen( uiState = uiStateSuccess, onCoinClick = {}, onRefresh = {} @@ -280,7 +280,7 @@ class CoinListScreenTest { fun when_coinItemClicked_should_callOnClick() { var onCoinClickCalled = false - val uiStateSuccess = CoinListUiState.Success( + val uiStateSuccess = ListUiState.Success( coins = persistentListOf( Coin( id = "bitcoin", @@ -301,7 +301,7 @@ class CoinListScreenTest { composeTestRule.setContent { AppTheme { - CoinListScreen( + ListScreen( uiState = uiStateSuccess, onCoinClick = ({ onCoinClickCalled = true }), onRefresh = {} @@ -320,13 +320,13 @@ class CoinListScreenTest { fun when_favouriteCoinItemClicked_should_callOnClick() { var onCoinClickCalled = false - val uiStateSuccess = CoinListUiState.Success( + val uiStateSuccess = ListUiState.Success( coins = persistentListOf() ) composeTestRule.setContent { AppTheme { - CoinListScreen( + ListScreen( uiState = uiStateSuccess, onCoinClick = ({ onCoinClickCalled = true }), onRefresh = {} @@ -343,13 +343,13 @@ class CoinListScreenTest { @Test fun when_timeOfDayMorning_should_showMorningGreeting() { - val uiStateSuccess = CoinListUiState.Success( + val uiStateSuccess = ListUiState.Success( coins = persistentListOf() ) composeTestRule.setContent { AppTheme { - CoinListScreen( + ListScreen( uiState = uiStateSuccess, onCoinClick = {}, onRefresh = {} @@ -364,13 +364,13 @@ class CoinListScreenTest { @Test fun when_timeOfDayAfternoon_should_showAfternoonGreeting() { - val uiStateSuccess = CoinListUiState.Success( + val uiStateSuccess = ListUiState.Success( coins = persistentListOf() ) composeTestRule.setContent { AppTheme { - CoinListScreen( + ListScreen( uiState = uiStateSuccess, onCoinClick = {}, onRefresh = {} @@ -385,13 +385,13 @@ class CoinListScreenTest { @Test fun when_timeOfDayEvening_should_showEveningGreeting() { - val uiStateSuccess = CoinListUiState.Success( + val uiStateSuccess = ListUiState.Success( coins = persistentListOf() ) composeTestRule.setContent { AppTheme { - CoinListScreen( + ListScreen( uiState = uiStateSuccess, onCoinClick = {}, onRefresh = {} diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/ListUiStatePreviewProvider.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/ListUiStatePreviewProvider.kt index c5f3e247..372d02ee 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/ListUiStatePreviewProvider.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/previewdata/ListUiStatePreviewProvider.kt @@ -5,20 +5,20 @@ import dev.shorthouse.coinwatch.model.Coin import dev.shorthouse.coinwatch.model.Percentage import dev.shorthouse.coinwatch.model.Price import dev.shorthouse.coinwatch.ui.previewdata.CoinListPreviewData.coins -import dev.shorthouse.coinwatch.ui.screen.list.CoinListUiState +import dev.shorthouse.coinwatch.ui.screen.list.ListUiState import java.math.BigDecimal import kotlinx.collections.immutable.persistentListOf -class ListUiStatePreviewProvider : PreviewParameterProvider { +class ListUiStatePreviewProvider : PreviewParameterProvider { override val values = sequenceOf( - CoinListUiState.Success( + ListUiState.Success( coins = coins ), - CoinListUiState.Success( + ListUiState.Success( coins = persistentListOf() ), - CoinListUiState.Error("No internet connection"), - CoinListUiState.Loading + ListUiState.Error("No internet connection"), + ListUiState.Loading ) } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/ListScreen.kt similarity index 92% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/ListScreen.kt index ffc8a60b..a18f0125 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/ListScreen.kt @@ -4,7 +4,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState @@ -50,13 +50,13 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.launch @Composable -fun CoinListScreen( +fun ListScreen( navController: NavController, - viewModel: CoinListViewModel = hiltViewModel() + viewModel: ListViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - CoinListScreen( + ListScreen( uiState = uiState, onCoinClick = { coin -> navController.navigate(Screen.Details.route + "/${coin.id}") @@ -67,8 +67,8 @@ fun CoinListScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun CoinListScreen( - uiState: CoinListUiState, +fun ListScreen( + uiState: ListUiState, onCoinClick: (Coin) -> Unit, onRefresh: () -> Unit, modifier: Modifier = Modifier @@ -84,12 +84,12 @@ fun CoinListScreen( Scaffold( topBar = { - CoinListTopBar(scrollBehavior = scrollBehavior) + ListTopBar(scrollBehavior = scrollBehavior) }, content = { scaffoldPadding -> when (uiState) { - is CoinListUiState.Success -> { - CoinListContent( + is ListUiState.Success -> { + ListContent( coins = uiState.coins, onCoinClick = onCoinClick, lazyListState = lazyListState, @@ -97,7 +97,7 @@ fun CoinListScreen( ) } - is CoinListUiState.Error -> { + is ListUiState.Error -> { ErrorState( message = uiState.message, onRetry = onRefresh, @@ -105,7 +105,7 @@ fun CoinListScreen( ) } - is CoinListUiState.Loading -> { + is ListUiState.Loading -> { ListSkeletonLoader(modifier = Modifier.padding(scaffoldPadding)) } } @@ -140,7 +140,7 @@ fun CoinListScreen( @Composable @OptIn(ExperimentalMaterial3Api::class) -private fun CoinListTopBar( +fun ListTopBar( scrollBehavior: TopAppBarScrollBehavior, modifier: Modifier = Modifier ) { @@ -162,19 +162,23 @@ private fun CoinListTopBar( } @Composable -private fun CoinListContent( +fun ListContent( coins: ImmutableList, onCoinClick: (Coin) -> Unit, lazyListState: LazyListState, modifier: Modifier = Modifier ) { if (coins.isEmpty()) { - ListEmptyState() + ListEmptyState( + modifier = modifier + .fillMaxSize() + .padding(12.dp) + ) } else { LazyColumn( state = lazyListState, contentPadding = PaddingValues(horizontal = 12.dp), - modifier = modifier + modifier = modifier.fillMaxSize() ) { items( count = coins.size, @@ -213,11 +217,11 @@ private fun CoinListContent( @Composable @Preview(showBackground = true) -private fun CoinListScreenPreview( - @PreviewParameter(ListUiStatePreviewProvider::class) uiState: CoinListUiState +private fun ListScreenPreview( + @PreviewParameter(ListUiStatePreviewProvider::class) uiState: ListUiState ) { AppTheme { - CoinListScreen( + ListScreen( uiState = uiState, onCoinClick = {}, onRefresh = {} diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListUiState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/ListUiState.kt similarity index 57% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListUiState.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/ListUiState.kt index 4f8ca1b9..4f18317c 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListUiState.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/ListUiState.kt @@ -3,11 +3,11 @@ package dev.shorthouse.coinwatch.ui.screen.list import dev.shorthouse.coinwatch.model.Coin import kotlinx.collections.immutable.ImmutableList -sealed interface CoinListUiState { +sealed interface ListUiState { data class Success( val coins: ImmutableList - ) : CoinListUiState + ) : ListUiState - data class Error(val message: String?) : CoinListUiState - object Loading : CoinListUiState + data class Error(val message: String?) : ListUiState + object Loading : ListUiState } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListViewModel.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/ListViewModel.kt similarity index 79% rename from app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListViewModel.kt rename to app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/ListViewModel.kt index 9060f4d2..c5ebd5c9 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListViewModel.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/ListViewModel.kt @@ -14,10 +14,10 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update @HiltViewModel -class CoinListViewModel @Inject constructor( +class ListViewModel @Inject constructor( private val getCoinsUseCase: GetCoinsUseCase ) : ViewModel() { - private val _uiState = MutableStateFlow(CoinListUiState.Loading) + private val _uiState = MutableStateFlow(ListUiState.Loading) val uiState = _uiState.asStateFlow() init { @@ -25,21 +25,21 @@ class CoinListViewModel @Inject constructor( } fun initialiseUiState() { - _uiState.update { CoinListUiState.Loading } + _uiState.update { ListUiState.Loading } val coinsFlow = getCoinsUseCase() coinsFlow.onEach { coinsResult -> when (coinsResult) { is Result.Error -> { - _uiState.update { CoinListUiState.Error(coinsResult.message) } + _uiState.update { ListUiState.Error(coinsResult.message) } } is Result.Success -> { val coins = coinsResult.data.toImmutableList() _uiState.update { - CoinListUiState.Success( + ListUiState.Success( coins = coins ) } diff --git a/app/src/test/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListViewModelTest.kt b/app/src/test/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListViewModelTest.kt index ac0ce31f..2244c640 100644 --- a/app/src/test/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListViewModelTest.kt +++ b/app/src/test/java/dev/shorthouse/coinwatch/ui/screen/list/CoinListViewModelTest.kt @@ -28,7 +28,7 @@ class CoinListViewModelTest { val mainDispatcherRule = MainDispatcherRule() // Class under test - private lateinit var viewModel: CoinListViewModel + private lateinit var viewModel: ListViewModel @RelaxedMockK private lateinit var getCoinsUseCase: GetCoinsUseCase @@ -40,7 +40,7 @@ class CoinListViewModelTest { fun setup() { MockKAnnotations.init(this) - viewModel = CoinListViewModel( + viewModel = ListViewModel( getCoinsUseCase = getCoinsUseCase, getFavouriteCoinsUseCase = getFavouriteCoinsUseCase ) @@ -54,7 +54,7 @@ class CoinListViewModelTest { @Test fun `When ViewModel is initialised should have loading UI state`() = runTest { // Arrange - val expectedUiState = CoinListUiState.Loading + val expectedUiState = ListUiState.Loading // Act @@ -66,7 +66,7 @@ class CoinListViewModelTest { fun `When coins returns error should have error UI state`() = runTest { // Arrange val errorMessage = "Coins error" - val expectedUiState = CoinListUiState.Error(errorMessage) + val expectedUiState = ListUiState.Error(errorMessage) every { getCoinsUseCase() } returns flowOf(Result.Error(errorMessage)) every { getFavouriteCoinsUseCase() } returns flowOf(Result.Success(emptyList())) @@ -82,7 +82,7 @@ class CoinListViewModelTest { fun `When favourite coins returns error should have error UI state`() = runTest { // Arrange val errorMessage = "Favourite coins error" - val expectedUiState = CoinListUiState.Error(errorMessage) + val expectedUiState = ListUiState.Error(errorMessage) every { getCoinsUseCase() } returns flowOf(Result.Success(emptyList())) every { getFavouriteCoinsUseCase() } returns flowOf(Result.Error(errorMessage)) @@ -148,7 +148,7 @@ class CoinListViewModelTest { else -> TimeOfDay.Evening } - val expectedUiState = CoinListUiState.Success( + val expectedUiState = ListUiState.Success( coins = coins, favouriteCoins = expectedFavouriteCoins, timeOfDay = expectedTimeOfDay From e75d7efaea8b0fe7c4f66b4ef47a117969927bc0 Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Thu, 26 Oct 2023 18:48:26 +0100 Subject: [PATCH 67/78] Add primary white colour to theme --- app/src/main/java/dev/shorthouse/coinwatch/ui/theme/Theme.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/theme/Theme.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/theme/Theme.kt index 7614b668..49c27503 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/theme/Theme.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/theme/Theme.kt @@ -4,10 +4,12 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.graphics.Color.Companion.White import com.google.accompanist.systemuicontroller.SystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController private val darkColorScheme = darkColorScheme( + primary = White, background = dark_background, onBackground = dark_onBackground, surface = dark_surface, From e7ca43e299e586d44198edf065964e87b619853d Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Thu, 26 Oct 2023 18:48:33 +0100 Subject: [PATCH 68/78] Add details empty top app bar --- .../component/DetailsEmptyTopAppBar.kt | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/DetailsEmptyTopAppBar.kt diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/DetailsEmptyTopAppBar.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/DetailsEmptyTopAppBar.kt new file mode 100644 index 00000000..f7a31662 --- /dev/null +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/DetailsEmptyTopAppBar.kt @@ -0,0 +1,63 @@ +package dev.shorthouse.coinwatch.ui.screen.details.component + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.material.icons.rounded.StarOutline +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import dev.shorthouse.coinwatch.R +import dev.shorthouse.coinwatch.ui.theme.AppTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DetailsEmptyTopBar( + onNavigateUp: () -> Unit, + showFavouriteAction: Boolean, + modifier: Modifier = Modifier +) { + LargeTopAppBar( + navigationIcon = { + IconButton(onClick = onNavigateUp) { + Icon( + imageVector = Icons.Rounded.ArrowBack, + contentDescription = stringResource(R.string.cd_top_bar_back) + ) + } + }, + title = {}, + actions = { + if (showFavouriteAction) { + IconButton(onClick = {}) { + Icon( + imageVector = Icons.Rounded.StarOutline, + contentDescription = stringResource(R.string.cd_top_bar_favourite), + tint = MaterialTheme.colorScheme.onBackground + ) + } + } + }, + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = MaterialTheme.colorScheme.background + ), + modifier = modifier + ) +} + +@Preview +@Composable +private fun DetailsEmptyTopBar() { + AppTheme { + DetailsEmptyTopBar( + onNavigateUp = {}, + showFavouriteAction = true + ) + } +} From b55100d892284ddb1cb17830fdc95a3020605598 Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Thu, 26 Oct 2023 18:48:41 +0100 Subject: [PATCH 69/78] Build out new nested nav graph --- app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt b/app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt index f59fa9a5..85bc295f 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt @@ -5,7 +5,7 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import dagger.hilt.android.AndroidEntryPoint -import dev.shorthouse.coinwatch.navigation.AppNavHost +import dev.shorthouse.coinwatch.navigation.RootNavGraph import dev.shorthouse.coinwatch.ui.theme.AppTheme @AndroidEntryPoint @@ -16,7 +16,7 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { AppTheme { - AppNavHost() + RootNavGraph() } } } From 23a3ed82bc754ffcf57a6ffdb6ebdcc374f9ed03 Mon Sep 17 00:00:00 2001 From: Harry S Date: Thu, 26 Oct 2023 20:52:45 +0100 Subject: [PATCH 70/78] Merge together navigation ideas for new navigation bar --- .../dev/shorthouse/coinwatch/MainActivity.kt | 4 +- .../coinwatch/navigation/AppNavHost.kt | 119 --------------- .../coinwatch/navigation/AppRootScaffold.kt | 140 ++++++++++++++++++ .../coinwatch/navigation/RootNavGraph.kt | 12 +- 4 files changed, 143 insertions(+), 132 deletions(-) delete mode 100644 app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt create mode 100644 app/src/main/java/dev/shorthouse/coinwatch/navigation/AppRootScaffold.kt diff --git a/app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt b/app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt index 85bc295f..8a79de5d 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt @@ -5,7 +5,7 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import dagger.hilt.android.AndroidEntryPoint -import dev.shorthouse.coinwatch.navigation.RootNavGraph +import dev.shorthouse.coinwatch.navigation.AppRootScaffold import dev.shorthouse.coinwatch.ui.theme.AppTheme @AndroidEntryPoint @@ -16,7 +16,7 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { AppTheme { - RootNavGraph() + AppRootScaffold() } } } diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt deleted file mode 100644 index d591d0dc..00000000 --- a/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt +++ /dev/null @@ -1,119 +0,0 @@ -package dev.shorthouse.coinwatch.navigation - -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.NavigationBarItemDefaults -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.navigation.NavDestination.Companion.hierarchy -import androidx.navigation.NavGraph.Companion.findStartDestination -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController -import dev.shorthouse.coinwatch.ui.screen.details.CoinDetailsScreen -import dev.shorthouse.coinwatch.ui.screen.favourites.FavouritesScreen -import dev.shorthouse.coinwatch.ui.screen.list.ListScreen -import dev.shorthouse.coinwatch.ui.screen.search.SearchScreen -import kotlinx.collections.immutable.persistentListOf - -@Composable -fun AppNavHost(modifier: Modifier = Modifier) { - val navController = rememberNavController() - - val navigationBarScreens = remember { - persistentListOf( - NavigationBarScreen.Market, - NavigationBarScreen.Favourites, - NavigationBarScreen.Search - ) - } - - val showNavigationBar = navController.currentBackStackEntryAsState() - .value?.destination?.route in navigationBarScreens.map { it.route } - - Scaffold( - bottomBar = { - if (showNavigationBar) { - NavigationBar( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface - ) { - val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentDestination = navBackStackEntry?.destination - - navigationBarScreens.forEach { screen -> - val selected = currentDestination?.hierarchy?.any { destination -> - destination.route == screen.route - } == true - - NavigationBarItem( - icon = { - if (selected) { - Icon( - imageVector = screen.selectedIcon, - contentDescription = null - ) - } else { - Icon( - imageVector = screen.icon, - contentDescription = null - ) - } - }, - label = { - Text(text = stringResource(screen.nameResourceId)) - }, - selected = selected, - onClick = { - navController.navigate(screen.route) { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - }, - colors = NavigationBarItemDefaults.colors( - selectedIconColor = MaterialTheme.colorScheme.onSurface, - selectedTextColor = MaterialTheme.colorScheme.onSurface, - indicatorColor = MaterialTheme.colorScheme.background, - unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, - unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant - ) - ) - } - } - } - }, - content = { scaffoldPadding -> - NavHost( - navController = navController, - startDestination = NavigationBarScreen.Market.route, - modifier = Modifier.padding(scaffoldPadding) - ) { - composable(route = NavigationBarScreen.Market.route) { - ListScreen(navController = navController) - } - composable(route = NavigationBarScreen.Favourites.route) { - FavouritesScreen(navController = navController) - } - composable(route = NavigationBarScreen.Search.route) { - SearchScreen(navController = navController) - } - composable(route = Screen.Details.route + "/{coinId}") { - CoinDetailsScreen(navController = navController) - } - } - }, - modifier = modifier - ) -} diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppRootScaffold.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppRootScaffold.kt new file mode 100644 index 00000000..fbe64892 --- /dev/null +++ b/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppRootScaffold.kt @@ -0,0 +1,140 @@ +package dev.shorthouse.coinwatch.navigation + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavDestination +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import dev.shorthouse.coinwatch.ui.screen.details.CoinDetailsScreen +import dev.shorthouse.coinwatch.ui.screen.favourites.FavouritesScreen +import dev.shorthouse.coinwatch.ui.screen.list.ListScreen +import dev.shorthouse.coinwatch.ui.screen.search.SearchScreen +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun AppRootScaffold( + modifier: Modifier = Modifier, + navController: NavHostController = rememberNavController() +) { + val navigationBarScreens = remember { + persistentListOf( + NavigationBarScreen.Market, + NavigationBarScreen.Favourites, + NavigationBarScreen.Search + ) + } + + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + val showNavigationBar = navigationBarScreens.any { it.route == currentDestination?.route } + + Scaffold( + bottomBar = { + if (showNavigationBar) { + NavigationBar( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface + ) { + navigationBarScreens.forEach { screen -> + AddNavigationBarItem( + screen = screen, + currentDestination = currentDestination, + navController = navController + ) + } + } + } + }, + content = { scaffoldPadding -> + AppNavHost( + navController = navController, + modifier = Modifier.padding(scaffoldPadding) + ) + }, + modifier = modifier + ) +} + +@Composable +fun AppNavHost( + navController: NavHostController, + modifier: Modifier = Modifier +) { + NavHost( + navController = navController, + startDestination = NavigationBarScreen.Market.route, + modifier = modifier + ) { + composable(route = NavigationBarScreen.Market.route) { + ListScreen(navController = navController) + } + composable(route = NavigationBarScreen.Favourites.route) { + FavouritesScreen(navController = navController) + } + composable(route = NavigationBarScreen.Search.route) { + SearchScreen(navController = navController) + } + composable(route = Screen.Details.route + "/{coinId}") { + CoinDetailsScreen(navController = navController) + } + } +} + +@Composable +fun RowScope.AddNavigationBarItem( + screen: NavigationBarScreen, + currentDestination: NavDestination?, + navController: NavHostController, + modifier: Modifier = Modifier +) { + val selected = currentDestination?.hierarchy?.any { destination -> + destination.route == screen.route + } == true + + NavigationBarItem( + label = { + Text(text = stringResource(screen.nameResourceId)) + }, + icon = { + Icon( + imageVector = screen.icon, + contentDescription = null + ) + }, + selected = selected, + onClick = { + navController.navigate(screen.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + }, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = MaterialTheme.colorScheme.onSurface, + selectedTextColor = MaterialTheme.colorScheme.onSurface, + indicatorColor = MaterialTheme.colorScheme.background, + unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, + unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant + ), + modifier = modifier + ) +} diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/RootNavGraph.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/RootNavGraph.kt index e83c3c3a..69c016c7 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/navigation/RootNavGraph.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/navigation/RootNavGraph.kt @@ -1,22 +1,12 @@ package dev.shorthouse.coinwatch.navigation -import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.navigation.NavDestination -import androidx.navigation.NavDestination.Companion.hierarchy -import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHost import androidx.navigation.NavHostController @@ -119,7 +109,7 @@ fun AppNavigationBar(navController: NavHostController) { } @Composable -fun RowScope.AddItem( +private fun RowScope.AddItem( screen: NavigationBarScreen, currentDestination: NavDestination?, navController: NavHostController, From c9e427193c95d78514b69180d529f49ea4c514d5 Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Sat, 28 Oct 2023 11:48:38 +0100 Subject: [PATCH 71/78] Add in new AppNavHost --- app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt b/app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt index 8a79de5d..f59fa9a5 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/MainActivity.kt @@ -5,7 +5,7 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import dagger.hilt.android.AndroidEntryPoint -import dev.shorthouse.coinwatch.navigation.AppRootScaffold +import dev.shorthouse.coinwatch.navigation.AppNavHost import dev.shorthouse.coinwatch.ui.theme.AppTheme @AndroidEntryPoint @@ -16,7 +16,7 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { AppTheme { - AppRootScaffold() + AppNavHost() } } } From 0a3338f23bb301633b8cfbd4962677a79c0f5e89 Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Sat, 28 Oct 2023 11:48:57 +0100 Subject: [PATCH 72/78] Add in new nav host with bottom nav bar --- .../coinwatch/navigation/AppNavHost.kt | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt new file mode 100644 index 00000000..490d6c86 --- /dev/null +++ b/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppNavHost.kt @@ -0,0 +1,27 @@ +package dev.shorthouse.coinwatch.navigation + +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import dev.shorthouse.coinwatch.ui.screen.details.CoinDetailsScreen + +@Composable +fun AppNavHost(navController: NavHostController = rememberNavController()) { + val onNavigateDetails: (String) -> Unit = { coinId -> + navController.navigate(Screen.Details.route + "/$coinId") + } + + NavHost( + navController = navController, + startDestination = Screen.NavigationBar.route + ) { + composable(Screen.NavigationBar.route) { + NavigationBarScaffold(onNavigateDetails = onNavigateDetails) + } + composable(route = Screen.Details.route + "/{coinId}") { + CoinDetailsScreen(onNavigateUp = { navController.navigateUp() }) + } + } +} From 82e83643e134fd2a6302ca3a23a7232fa7510038 Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Sat, 28 Oct 2023 11:49:16 +0100 Subject: [PATCH 73/78] Refactor and change out new navigation components --- .../shorthouse/coinwatch/navigation/Graph.kt | 7 - ...otScaffold.kt => NavigationBarScaffold.kt} | 40 +++-- .../coinwatch/navigation/RootNavGraph.kt | 151 ------------------ .../shorthouse/coinwatch/navigation/Screen.kt | 19 +-- 4 files changed, 30 insertions(+), 187 deletions(-) delete mode 100644 app/src/main/java/dev/shorthouse/coinwatch/navigation/Graph.kt rename app/src/main/java/dev/shorthouse/coinwatch/navigation/{AppRootScaffold.kt => NavigationBarScaffold.kt} (81%) delete mode 100644 app/src/main/java/dev/shorthouse/coinwatch/navigation/RootNavGraph.kt diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/Graph.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/Graph.kt deleted file mode 100644 index b5a8e6ad..00000000 --- a/app/src/main/java/dev/shorthouse/coinwatch/navigation/Graph.kt +++ /dev/null @@ -1,7 +0,0 @@ -package dev.shorthouse.coinwatch.navigation - -sealed class Graph(val route: String) { - object Root : Screen("root_graph") - object Home : Screen("home_graph") - object Details : Screen("details_graph") -} diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppRootScaffold.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/NavigationBarScaffold.kt similarity index 81% rename from app/src/main/java/dev/shorthouse/coinwatch/navigation/AppRootScaffold.kt rename to app/src/main/java/dev/shorthouse/coinwatch/navigation/NavigationBarScaffold.kt index fbe64892..7714cbf0 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/navigation/AppRootScaffold.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/navigation/NavigationBarScaffold.kt @@ -14,6 +14,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp import androidx.navigation.NavDestination import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavGraph.Companion.findStartDestination @@ -22,17 +24,18 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController -import dev.shorthouse.coinwatch.ui.screen.details.CoinDetailsScreen import dev.shorthouse.coinwatch.ui.screen.favourites.FavouritesScreen import dev.shorthouse.coinwatch.ui.screen.list.ListScreen import dev.shorthouse.coinwatch.ui.screen.search.SearchScreen import kotlinx.collections.immutable.persistentListOf @Composable -fun AppRootScaffold( - modifier: Modifier = Modifier, - navController: NavHostController = rememberNavController() +fun NavigationBarScaffold( + onNavigateDetails: (String) -> Unit, + modifier: Modifier = Modifier ) { + val navController = rememberNavController() + val navigationBarScreens = remember { persistentListOf( NavigationBarScreen.Market, @@ -49,8 +52,9 @@ fun AppRootScaffold( bottomBar = { if (showNavigationBar) { NavigationBar( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + tonalElevation = 0.dp ) { navigationBarScreens.forEach { screen -> AddNavigationBarItem( @@ -63,8 +67,9 @@ fun AppRootScaffold( } }, content = { scaffoldPadding -> - AppNavHost( + NavigationBarNavHost( navController = navController, + onNavigateDetails = onNavigateDetails, modifier = Modifier.padding(scaffoldPadding) ) }, @@ -73,9 +78,10 @@ fun AppRootScaffold( } @Composable -fun AppNavHost( +private fun NavigationBarNavHost( navController: NavHostController, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + onNavigateDetails: (String) -> Unit ) { NavHost( navController = navController, @@ -83,22 +89,19 @@ fun AppNavHost( modifier = modifier ) { composable(route = NavigationBarScreen.Market.route) { - ListScreen(navController = navController) + ListScreen(onNavigateDetails = onNavigateDetails) } composable(route = NavigationBarScreen.Favourites.route) { - FavouritesScreen(navController = navController) + FavouritesScreen(onNavigateDetails = onNavigateDetails) } composable(route = NavigationBarScreen.Search.route) { - SearchScreen(navController = navController) - } - composable(route = Screen.Details.route + "/{coinId}") { - CoinDetailsScreen(navController = navController) + SearchScreen(onNavigateDetails = onNavigateDetails) } } } @Composable -fun RowScope.AddNavigationBarItem( +private fun RowScope.AddNavigationBarItem( screen: NavigationBarScreen, currentDestination: NavDestination?, navController: NavHostController, @@ -110,7 +113,10 @@ fun RowScope.AddNavigationBarItem( NavigationBarItem( label = { - Text(text = stringResource(screen.nameResourceId)) + Text( + text = stringResource(screen.nameResourceId), + fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal + ) }, icon = { Icon( diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/RootNavGraph.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/RootNavGraph.kt deleted file mode 100644 index 69c016c7..00000000 --- a/app/src/main/java/dev/shorthouse/coinwatch/navigation/RootNavGraph.kt +++ /dev/null @@ -1,151 +0,0 @@ -package dev.shorthouse.coinwatch.navigation - -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavHost -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.navigation -import androidx.navigation.compose.rememberNavController -import dev.shorthouse.coinwatch.ui.screen.details.CoinDetailsScreen -import dev.shorthouse.coinwatch.ui.screen.favourites.FavouritesScreen -import dev.shorthouse.coinwatch.ui.screen.list.ListScreen -import dev.shorthouse.coinwatch.ui.screen.search.SearchScreen -import kotlinx.collections.immutable.persistentListOf - -@Composable -fun RootNavGraph( - modifier: Modifier = Modifier, - navController: NavHostController = rememberNavController() -) { - NavHost( - navController = navController, - route = Graph.Root.route, - startDestination = Graph.Home.route - ) { - composable(route = Graph.Home.route) { - Scaffold( - bottomBar = { AppNavigationBar(navController = navController) }, - content = { scaffoldPadding -> - HomeNavGraph( - navController = navController, - modifier = Modifier.padding(scaffoldPadding) - ) - }, - modifier = modifier - ) - } - } -} - -@Composable -fun HomeNavGraph( - navController: NavHostController, - modifier: Modifier = Modifier -) { - NavHost( - navController = navController, - route = Graph.Home.route, - startDestination = NavigationBarScreen.Market.route, - modifier = modifier - ) { - composable(route = NavigationBarScreen.Market.route) { - ListScreen(navController = navController) - } - composable(route = NavigationBarScreen.Favourites.route) { - FavouritesScreen(navController = navController) - } - composable(route = NavigationBarScreen.Search.route) { - SearchScreen(navController = navController) - } - detailsNavGraph(navController = navController) - } -} - -fun NavGraphBuilder.detailsNavGraph(navController: NavHostController) { - navigation( - route = Graph.Details.route, - startDestination = Screen.Details.route - ) { - composable(route = Screen.Details.route + "/{coinId}") { - CoinDetailsScreen(navController = navController) - } - } -} - -@Composable -fun AppNavigationBar(navController: NavHostController) { - val navigationBarScreens = remember { - persistentListOf( - NavigationBarScreen.Market, - NavigationBarScreen.Favourites, - NavigationBarScreen.Search - ) - } - - val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentDestination = navBackStackEntry?.destination - val showNavigationBar = navigationBarScreens.any { it.route == currentDestination?.route } - - if (showNavigationBar) { - NavigationBar { - navigationBarScreens.forEach { screen -> - AddItem( - screen = screen, - currentDestination = currentDestination, - navController = navController - ) - } - } - } -} - -@Composable -private fun RowScope.AddItem( - screen: NavigationBarScreen, - currentDestination: NavDestination?, - navController: NavHostController, - modifier: Modifier = Modifier -) { - val selected = currentDestination?.hierarchy?.any { destination -> - destination.route == screen.route - } == true - - NavigationBarItem( - label = { - Text(text = stringResource(screen.nameResourceId)) - }, - icon = { - Icon( - imageVector = screen.icon, - contentDescription = null - ) - }, - selected = selected, - onClick = { - navController.navigate(screen.route) { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - }, - colors = NavigationBarItemDefaults.colors( - selectedIconColor = MaterialTheme.colorScheme.onSurface, - selectedTextColor = MaterialTheme.colorScheme.onSurface, - indicatorColor = MaterialTheme.colorScheme.background, - unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, - unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant - ), - modifier = modifier - ) -} diff --git a/app/src/main/java/dev/shorthouse/coinwatch/navigation/Screen.kt b/app/src/main/java/dev/shorthouse/coinwatch/navigation/Screen.kt index 7e25a4e2..a934257f 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/navigation/Screen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/navigation/Screen.kt @@ -2,42 +2,37 @@ package dev.shorthouse.coinwatch.navigation import androidx.annotation.StringRes import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.InsertChart -import androidx.compose.material.icons.rounded.InsertChartOutlined +import androidx.compose.material.icons.rounded.BarChart +import androidx.compose.material.icons.rounded.Favorite import androidx.compose.material.icons.rounded.Search -import androidx.compose.material.icons.rounded.Star -import androidx.compose.material.icons.rounded.StarOutline import androidx.compose.ui.graphics.vector.ImageVector import dev.shorthouse.coinwatch.R sealed class NavigationBarScreen( val route: String, @StringRes val nameResourceId: Int, - val icon: ImageVector, - val selectedIcon: ImageVector + val icon: ImageVector ) { object Market : NavigationBarScreen( route = "market_screen", nameResourceId = R.string.market_screen, - icon = Icons.Rounded.InsertChartOutlined, - selectedIcon = Icons.Rounded.InsertChart + icon = Icons.Rounded.BarChart ) object Favourites : NavigationBarScreen( route = "favourites_screen", nameResourceId = R.string.favourites_screen, - icon = Icons.Rounded.StarOutline, - selectedIcon = Icons.Rounded.Star + icon = Icons.Rounded.Favorite ) object Search : NavigationBarScreen( route = "search_screen", nameResourceId = R.string.search_screen, - icon = Icons.Rounded.Search, - selectedIcon = Icons.Rounded.Search + icon = Icons.Rounded.Search ) } sealed class Screen(val route: String) { object Details : Screen("details_screen") + object NavigationBar : Screen("navigation_bar_screen") } From 36f54c2dcfa27be9a345c6dac8f7b9f678972605 Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Sat, 28 Oct 2023 11:49:33 +0100 Subject: [PATCH 74/78] Change colours to improve contrast and add new third colour --- .../main/java/dev/shorthouse/coinwatch/ui/theme/Color.kt | 6 ++++-- .../main/java/dev/shorthouse/coinwatch/ui/theme/Theme.kt | 4 ++-- app/src/main/res/values/colors.xml | 6 +++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/theme/Color.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/theme/Color.kt index 7137b630..a2a60255 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/theme/Color.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/theme/Color.kt @@ -2,9 +2,11 @@ package dev.shorthouse.coinwatch.ui.theme import androidx.compose.ui.graphics.Color -val dark_background = Color(0xFF19284D) +val dark_primaryContainer = Color(0xFF384670) +val dark_onPrimaryContainer = Color(0xFFFFFFFF) +val dark_background = Color(0xFF152241) val dark_onBackground = Color(0xFFFFFFFF) -val dark_surface = Color(0xFF253667) +val dark_surface = Color(0xFF223260) val dark_onSurface = Color(0xFFFFFFFF) val dark_onSurfaceVariant = Color(0xFFBDBDBD) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/theme/Theme.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/theme/Theme.kt index 49c27503..bccd9cd9 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/theme/Theme.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/theme/Theme.kt @@ -4,12 +4,12 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.ui.graphics.Color.Companion.White import com.google.accompanist.systemuicontroller.SystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController private val darkColorScheme = darkColorScheme( - primary = White, + primaryContainer = dark_primaryContainer, + onPrimaryContainer = dark_onPrimaryContainer, background = dark_background, onBackground = dark_onBackground, surface = dark_surface, diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 142575ee..736d12e2 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,8 +1,8 @@ - #19284D - #19284D - #253667 + #152241 + #152241 + #223260 #6F6C88 #3F3d56 #FFFFFF From 6bdd29dc5a5d9c188cadf909bac36c86a75c3bde Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Sat, 28 Oct 2023 11:49:57 +0100 Subject: [PATCH 75/78] Hoist navigation out of screens --- .../ui/screen/details/DetailsScreen.kt | 18 ++++++++---------- .../ui/screen/favourites/FavouritesScreen.kt | 8 +++----- .../coinwatch/ui/screen/list/ListScreen.kt | 16 +++++++++------- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt index a6ef6e00..a406367e 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/DetailsScreen.kt @@ -11,8 +11,8 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ArrowBack -import androidx.compose.material.icons.rounded.Star -import androidx.compose.material.icons.rounded.StarOutline +import androidx.compose.material.icons.rounded.Favorite +import androidx.compose.material.icons.rounded.FavoriteBorder import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -37,7 +37,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import coil.compose.AsyncImage import coil.decode.SvgDecoder import coil.request.ImageRequest @@ -56,14 +55,14 @@ import dev.shorthouse.coinwatch.ui.theme.AppTheme @Composable fun CoinDetailsScreen( - navController: NavController, - viewModel: DetailsViewModel = hiltViewModel() + viewModel: DetailsViewModel = hiltViewModel(), + onNavigateUp: () -> Unit ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() CoinDetailsScreen( uiState = uiState, - onNavigateUp = { navController.navigateUp() }, + onNavigateUp = onNavigateUp, onClickFavouriteCoin = { viewModel.toggleIsCoinFavourite() }, onClickChartPeriod = { viewModel.updateChartPeriod(it) }, onRefresh = { viewModel.initialiseUiState() } @@ -97,8 +96,7 @@ fun CoinDetailsScreen( else -> { DetailsEmptyTopBar( - onNavigateUp = onNavigateUp, - showFavouriteAction = uiState == DetailsUiState.Loading + onNavigateUp = onNavigateUp ) } } @@ -191,9 +189,9 @@ fun CoinDetailsTopBar( IconButton(onClick = onClickFavouriteCoin) { Icon( imageVector = if (isCoinFavourite) { - Icons.Rounded.Star + Icons.Rounded.Favorite } else { - Icons.Rounded.StarOutline + Icons.Rounded.FavoriteBorder }, contentDescription = stringResource(R.string.cd_top_bar_favourite), tint = MaterialTheme.colorScheme.onBackground diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt index ee58475c..75308ec4 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/FavouritesScreen.kt @@ -23,10 +23,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import dev.shorthouse.coinwatch.R import dev.shorthouse.coinwatch.model.Coin -import dev.shorthouse.coinwatch.navigation.Screen import dev.shorthouse.coinwatch.ui.component.ErrorState import dev.shorthouse.coinwatch.ui.previewdata.FavouritesUiStatePreviewProvider import dev.shorthouse.coinwatch.ui.screen.favourites.component.CoinFavouriteItem @@ -37,15 +35,15 @@ import kotlinx.collections.immutable.ImmutableList @Composable fun FavouritesScreen( - navController: NavController, - viewModel: FavouritesViewModel = hiltViewModel() + viewModel: FavouritesViewModel = hiltViewModel(), + onNavigateDetails: (String) -> Unit ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() FavouriteScreen( uiState = uiState, onCoinClick = { coin -> - navController.navigate(Screen.Details.route + "/${coin.id}") + onNavigateDetails(coin.id) }, onRefresh = { viewModel.initialiseUiState() } ) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/ListScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/ListScreen.kt index a18f0125..77b44974 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/ListScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/list/ListScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.KeyboardDoubleArrowUp import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -35,10 +36,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import dev.shorthouse.coinwatch.R import dev.shorthouse.coinwatch.model.Coin -import dev.shorthouse.coinwatch.navigation.Screen import dev.shorthouse.coinwatch.ui.component.ErrorState import dev.shorthouse.coinwatch.ui.previewdata.ListUiStatePreviewProvider import dev.shorthouse.coinwatch.ui.screen.list.component.ListEmptyState @@ -51,15 +50,15 @@ import kotlinx.coroutines.launch @Composable fun ListScreen( - navController: NavController, - viewModel: ListViewModel = hiltViewModel() + viewModel: ListViewModel = hiltViewModel(), + onNavigateDetails: (String) -> Unit ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() ListScreen( uiState = uiState, onCoinClick = { coin -> - navController.navigate(Screen.Details.route + "/${coin.id}") + onNavigateDetails(coin.id) }, onRefresh = { viewModel.initialiseUiState() } ) @@ -123,8 +122,11 @@ fun ListScreen( lazyListState.animateScrollToItem(0) } }, - containerColor = MaterialTheme.colorScheme.background, - contentColor = MaterialTheme.colorScheme.onBackground, + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + elevation = FloatingActionButtonDefaults.elevation( + defaultElevation = 12.dp + ), content = { Icon( imageVector = Icons.Rounded.KeyboardDoubleArrowUp, From 140e14e000357884bc5e292b11f7c5fe5e36bdde Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Sat, 28 Oct 2023 11:50:05 +0100 Subject: [PATCH 76/78] Hoist navigation out of screens --- .../shorthouse/coinwatch/ui/screen/search/SearchScreen.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchScreen.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchScreen.kt index f782357f..231347b3 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchScreen.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/search/SearchScreen.kt @@ -28,10 +28,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import dev.shorthouse.coinwatch.R import dev.shorthouse.coinwatch.model.SearchCoin -import dev.shorthouse.coinwatch.navigation.Screen import dev.shorthouse.coinwatch.ui.component.ErrorState import dev.shorthouse.coinwatch.ui.previewdata.SearchUiStatePreviewProvider import dev.shorthouse.coinwatch.ui.screen.search.component.SearchEmptyState @@ -42,8 +40,8 @@ import kotlinx.collections.immutable.ImmutableList @Composable fun SearchScreen( - navController: NavController, - viewModel: SearchViewModel = hiltViewModel() + viewModel: SearchViewModel = hiltViewModel(), + onNavigateDetails: (String) -> Unit ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -52,7 +50,7 @@ fun SearchScreen( searchQuery = viewModel.searchQuery, onSearchQueryChange = { viewModel.updateSearchQuery(it) }, onCoinClick = { coin -> - navController.navigate(Screen.Details.route + "/${coin.id}") + onNavigateDetails(coin.id) }, onRefresh = { viewModel.initialiseUiState() } ) From b683c7265cb9f887bbac37c701cb05750ed6c72d Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Sat, 28 Oct 2023 11:50:30 +0100 Subject: [PATCH 77/78] Remove favourite icon from loading details top bar --- .../details/component/DetailsEmptyTopAppBar.kt | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/DetailsEmptyTopAppBar.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/DetailsEmptyTopAppBar.kt index f7a31662..480cce97 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/DetailsEmptyTopAppBar.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/details/component/DetailsEmptyTopAppBar.kt @@ -2,7 +2,6 @@ package dev.shorthouse.coinwatch.ui.screen.details.component import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ArrowBack -import androidx.compose.material.icons.rounded.StarOutline import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -20,7 +19,6 @@ import dev.shorthouse.coinwatch.ui.theme.AppTheme @Composable fun DetailsEmptyTopBar( onNavigateUp: () -> Unit, - showFavouriteAction: Boolean, modifier: Modifier = Modifier ) { LargeTopAppBar( @@ -33,17 +31,6 @@ fun DetailsEmptyTopBar( } }, title = {}, - actions = { - if (showFavouriteAction) { - IconButton(onClick = {}) { - Icon( - imageVector = Icons.Rounded.StarOutline, - contentDescription = stringResource(R.string.cd_top_bar_favourite), - tint = MaterialTheme.colorScheme.onBackground - ) - } - } - }, colors = TopAppBarDefaults.largeTopAppBarColors( containerColor = MaterialTheme.colorScheme.background ), @@ -57,7 +44,6 @@ private fun DetailsEmptyTopBar() { AppTheme { DetailsEmptyTopBar( onNavigateUp = {}, - showFavouriteAction = true ) } } From 53f20d16ef7814e55f2f297b7ad40824d5876164 Mon Sep 17 00:00:00 2001 From: "Harry S (Macbook)" Date: Sat, 28 Oct 2023 11:50:38 +0100 Subject: [PATCH 78/78] Change favourite from star to heart --- .../ui/screen/favourites/component/FavouritesEmptyState.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesEmptyState.kt b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesEmptyState.kt index 2ce0a2ca..2261bb45 100644 --- a/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesEmptyState.kt +++ b/app/src/main/java/dev/shorthouse/coinwatch/ui/screen/favourites/component/FavouritesEmptyState.kt @@ -5,7 +5,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.StarOutline +import androidx.compose.material.icons.rounded.FavoriteBorder import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -35,7 +35,7 @@ fun FavouritesEmptyState(modifier: Modifier = Modifier) { ) Icon( - imageVector = Icons.Rounded.StarOutline, + imageVector = Icons.Rounded.FavoriteBorder, contentDescription = stringResource(R.string.cd_top_bar_favourite), tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier