Skip to content

Commit

Permalink
[Jetcaster] Handle error and loading states (#1363)
Browse files Browse the repository at this point in the history
* Adding loading/error states to Home
* Adding loading/error states to PodcastImage

<img width="768" alt="image"
src="https://github.com/android/compose-samples/assets/463186/fddef339-3ff2-4035-a090-5ff3d0ba8056">
  • Loading branch information
arriolac authored Apr 26, 2024
2 parents 8206dec + 8f4c508 commit 1c7bac4
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 72 deletions.
2 changes: 1 addition & 1 deletion Jetcaster/designsystem/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ android {

defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()

vectorDrawables.useSupportLibrary = true
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
Expand All @@ -32,10 +31,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource
import coil.compose.AsyncImagePainter
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import com.example.jetcaster.designsystem.R

@Composable
fun PodcastImage(
Expand All @@ -45,6 +46,11 @@ fun PodcastImage(
contentScale: ContentScale = ContentScale.Crop,
placeholderBrush: Brush = thumbnailPlaceholderDefaultBrush(),
) {
if (LocalInspectionMode.current) {
Box(modifier = modifier.background(MaterialTheme.colorScheme.primary))
return
}

var imagePainterState by remember {
mutableStateOf<AsyncImagePainter.State>(AsyncImagePainter.State.Empty)
}
Expand All @@ -63,14 +69,15 @@ fun PodcastImage(
contentAlignment = Alignment.Center
) {
when (imagePainterState) {
is AsyncImagePainter.State.Loading -> {
CircularProgressIndicator(
is AsyncImagePainter.State.Loading,
is AsyncImagePainter.State.Error -> {
Image(
painter = painterResource(id = R.drawable.img_empty),
contentDescription = null,
modifier = Modifier
.size(48.dp)
.align(Alignment.Center)
.fillMaxSize()
)
}

else -> {
Box(
modifier = Modifier
Expand Down
61 changes: 61 additions & 0 deletions Jetcaster/designsystem/src/main/res/drawable/img_empty.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="180dp"
android:height="180dp"
android:viewportWidth="180"
android:viewportHeight="180">
<path
android:pathData="M0,0h180v180h-180z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="90"
android:startY="0"
android:endX="90"
android:endY="180"
android:type="linear">
<item android:offset="0" android:color="@color/surface_bright"/>
<item android:offset="1" android:color="@color/background"/>
</gradient>
</aapt:attr>
</path>
<group>
<clip-path
android:pathData="M56.67,123.52l66.85,-0l0,-66.85l-66.85,-0z"/>
<path
android:pathData="M80.54,108.49C77.92,108.49 75.77,107.33 75.77,105.9V59.26C75.77,57.83 77.92,56.67 80.54,56.67C83.17,56.67 85.32,57.83 85.32,59.26V105.9C85.32,107.33 83.17,108.49 80.54,108.49Z"
android:fillColor="#1A120A"/>
<path
android:pathData="M61.44,92.95C58.82,92.95 56.67,91.78 56.67,90.35V74.81C56.67,73.38 58.82,72.22 61.44,72.22C64.07,72.22 66.22,73.38 66.22,74.81V90.35C66.22,91.78 64.07,92.95 61.44,92.95Z"
android:fillColor="#1A120A"/>
<path
android:pathData="M99.64,98.13C97.02,98.13 94.87,96.96 94.87,95.54V69.62C94.87,68.2 97.02,67.03 99.64,67.03C102.27,67.03 104.42,68.2 104.42,69.62V95.54C104.42,96.96 102.27,98.13 99.64,98.13Z"
android:fillColor="#1A120A"/>
<path
android:pathData="M118.74,90.35C116.11,90.35 113.97,89.19 113.97,87.76V77.4C113.97,75.97 116.11,74.81 118.74,74.81C121.37,74.81 123.52,75.97 123.52,77.4V87.76C123.52,89.19 121.37,90.35 118.74,90.35Z"
android:fillColor="#1A120A"/>
<path
android:pathData="M80.54,115.46C77.92,115.46 75.77,114.29 75.77,112.86V66.22C75.77,64.8 77.92,63.63 80.54,63.63C83.17,63.63 85.32,64.8 85.32,66.22V112.86C85.32,114.29 83.17,115.46 80.54,115.46Z"
android:fillColor="#1A120A"/>
<path
android:pathData="M61.44,99.91C58.82,99.91 56.67,98.74 56.67,97.32V81.77C56.67,80.34 58.82,79.18 61.44,79.18C64.07,79.18 66.22,80.34 66.22,81.77V97.32C66.22,98.74 64.07,99.91 61.44,99.91Z"
android:fillColor="#1A120A"/>
<path
android:pathData="M99.64,105.09C97.02,105.09 94.87,103.92 94.87,102.5V76.59C94.87,75.16 97.02,74 99.64,74C102.27,74 104.42,75.16 104.42,76.59V102.5C104.42,103.92 102.27,105.09 99.64,105.09Z"
android:fillColor="#1A120A"/>
<path
android:pathData="M118.74,97.32C116.11,97.32 113.97,96.15 113.97,94.73V84.36C113.97,82.93 116.11,81.77 118.74,81.77C121.37,81.77 123.52,82.93 123.52,84.36V94.73C123.52,96.15 121.37,97.32 118.74,97.32Z"
android:fillColor="#1A120A"/>
<path
android:pathData="M80.54,123.52C77.91,123.52 75.76,122.48 75.76,121.21V79.55C75.76,78.28 77.91,77.24 80.54,77.24C83.16,77.24 85.31,78.28 85.31,79.55V121.21C85.31,122.48 83.16,123.52 80.54,123.52Z"
android:fillColor="#1A120A"/>
<path
android:pathData="M61.44,109.64C58.81,109.64 56.67,108.59 56.67,107.32V93.44C56.67,92.16 58.81,91.12 61.44,91.12C64.07,91.12 66.21,92.16 66.21,93.44V107.32C66.21,108.59 64.07,109.64 61.44,109.64Z"
android:fillColor="#1A120A"/>
<path
android:pathData="M99.63,114.26C97.01,114.26 94.86,113.22 94.86,111.95V88.81C94.86,87.54 97.01,86.49 99.63,86.49C102.26,86.49 104.41,87.54 104.41,88.81V111.95C104.41,113.22 102.26,114.26 99.63,114.26Z"
android:fillColor="#1A120A"/>
<path
android:pathData="M118.73,107.32C116.11,107.32 113.96,106.28 113.96,105.01V95.75C113.96,94.48 116.11,93.44 118.73,93.44C121.36,93.44 123.5,94.48 123.5,95.75V105.01C123.5,106.28 121.36,107.32 118.73,107.32Z"
android:fillColor="#1A120A"/>
</group>
</vector>
5 changes: 5 additions & 0 deletions Jetcaster/designsystem/src/main/res/values-night/colors.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="background">#FF1A120A</color>
<color name="surface_bright">#FF42372D</color>
</resources>
5 changes: 5 additions & 0 deletions Jetcaster/designsystem/src/main/res/values/colors.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="background">#FFFFF8F4</color>
<color name="surface_bright">#FFFFF8F4</color>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
Expand Down Expand Up @@ -129,7 +131,6 @@ import kotlinx.coroutines.launch
data class HomeState(
val windowSizeClass: WindowSizeClass,
val featuredPodcasts: PersistentList<PodcastInfo>,
val isRefreshing: Boolean,
val selectedHomeCategory: HomeCategory,
val homeCategories: List<HomeCategory>,
val filterableCategoriesModel: FilterableCategoriesModel,
Expand Down Expand Up @@ -230,14 +231,73 @@ private fun getExcludedVerticalBounds(posture: Posture, hingePolicy: HingePolicy
}
}

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun MainScreen(
windowSizeClass: WindowSizeClass,
navigateToPlayer: (EpisodeInfo) -> Unit,
viewModel: HomeViewModel = hiltViewModel()
) {
val viewState by viewModel.state.collectAsStateWithLifecycle()
val homeScreenUiState by viewModel.state.collectAsStateWithLifecycle()
when (val uiState = homeScreenUiState) {
is HomeScreenUiState.Loading -> HomeScreenLoading()
is HomeScreenUiState.Error -> HomeScreenError(onRetry = viewModel::refresh)
is HomeScreenUiState.Ready -> {
HomeScreenReady(
uiState = uiState,
windowSizeClass = windowSizeClass,
navigateToPlayer = navigateToPlayer,
viewModel = viewModel,
)
}
}
}

@Composable
private fun HomeScreenLoading(modifier: Modifier = Modifier) {
Surface(modifier.fillMaxSize()) {
Box {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
}
}

@Composable
private fun HomeScreenError(onRetry: () -> Unit, modifier: Modifier = Modifier) {
Surface(modifier = modifier) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize(),
) {
Text(
text = stringResource(id = R.string.an_error_has_occurred),
modifier = Modifier.padding(16.dp)
)
Button(onClick = onRetry) {
Text(text = stringResource(id = R.string.retry_label))
}
}
}
}

@Preview
@Composable
fun HomeScreenErrorPreview() {
JetcasterTheme {
HomeScreenError(onRetry = {})
}
}

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
private fun HomeScreenReady(
uiState: HomeScreenUiState.Ready,
windowSizeClass: WindowSizeClass,
navigateToPlayer: (EpisodeInfo) -> Unit,
viewModel: HomeViewModel = hiltViewModel()
) {
val navigator = rememberSupportingPaneScaffoldNavigator<String>(
scaffoldDirective = calculateScaffoldDirective(currentWindowAdaptiveInfo())
)
Expand All @@ -247,13 +307,12 @@ fun MainScreen(

val homeState = HomeState(
windowSizeClass = windowSizeClass,
featuredPodcasts = viewState.featuredPodcasts,
isRefreshing = viewState.refreshing,
homeCategories = viewState.homeCategories,
selectedHomeCategory = viewState.selectedHomeCategory,
filterableCategoriesModel = viewState.filterableCategoriesModel,
podcastCategoryFilterResult = viewState.podcastCategoryFilterResult,
library = viewState.library,
featuredPodcasts = uiState.featuredPodcasts,
homeCategories = uiState.homeCategories,
selectedHomeCategory = uiState.selectedHomeCategory,
filterableCategoriesModel = uiState.filterableCategoriesModel,
podcastCategoryFilterResult = uiState.podcastCategoryFilterResult,
library = uiState.library,
onHomeCategorySelected = viewModel::onHomeCategorySelected,
onCategorySelected = viewModel::onCategorySelected,
onPodcastUnfollowed = viewModel::onPodcastUnfollowed,
Expand Down Expand Up @@ -403,7 +462,6 @@ private fun HomeScreen(
showGrid = showGrid,
showHomeCategoryTabs = homeState.showHomeCategoryTabs,
featuredPodcasts = homeState.featuredPodcasts,
isRefreshing = homeState.isRefreshing,
selectedHomeCategory = homeState.selectedHomeCategory,
homeCategories = homeState.homeCategories,
filterableCategoriesModel = homeState.filterableCategoriesModel,
Expand Down Expand Up @@ -433,7 +491,6 @@ private fun HomeContent(
showGrid: Boolean,
showHomeCategoryTabs: Boolean,
featuredPodcasts: PersistentList<PodcastInfo>,
isRefreshing: Boolean,
selectedHomeCategory: HomeCategory,
homeCategories: List<HomeCategory>,
filterableCategoriesModel: FilterableCategoriesModel,
Expand Down Expand Up @@ -467,7 +524,6 @@ private fun HomeContent(
pagerState = pagerState,
showHomeCategoryTabs = showHomeCategoryTabs,
featuredPodcasts = featuredPodcasts,
isRefreshing = isRefreshing,
selectedHomeCategory = selectedHomeCategory,
homeCategories = homeCategories,
filterableCategoriesModel = filterableCategoriesModel,
Expand All @@ -487,7 +543,6 @@ private fun HomeContent(
pagerState = pagerState,
showHomeCategoryTabs = showHomeCategoryTabs,
featuredPodcasts = featuredPodcasts,
isRefreshing = isRefreshing,
selectedHomeCategory = selectedHomeCategory,
homeCategories = homeCategories,
filterableCategoriesModel = filterableCategoriesModel,
Expand All @@ -511,7 +566,6 @@ private fun HomeContentColumn(
showHomeCategoryTabs: Boolean,
pagerState: PagerState,
featuredPodcasts: PersistentList<PodcastInfo>,
isRefreshing: Boolean,
selectedHomeCategory: HomeCategory,
homeCategories: List<HomeCategory>,
filterableCategoriesModel: FilterableCategoriesModel,
Expand Down Expand Up @@ -542,10 +596,6 @@ private fun HomeContentColumn(
}
}

if (isRefreshing) {
// TODO show a progress indicator or similar
}

if (showHomeCategoryTabs) {
item {
HomeCategoryTabs(
Expand Down Expand Up @@ -586,7 +636,6 @@ private fun HomeContentGrid(
showHomeCategoryTabs: Boolean,
pagerState: PagerState,
featuredPodcasts: PersistentList<PodcastInfo>,
isRefreshing: Boolean,
selectedHomeCategory: HomeCategory,
homeCategories: List<HomeCategory>,
filterableCategoriesModel: FilterableCategoriesModel,
Expand Down Expand Up @@ -618,10 +667,6 @@ private fun HomeContentGrid(
}
}

if (isRefreshing) {
// TODO show a progress indicator or similar
}

if (showHomeCategoryTabs) {
fullWidthItem {
Row {
Expand Down Expand Up @@ -868,7 +913,6 @@ private fun PreviewHomeContent() {
val homeState = HomeState(
windowSizeClass = CompactWindowSizeClass,
featuredPodcasts = PreviewPodcasts.toPersistentList(),
isRefreshing = false,
homeCategories = HomeCategory.entries,
selectedHomeCategory = HomeCategory.Discover,
filterableCategoriesModel = FilterableCategoriesModel(
Expand Down Expand Up @@ -905,7 +949,6 @@ private fun PreviewHomeContentExpanded() {
val homeState = HomeState(
windowSizeClass = CompactWindowSizeClass,
featuredPodcasts = PreviewPodcasts.toPersistentList(),
isRefreshing = false,
homeCategories = HomeCategory.entries,
selectedHomeCategory = HomeCategory.Discover,
filterableCategoriesModel = FilterableCategoriesModel(
Expand Down
Loading

0 comments on commit 1c7bac4

Please sign in to comment.