Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/add catalog detail UI route #43

Merged
merged 31 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0c81aa0
feat(mobile-app): Updated unit tests
marlonlom Mar 27, 2024
51b3ced
feat(mobile-app): Updated unit tests
marlonlom Mar 27, 2024
dc726e5
feat(mobile-app): Updated di module definitions
marlonlom Mar 27, 2024
7474cfd
feat(mobile-app): Updated catalog details viewmodel
marlonlom Mar 27, 2024
9b164cc
feat(mobile-app): Added catalog detail screen composables
marlonlom Mar 27, 2024
84fbd8c
feat(mobile-app): Added catalog list click to details actions
marlonlom Mar 27, 2024
7492a5e
feat(mobile-app): Added catalog item sharing utility, updated app con…
marlonlom Mar 27, 2024
1da7f5f
feat(mobile-app): Added catalog detail navigation action
marlonlom Mar 27, 2024
1b115f4
feat(mobile-app): Updated validation for detail route display
marlonlom Mar 27, 2024
a0c4724
feat(mobile-app): Updated navigation for catalog detail destination
marlonlom Mar 27, 2024
4f60696
feat(mobile-app): Updated string resources for catalog detail screens
marlonlom Mar 27, 2024
514a66a
feat(mobile-app): Updated catalog item sharing utility, updated app c…
marlonlom Mar 27, 2024
ee440a4
feat(mobile-app): Added catalog detail route screen composable ui
marlonlom Mar 27, 2024
26977a4
feat(mobile-app): Added callbacks for sharing feature in navigation g…
marlonlom Mar 27, 2024
a5f0656
feat(mobile-app): Added callbacks for sharing feature in catalog home ui
marlonlom Mar 27, 2024
7865385
feat(mobile-app): Updated catalog list composable screens
marlonlom Mar 27, 2024
b4835c2
feat(mobile-app): Updated catalog search composable screens
marlonlom Mar 27, 2024
2fc3b7d
feat(mobile-app): Updated catalog list composable screens
marlonlom Mar 27, 2024
619e94a
feat(mobile-app): Updated catalog search composable screens
marlonlom Mar 27, 2024
b75b9f9
feat(mobile-app): Updated catalog favorites composable screens
marlonlom Mar 27, 2024
59083dc
feat(mobile-app): Updated catalog favorites composable screens
marlonlom Mar 27, 2024
dd65e19
feat(mobile-app): Updated navigation host
marlonlom Mar 27, 2024
fc6fd18
feat(mobile-app): Updated string resources for catalog detail screens
marlonlom Mar 27, 2024
ae294a8
feat(mobile-app): Updated docs for catalog favorites ui
marlonlom Mar 27, 2024
3f7b185
feat(mobile-app): Updated catalog details slots ui
marlonlom Mar 27, 2024
61ee633
feat(mobile-app): Updated catalog details top app bar
marlonlom Mar 27, 2024
1a15ac4
feat(mobile-app): Updated catalog details screen layouts
marlonlom Mar 27, 2024
8df693a
feat(mobile-app): Updated catalog details screen route composable
marlonlom Mar 27, 2024
dca5dbf
feat(mobile-app): Updated catalog list composable javadocs
marlonlom Mar 27, 2024
c77c21d
feat(mobile-app): Updated catalog home,favorites,search ui, javadocs,…
marlonlom Mar 27, 2024
62563b4
feat(mobile-app): Updated navigation host
marlonlom Mar 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import dev.marlonlom.apps.cappajv.core.database.datasource.LocalDataSource
import dev.marlonlom.apps.cappajv.core.database.datasource.LocalDataSourceImpl
import dev.marlonlom.apps.cappajv.core.preferences.UserPreferencesRepository
import dev.marlonlom.apps.cappajv.dataStore
import dev.marlonlom.apps.cappajv.features.catalog_detail.CatalogDetailRepository
import dev.marlonlom.apps.cappajv.features.catalog_favorites.CatalogFavoritesRepository
import dev.marlonlom.apps.cappajv.features.catalog_list.CatalogListRepository
import dev.marlonlom.apps.cappajv.features.catalog_search.CatalogSearchRepository
Expand Down Expand Up @@ -48,6 +49,11 @@ val dataModule = module {
localDataSource = get(),
)
}
single<CatalogDetailRepository> {
CatalogDetailRepository(
localDataSource = get(),
)
}
single {
UserPreferencesRepository(androidContext().dataStore)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import dev.marlonlom.apps.cappajv.features.catalog_favorites.CatalogFavoritesVie
import dev.marlonlom.apps.cappajv.features.catalog_list.CatalogListViewModel
import dev.marlonlom.apps.cappajv.features.catalog_search.CatalogSearchViewModel
import dev.marlonlom.apps.cappajv.features.settings.SettingsViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.dsl.module

@ExperimentalCoroutinesApi
val viewModelsModule = module {
includes(dataModule)
viewModelOf(::CatalogListViewModel)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2024 Marlonlom
* SPDX-License-Identifier: Apache-2.0
*/

package dev.marlonlom.apps.cappajv.features.catalog_detail

import androidx.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.marlonlom.apps.cappajv.features.catalog_detail.screens.CatalogDetailRouteScreen
import dev.marlonlom.apps.cappajv.ui.main.AppContentCallbacks
import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.koin.androidx.compose.koinViewModel

/**
* Catalog detail route composable ui.
*
* @author marlonlom
*
* @param appState Application ui state.
* @param catalogId Selected catalog item id.
* @param viewModel Catalog detail viewmodel.
*/
@ExperimentalFoundationApi
@ExperimentalCoroutinesApi
@Composable
fun CatalogDetailRoute(
appState: CappajvAppState,
appContentCallbacks: AppContentCallbacks,
isRouting: Boolean,
catalogId: Long,
viewModel: CatalogDetailViewModel = koinViewModel(),
) {

if (isRouting) {
BackHandler {
appState.navController.navigateUp()
}
}

viewModel.find(catalogId)
val detailUiState by viewModel.detail.collectAsStateWithLifecycle()

CatalogDetailRouteScreen(
appState = appState,
appContentCallbacks = appContentCallbacks,
isRouting = isRouting,
detailUiState = detailUiState,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,84 @@

package dev.marlonlom.apps.cappajv.features.catalog_detail

import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import dev.marlonlom.apps.cappajv.features.catalog_detail.CatalogDetailUiState.Found
import dev.marlonlom.apps.cappajv.features.catalog_detail.CatalogDetailUiState.NotFound
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn

/**
* Catalog detail view model class.
*
* @author marlonlom
*
* @property repository catalog detail repository dependency
* @property savedStateHandle Saved state handle for the viewmodel.
*/
@ExperimentalCoroutinesApi
class CatalogDetailViewModel(
private val repository: CatalogDetailRepository
private val repository: CatalogDetailRepository,
private val savedStateHandle: SavedStateHandle = SavedStateHandle()
) : ViewModel() {

var detail: MutableState<CatalogDetail?> = mutableStateOf(null)
private set
/** Catalog detailed item value as state flow. */
val detail = savedStateHandle.getStateFlow(CATALOG_DETAIL_ID_KEY, NO_CATALOG_ID)
.flatMapLatest { catalogId ->
repository.find(catalogId)
}.mapLatest { detail ->
if (detail == null) NotFound else Found(detail)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = NotFound
)

suspend fun find(itemId: Long) {
viewModelScope.launch {
repository.find(itemId).collect {
detail.value = it
}
}
/**
* Perform detailed search using catalog selected id.
*
* @param itemId Catalog item id value.
*/
fun find(itemId: Long) {
savedStateHandle[CATALOG_DETAIL_ID_KEY] = itemId
}

companion object {

private const val NO_CATALOG_ID = 0L

private const val CATALOG_DETAIL_ID_KEY = "CATALOG_DETAIL_ID"

}

}

/**
* Catalog details ui state sealed class.
*
* @author marlonlom
*/
sealed class CatalogDetailUiState {

/**
* Not found phase for catalog details ui state.
*
* @author marlonlom
*/
data object NotFound : CatalogDetailUiState()

/**
* Success phase for catalog details ui state.
*
* @author marlonlom
*
* @property detail Catalog item detailed information.
*/
data class Found(
val detail: CatalogDetail
) : CatalogDetailUiState()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2024 Marlonlom
* SPDX-License-Identifier: Apache-2.0
*/

package dev.marlonlom.apps.cappajv.features.catalog_detail.parts

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.FavoriteBorder
import androidx.compose.material.icons.rounded.Share
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import dev.marlonlom.apps.cappajv.R
import dev.marlonlom.apps.cappajv.core.database.entities.CatalogItem
import dev.marlonlom.apps.cappajv.ui.main.AppContentCallbacks
import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState

/**
* Catalog details buttons bar composable ui.
*
* @author marlonlom
*
* @param appState Application ui state.
* @param appContentCallbacks Application content callbacks.
* @param product Catalog detailed information
*/
@Composable
fun CatalogDetailButtonsBar(
appState: CappajvAppState,
appContentCallbacks: AppContentCallbacks,
product: CatalogItem,
) {
Row(
modifier = Modifier
.padding(top = 20.dp, bottom = 10.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
OutlinedButton(
onClick = { /*TODO*/ },
shape = MaterialTheme.shapes.small,
) {
Icon(imageVector = Icons.Rounded.FavoriteBorder, contentDescription = null)
Spacer(modifier = Modifier.width(10.dp))
Text(text = stringResource(id = R.string.text_catalog_detail_button_favorite))
}
val shareMessage = stringResource(
R.string.text_catalog_detail_sharing,
product.title,
)
val currentContext = LocalContext.current
OutlinedButton(
onClick = {
appContentCallbacks.onShareIconClicked(currentContext, shareMessage)
},
shape = MaterialTheme.shapes.small,
) {
Icon(imageVector = Icons.Rounded.Share, contentDescription = null)
Spacer(modifier = Modifier.width(10.dp))
Text(text = stringResource(id = R.string.text_catalog_detail_button_share))
}
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2024 Marlonlom
* SPDX-License-Identifier: Apache-2.0
*/

package dev.marlonlom.apps.cappajv.features.catalog_detail.parts

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
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.unit.DpSize
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.request.ImageRequest

/**
* Catalog favorite item card image composable.
*
* @author marlonlom
*
* @param title Title for content description.
* @param picture Image picture url.
* @param modifier Modifier for this composable.
*/
@Composable
fun CatalogDetailImage(
title: String,
picture: String,
modifier: Modifier = Modifier,
) {
val imageRequest = ImageRequest.Builder(LocalContext.current).data(picture).crossfade(true).build()

val imageModifier = modifier
.border(
width = 2.dp,
color = MaterialTheme.colorScheme.secondary,
shape = MaterialTheme.shapes.medium,
)
.clip(MaterialTheme.shapes.medium)
.size(DpSize(184.dp, 240.dp))
.background(Color.White)

AsyncImage(
model = imageRequest,
contentDescription = title,
contentScale = ContentScale.Crop,
modifier = imageModifier,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2024 Marlonlom
* SPDX-License-Identifier: Apache-2.0
*/

package dev.marlonlom.apps.cappajv.features.catalog_detail.parts

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.paddingFromBaseline
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import dev.marlonlom.apps.cappajv.core.database.entities.CatalogItem

@Composable
internal fun CatalogDetailProductDescriptionText(product: CatalogItem) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 10.dp),
textAlign = TextAlign.Center,
text = product.detail
)
}

@Composable
internal fun CatalogDetailProductCategoryText(product: CatalogItem) {
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
text = product.category,
style = MaterialTheme.typography.bodyMedium
)
}

@Composable
internal fun CatalogDetailProductTitle(product: CatalogItem) {
Text(
modifier = Modifier
.fillMaxWidth()
.paddingFromBaseline(60.dp, 10.dp),
textAlign = TextAlign.Center,
text = product.title,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.headlineLarge
)
}

Loading
Loading