From 993f27c2495cd39a5f020ab3f2b09beee7c4cb66 Mon Sep 17 00:00:00 2001 From: Ali hrera Date: Tue, 7 Oct 2025 18:51:28 +0300 Subject: [PATCH] Refactor: Organize HomeScreen composables into separate files This commit refactors `HomeScreen.kt` by extracting its main UI components into their own composable files within a new `composables` package. This improves code organization, readability, and reusability within the `home` feature module. Refactoring:** * The main `LazyColumn` logic has been simplified by using a `when` statement to handle `LoadState.Loading`, `LoadState.Error`, and the default content state. * The logic for displaying items, shimmer effects, and error states is now delegated to the newly created composables. --- .../main/java/com/droidos/home/HomeScreen.kt | 243 +++--------------- .../droidos/home/composables/CharacterItem.kt | 62 +++++ .../home/composables/CharacterShimmerItem.kt | 100 +++++++ .../home/composables/NoResultsPlaceholder.kt | 43 ++++ .../home/composables/RenderNoCharacters.kt | 33 +++ 5 files changed, 277 insertions(+), 204 deletions(-) create mode 100644 feature/home/src/main/java/com/droidos/home/composables/CharacterItem.kt create mode 100644 feature/home/src/main/java/com/droidos/home/composables/CharacterShimmerItem.kt create mode 100644 feature/home/src/main/java/com/droidos/home/composables/NoResultsPlaceholder.kt create mode 100644 feature/home/src/main/java/com/droidos/home/composables/RenderNoCharacters.kt diff --git a/feature/home/src/main/java/com/droidos/home/HomeScreen.kt b/feature/home/src/main/java/com/droidos/home/HomeScreen.kt index 3730d75..490c905 100644 --- a/feature/home/src/main/java/com/droidos/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/droidos/home/HomeScreen.kt @@ -1,47 +1,18 @@ package com.droidos.home import android.content.res.Configuration -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -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.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Search -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -50,9 +21,12 @@ import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import com.droidos.design.components.ErrorCard -import com.droidos.design.components.LoadingImage import com.droidos.design.theme.RMCTheme import com.droidos.home.actions.HomeActions +import com.droidos.home.composables.CharacterItem +import com.droidos.home.composables.CharacterShimmerItem +import com.droidos.home.composables.NoResultsPlaceholder +import com.droidos.home.composables.RenderNoCharacters import com.droidos.home.uiState.HomeUiState import com.droidos.model.beans.CharacterUIModel import retrofit2.HttpException @@ -98,60 +72,34 @@ fun HomeScreen( verticalArrangement = Arrangement.spacedBy(16.dp), state = characterState ) { - if (refreshState is LoadState.Loading) { - items(10) { - CharacterShimmerItem() - } - } - - else if (refreshState is LoadState.Error) { - val error = refreshState.error - item { - if (error is HttpException && error.code() == 404) { - NoResultsPlaceholder( - message = stringResource( - R.string.no_characters_found_for, - uiState.searchQuery - ) - ) - } else { - ErrorCard { - characters.refresh() - } + when (refreshState) { + is LoadState.Loading -> + items(10) { + CharacterShimmerItem() } - } - } - else { - if (characters.itemCount == 0) { + is LoadState.Error -> item { - NoResultsPlaceholder( - message = stringResource( - R.string.no_characters_found_for, - uiState.searchQuery - ) - ) - } - } else { - items(count = characters.itemCount) { index -> - characters[index]?.let { character -> - CharacterItem( - uiState = character, - onCLick = { onNavToDetails(character.id) } - ) - } + RenderError(refreshState.error, uiState, characters) } - if (appendState is LoadState.Loading) { - items(10) { - CharacterShimmerItem() + else -> { + if (characters.itemCount > 0) { + items(count = characters.itemCount) { index -> + characters[index]?.let { character -> + CharacterItem( + uiState = character, + onCLick = { onNavToDetails(character.id) } + ) + } } - } - else if (appendState is LoadState.Error) { + } else { item { - ErrorCard { - characters.retry() - } + RenderNoCharacters( + characters = characters, + uiState = uiState, + appendState = appendState + ) } } } @@ -160,139 +108,26 @@ fun HomeScreen( } @Composable -fun CharacterItem(uiState: CharacterUIModel, onCLick: () -> Unit) { - Card( - modifier = Modifier - .fillMaxWidth() - .clickable { onCLick() }, - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - LoadingImage( - modifier = Modifier - .size(90.dp) - .clip(CircleShape), - url = uiState.image, - description = "${uiState.name}'s image", - ) - - Spacer(modifier = Modifier.width(10.dp)) - Column { - Text( - text = uiState.name, - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = uiState.species, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) - ) - } - } - } -} - -@Composable -fun CharacterShimmerItem() { - val shimmerColors = listOf( - Color.LightGray.copy(alpha = 0.6f), - Color.LightGray.copy(alpha = 0.2f), - Color.LightGray.copy(alpha = 0.6f) - ) - - val transition = rememberInfiniteTransition(label = "") - val translateAnim = transition.animateFloat( - initialValue = 0f, - targetValue = 1000f, - animationSpec = infiniteRepeatable( - animation = tween(1200, easing = LinearEasing), - repeatMode = RepeatMode.Restart - ), label = "" - ) - - val brush = Brush.linearGradient( - colors = shimmerColors, - start = Offset(translateAnim.value, translateAnim.value), - end = Offset(translateAnim.value + 200f, translateAnim.value + 200f) - ) - - Card( - modifier = Modifier - .fillMaxWidth(), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Box( - modifier = Modifier - .size(90.dp) - .clip(CircleShape) - .background(brush) +private fun RenderError( + error: Throwable, + uiState: HomeUiState, + characters: LazyPagingItems +) { + if (error is HttpException && error.code() == 404) { + NoResultsPlaceholder( + message = stringResource( + R.string.no_characters_found_for, + uiState.searchQuery ) - - Spacer(modifier = Modifier.width(10.dp)) - - Column( - verticalArrangement = Arrangement.spacedBy(6.dp) - ) { - Box( - modifier = Modifier - .height(20.dp) - .fillMaxWidth(0.4f) - .clip(RoundedCornerShape(4.dp)) - .background(brush) - ) - - Box( - modifier = Modifier - .height(14.dp) - .fillMaxWidth(0.25f) - .clip(RoundedCornerShape(4.dp)) - .background(brush) - ) - } - } - } -} - -@Composable -fun NoResultsPlaceholder(message: String) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(24.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Icon( - imageVector = Icons.Default.Search, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(64.dp) - ) - Spacer(modifier = Modifier.height(12.dp)) - Text( - text = message, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant ) + } else { + ErrorCard { + characters.refresh() + } } } - @Preview( showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO or Configuration.UI_MODE_TYPE_NORMAL diff --git a/feature/home/src/main/java/com/droidos/home/composables/CharacterItem.kt b/feature/home/src/main/java/com/droidos/home/composables/CharacterItem.kt new file mode 100644 index 0000000..fea905d --- /dev/null +++ b/feature/home/src/main/java/com/droidos/home/composables/CharacterItem.kt @@ -0,0 +1,62 @@ +package com.droidos.home.composables + +import androidx.compose.foundation.clickable +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +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.draw.clip +import androidx.compose.ui.unit.dp +import com.droidos.design.components.LoadingImage +import com.droidos.model.beans.CharacterUIModel + +@Composable +fun CharacterItem(uiState: CharacterUIModel, onCLick: () -> Unit) { + Card( + modifier = Modifier + .fillMaxWidth() + .clickable { onCLick() }, + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + LoadingImage( + modifier = Modifier + .size(90.dp) + .clip(CircleShape), + url = uiState.image, + description = "${uiState.name}'s image", + ) + + Spacer(modifier = Modifier.width(10.dp)) + Column { + Text( + text = uiState.name, + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = uiState.species, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + } + } + } +} diff --git a/feature/home/src/main/java/com/droidos/home/composables/CharacterShimmerItem.kt b/feature/home/src/main/java/com/droidos/home/composables/CharacterShimmerItem.kt new file mode 100644 index 0000000..8fb69e4 --- /dev/null +++ b/feature/home/src/main/java/com/droidos/home/composables/CharacterShimmerItem.kt @@ -0,0 +1,100 @@ +package com.droidos.home.composables + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +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.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.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +fun CharacterShimmerItem() { + val shimmerColors = listOf( + Color.LightGray.copy(alpha = 0.6f), + Color.LightGray.copy(alpha = 0.2f), + Color.LightGray.copy(alpha = 0.6f) + ) + + val transition = rememberInfiniteTransition(label = "") + val translateAnim = transition.animateFloat( + initialValue = 0f, + targetValue = 1000f, + animationSpec = infiniteRepeatable( + animation = tween(1200, easing = LinearEasing), + repeatMode = RepeatMode.Restart + ), label = "" + ) + + val brush = Brush.linearGradient( + colors = shimmerColors, + start = Offset(translateAnim.value, translateAnim.value), + end = Offset(translateAnim.value + 200f, translateAnim.value + 200f) + ) + + Card( + modifier = Modifier + .fillMaxWidth(), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(90.dp) + .clip(CircleShape) + .background(brush) + ) + + Spacer(modifier = Modifier.width(10.dp)) + + Column( + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Box( + modifier = Modifier + .height(20.dp) + .fillMaxWidth(0.4f) + .clip(RoundedCornerShape(4.dp)) + .background(brush) + ) + + Box( + modifier = Modifier + .height(14.dp) + .fillMaxWidth(0.25f) + .clip(RoundedCornerShape(4.dp)) + .background(brush) + ) + } + } + } +} diff --git a/feature/home/src/main/java/com/droidos/home/composables/NoResultsPlaceholder.kt b/feature/home/src/main/java/com/droidos/home/composables/NoResultsPlaceholder.kt new file mode 100644 index 0000000..3ad8a48 --- /dev/null +++ b/feature/home/src/main/java/com/droidos/home/composables/NoResultsPlaceholder.kt @@ -0,0 +1,43 @@ +package com.droidos.home.composables + +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.material.icons.Icons +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.Icon +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.unit.dp + +@Composable +fun NoResultsPlaceholder(message: String) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = Icons.Default.Search, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(64.dp) + ) + + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = message, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} diff --git a/feature/home/src/main/java/com/droidos/home/composables/RenderNoCharacters.kt b/feature/home/src/main/java/com/droidos/home/composables/RenderNoCharacters.kt new file mode 100644 index 0000000..6d50083 --- /dev/null +++ b/feature/home/src/main/java/com/droidos/home/composables/RenderNoCharacters.kt @@ -0,0 +1,33 @@ +package com.droidos.home.composables + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import com.droidos.design.components.ErrorCard +import com.droidos.home.R +import com.droidos.home.uiState.HomeUiState +import com.droidos.model.beans.CharacterUIModel + +@Composable + fun RenderNoCharacters( + characters: LazyPagingItems, + uiState: HomeUiState, + appendState: LoadState, + ) { + if (characters.itemCount == 0) { + NoResultsPlaceholder( + message = stringResource( + R.string.no_characters_found_for, + uiState.searchQuery + ) + ) + } else if (appendState is LoadState.Loading) { + CharacterShimmerItem() + + } else if (appendState is LoadState.Error) { + ErrorCard { + characters.retry() + } + } +} \ No newline at end of file