Skip to content

Commit

Permalink
Feature/upgrade catalog list UI (#3)
Browse files Browse the repository at this point in the history
* Updated version catalog, added koin for di and guava dependencies
* feat(mobile-app): Configured koin modules
* feat(mobile-app): Renamed old files
* feat(mobile-app): Updated welcome screen composables
* feat(mobile-app): Updated main activity
* feat(feat-core-catalog-src): Added catalog data in english for fetching
* feat(mobile-app): Updated catalog data service dependency for dependency injection
* feat(mobile-app): Updated string resource values
* feat(mobile-app): Updated unit tests
* feat(mobile-app): Renamed old files
* feat(mobile-app): Removed viewmodel factory for usage in koin di module
* feat(mobile-app): Updated catalog favorites route composable ui
* feat(mobile-app): Updated catalog settings route composable ui and viewmodel
* feat(mobile-app): Updated scaffold and navigation type
* feat(mobile-app): Updated device posture ui feature
* feat(mobile-app): Updated catalog search route composable ui
* feat(mobile-app): Updated catalog list route composable ui
* feat(mobile-app): Updated welcome route composable ui
* feat(mobile-app): Updated app ui state
* feat(mobile-app): Updated app scaffold content type classifier
* feat(mobile-app): Updated string resources
* feat(mobile-app): Updated catalog home banner image resources
* Updated version catalog
* feat(mobile-app): Updated string resources
* feat(mobile-app): Added jv logo image resources
* feat(mobile-app): Upgraded catalog list route ui
* feat(database): Upgraded catalog item entity information
* feat(mobile-app): Upgraded catalog list unit tests
* feat(mobile-app): Updated mobile app unit tests
* feat(mobile-app): Updated main activity
* feat(mobile-app): Updated app ui state
* feat(mobile-app): Organized opt-in annotations
* codefactor: Fixed unnecessary parentheses
  • Loading branch information
marlonlom authored Mar 6, 2024
1 parent 2b6bcdf commit 6fec77c
Show file tree
Hide file tree
Showing 41 changed files with 929 additions and 178 deletions.
1 change: 1 addition & 0 deletions apps/mobile-app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ dependencies {
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.bundles.database.room)
implementation(libs.bundles.mobileapp.google.accompanist.pager)

testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2024 Marlonlom
* SPDX-License-Identifier: Apache-2.0
*/

package dev.marlonlom.apps.cappajv.features.catalog_list

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import com.google.accompanist.pager.ExperimentalPagerApi
import dev.marlonlom.apps.cappajv.core.database.entities.CatalogItemTuple
import dev.marlonlom.apps.cappajv.features.catalog_list.screens.DefaultPortraitCatalogListScreen
import dev.marlonlom.apps.cappajv.features.catalog_list.screens.LandscapeCompactCatalogListScreen
import dev.marlonlom.apps.cappajv.ui.layout.DevicePosture
import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState

/**
* Catalog list content composable ui, conditioned by application ui state.
*
* @author marlonlom
*
* @param appState Application ui state.
* @param catalogItemsListState Catalog items lazy list state.
* @param catalogItems Catalog items list.
* @param categories Categories list.
* @param selectedCategory Selected category name.
* @param onSelectedCategoryChanged Action for category selected.
* @param onCatalogItemSelected Action for catalog item selected.
*/
@ExperimentalFoundationApi
@ExperimentalPagerApi
@ExperimentalLayoutApi
@Composable
fun CatalogListContent(
appState: CappajvAppState,
catalogItemsListState: LazyListState,
catalogItems: List<CatalogItemTuple>,
categories: List<String>,
selectedCategory: String,
onSelectedCategoryChanged: (String) -> Unit,
onCatalogItemSelected: (Long) -> Unit,
) {
when {
appState.isLandscape.and(appState.devicePosture == DevicePosture.Normal) -> {
LandscapeCompactCatalogListScreen(
appState = appState,
catalogItemsListState = catalogItemsListState,
catalogItems = catalogItems,
categories = categories,
selectedCategory = selectedCategory,
onSelectedCategoryChanged = onSelectedCategoryChanged,
onCatalogItemSelected = onCatalogItemSelected,
)
}

else -> {
DefaultPortraitCatalogListScreen(
appState = appState,
catalogItemsListState = catalogItemsListState,
catalogItems = catalogItems,
categories = categories,
selectedCategory = selectedCategory,
onSelectedCategoryChanged = onSelectedCategoryChanged,
onCatalogItemSelected = onCatalogItemSelected,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ class CatalogListRepository(
) {

/** Catalog products list as flow. */
val allProducts: Flow<CatalogListState> = localDataSource.getAllProducts()
val allProducts: Flow<CatalogListUiState> = localDataSource.getAllProducts()
.transform { tuples ->
when {
tuples.isNotEmpty() -> CatalogListState.Listing(tuples.groupBy { it.category })
else -> CatalogListState.Empty
tuples.isNotEmpty() -> CatalogListUiState.Listing(tuples.groupBy { it.category })
else -> CatalogListUiState.Empty
}.also { state ->
emit(state)
}
Expand Down Expand Up @@ -99,5 +99,7 @@ private val RemoteCatalogItem.toEntity: CatalogItem
titleNormalized = title.toSentenceCase,
picture = picture,
category = category,
detail = detail
detail = detail,
samplePunctuation = punctuations.first().let { "${it.label}:${it.pointsQty} pts" },
punctuationsCount = punctuations.count()
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,38 @@
package dev.marlonlom.apps.cappajv.features.catalog_list

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.paddingFromBaseline
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.res.stringArrayResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.google.accompanist.pager.ExperimentalPagerApi
import dev.marlonlom.apps.cappajv.R
import dev.marlonlom.apps.cappajv.core.database.entities.CatalogItemTuple
import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
import timber.log.Timber

/**
* Catalog list route composable ui.
*
* @author marlonlom
*
* @param appState Application ui state
* @param viewModel Catalog list viewmodel.
*/
@ExperimentalPagerApi
@ExperimentalFoundationApi
@ExperimentalMaterial3Api
@ExperimentalLayoutApi
Expand All @@ -56,124 +46,57 @@ fun CatalogListRoute(
appState: CappajvAppState,
viewModel: CatalogListViewModel = koinViewModel(),
) {
val coroutineScope = rememberCoroutineScope()
val categoriesList = stringArrayResource(R.array.array_catalog_list_categories).toList()
val firstCategory = categoriesList.first()
val selectedCategory = rememberSaveable { mutableStateOf(firstCategory) }
val catalogListUiState: CatalogListUiState by viewModel.uiState.collectAsStateWithLifecycle()
val catalogListScrollState = rememberLazyListState()

val catalogListState: CatalogListState by viewModel.uiState.collectAsStateWithLifecycle()
when (catalogListUiState) {
CatalogListUiState.Empty -> Unit
CatalogListUiState.Loading -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
}
}

val contentHorizontalPadding = when {
appState.isLandscape.not().and(appState.isMediumWidth) -> 40.dp
appState.isLandscape.not().and(appState.isExpandedWidth) -> 80.dp
else -> 20.dp
}
is CatalogListUiState.Listing -> {

LazyColumn(
modifier = Modifier
.safeContentPadding()
.padding(bottom = 0.dp),
) {
stickyHeader {
Text(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.paddingFromBaseline(top = 40.dp, bottom = 20.dp),
text = "Consigue Nuestros premios",
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onBackground,
style = MaterialTheme.typography.headlineMedium
)
}
val originalCatalogList: List<CatalogItemTuple> = (catalogListUiState as CatalogListUiState.Listing).catalogMap
.map { (_, tuples) -> tuples }.flatten()

when (catalogListState) {
CatalogListState.Empty -> {
item {
Text(" :( ")
val filteredCatalogByCategory: (String) -> List<CatalogItemTuple> = { category ->
when (category) {
firstCategory -> originalCatalogList
else -> originalCatalogList.filter { it.category == category }
}
}

CatalogListState.Loading -> {
item { Text("...") }
val catalogItemsList = rememberSaveable {
mutableStateOf(filteredCatalogByCategory(firstCategory))
}

is CatalogListState.Listing -> {
val listingsData = catalogListState as CatalogListState.Listing

listingsData.map.keys.sorted().forEach { category ->
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 20.dp, bottom = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = category,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
TextButton(onClick = {}) {
Text(text = stringResource(R.string.text_catalog_list_see_all))
}
}
}

item {
val tuples: List<CatalogItemTuple> = listingsData.map[category]
.orEmpty().shuffled().subList(0, 5)
LazyRow(
horizontalArrangement = Arrangement.spacedBy(20.dp),
) {
items(tuples.size) { pos ->
val tuple = tuples[pos]
Card(modifier = Modifier
.width(160.dp),
shape = RoundedCornerShape(10.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(
alpha = 0.25f
),
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
),
onClick = {
Timber.d("[CatalogListRoute] tuple=${tuple.title}")
}) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween,
) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(tuple.picture)
.crossfade(true)
.build(),
contentDescription = tuple.title,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.size(140.dp)
.background(Color.White),
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
text = tuple.title,
style = MaterialTheme.typography.bodyMedium,
minLines = 2,
overflow = TextOverflow.Ellipsis,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center
)
}
}
}
}
CatalogListContent(
appState = appState,
catalogItemsListState = catalogListScrollState,
catalogItems = catalogItemsList.value,
categories = categoriesList,
selectedCategory = selectedCategory.value,
onSelectedCategoryChanged = { category ->
selectedCategory.value = category
catalogItemsList.value = filteredCatalogByCategory(category)
coroutineScope.launch {
catalogListScrollState.animateScrollToItem(0)
}
}
}
},
onCatalogItemSelected = { itemId ->
Timber.d("[CatalogListRoute] CatalogItemTuple.id[$itemId]")
},
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,27 @@ package dev.marlonlom.apps.cappajv.features.catalog_list
import dev.marlonlom.apps.cappajv.core.database.entities.CatalogItemTuple

/**
* Catalog ui state sealed class.
* Catalog list ui state sealed class.
*
* @author marlonlom
*/
sealed class CatalogListState {
sealed class CatalogListUiState {
/**
* Catalog ui state as loading state object.
*/
data object Loading : CatalogListState()
data object Loading : CatalogListUiState()

/**
* Catalog ui state as empty results object.
*/
data object Empty : CatalogListState()
data object Empty : CatalogListUiState()

/**
* Catalog ui state as non empty list results data class.
*
* @property map Grouped catalog items map.
* @property catalogMap Grouped catalog items.
*/
data class Listing(val map: Map<String, List<CatalogItemTuple>>) : CatalogListState()
data class Listing(
val catalogMap: Map<String, List<CatalogItemTuple>>
) : CatalogListUiState()
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ package dev.marlonlom.apps.cappajv.features.catalog_list
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import dev.marlonlom.apps.cappajv.features.catalog_list.CatalogListState.Loading
import dev.marlonlom.apps.cappajv.features.catalog_list.CatalogListUiState.Loading
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
Expand Down
Loading

0 comments on commit 6fec77c

Please sign in to comment.