Complete migration to Compose (#194)
* Migrate disclaimer screen to Compose * Migrate base URL screen to Compose * Migrate base URL screen to Compose * Migrate authentication screen to Compose * Initialize add recipe screen * Remove unused resources * Display add recipe operation result * Add delete icon to ingredients and instructions * Allow navigating between fields on add recipe * Allow navigating between fields on authentication screen * Allow to proceed from keyboard on base url screen * Use material icons for recipe item * Expose base URL as flow * Initialize Compose navigation * Allow sending logs again * Allow to override navigation and top bar per screen * Add additional logs * Migrate share recipe screen to Compose * Fix unit tests * Restore recipe list tests * Ensure authentication is shown after URL input * Add autofill to authentication * Complete first set up test * Use image vector from Icons instead of drawable * Add transition animations * Fix logging host in Host header * Do not fail test if login token is used
This commit is contained in:
@@ -59,8 +59,10 @@ import gq.kirmanak.mealient.shopping_list.R
|
||||
import gq.kirmanak.mealient.shopping_lists.ui.composables.getErrorMessage
|
||||
import gq.kirmanak.mealient.ui.AppTheme
|
||||
import gq.kirmanak.mealient.ui.Dimens
|
||||
import gq.kirmanak.mealient.ui.components.BaseScreen
|
||||
import gq.kirmanak.mealient.ui.components.LazyColumnWithLoadingState
|
||||
import gq.kirmanak.mealient.ui.preview.ColorSchemePreview
|
||||
import gq.kirmanak.mealient.ui.util.LoadingState
|
||||
import gq.kirmanak.mealient.ui.util.data
|
||||
import gq.kirmanak.mealient.ui.util.error
|
||||
import gq.kirmanak.mealient.ui.util.map
|
||||
@@ -71,7 +73,6 @@ data class ShoppingListNavArgs(
|
||||
val shoppingListId: String,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination(
|
||||
navArgsDelegate = ShoppingListNavArgs::class,
|
||||
)
|
||||
@@ -80,12 +81,50 @@ internal fun ShoppingListScreen(
|
||||
shoppingListViewModel: ShoppingListViewModel = hiltViewModel(),
|
||||
) {
|
||||
val loadingState by shoppingListViewModel.loadingState.collectAsState()
|
||||
|
||||
BaseScreen { modifier ->
|
||||
ShoppingListScreen(
|
||||
modifier = modifier,
|
||||
loadingState = loadingState,
|
||||
errorToShowInSnackbar = shoppingListViewModel.errorToShowInSnackbar,
|
||||
onSnackbarShown = shoppingListViewModel::onSnackbarShown,
|
||||
onRefreshRequest = shoppingListViewModel::refreshShoppingList,
|
||||
onAddItemClicked = shoppingListViewModel::onAddItemClicked,
|
||||
onEditCancel = shoppingListViewModel::onEditCancel,
|
||||
onEditConfirm = shoppingListViewModel::onEditConfirm,
|
||||
onItemCheckedChange = shoppingListViewModel::onItemCheckedChange,
|
||||
onDeleteItem = shoppingListViewModel::deleteShoppingListItem,
|
||||
onEditStart = shoppingListViewModel::onEditStart,
|
||||
onAddCancel = shoppingListViewModel::onAddCancel,
|
||||
onAddConfirm = shoppingListViewModel::onAddConfirm,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ShoppingListScreen(
|
||||
loadingState: LoadingState<ShoppingListScreenState>,
|
||||
errorToShowInSnackbar: Throwable?,
|
||||
onSnackbarShown: () -> Unit,
|
||||
onRefreshRequest: () -> Unit,
|
||||
onAddItemClicked: () -> Unit,
|
||||
onEditCancel: (ShoppingListItemState.ExistingItem) -> Unit,
|
||||
onEditConfirm: (ShoppingListItemState.ExistingItem, ShoppingListItemEditorState) -> Unit,
|
||||
onItemCheckedChange: (ShoppingListItemState.ExistingItem, Boolean) -> Unit,
|
||||
onDeleteItem: (ShoppingListItemState.ExistingItem) -> Unit,
|
||||
onEditStart: (ShoppingListItemState.ExistingItem) -> Unit,
|
||||
onAddCancel: (ShoppingListItemState.NewItem) -> Unit,
|
||||
onAddConfirm: (ShoppingListItemState.NewItem) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val defaultEmptyListError = stringResource(
|
||||
R.string.shopping_list_screen_empty_list,
|
||||
loadingState.data?.name.orEmpty()
|
||||
)
|
||||
|
||||
LazyColumnWithLoadingState(
|
||||
modifier = modifier,
|
||||
loadingState = loadingState.map { it.items },
|
||||
emptyListError = loadingState.error?.let { getErrorMessage(it) } ?: defaultEmptyListError,
|
||||
retryButtonText = stringResource(id = R.string.shopping_lists_screen_empty_button_refresh),
|
||||
@@ -96,11 +135,11 @@ internal fun ShoppingListScreen(
|
||||
bottom = Dimens.Large * 4,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(Dimens.Medium),
|
||||
snackbarText = shoppingListViewModel.errorToShowInSnackbar?.let { getErrorMessage(error = it) },
|
||||
onSnackbarShown = shoppingListViewModel::onSnackbarShown,
|
||||
onRefresh = shoppingListViewModel::refreshShoppingList,
|
||||
snackbarText = errorToShowInSnackbar?.let { getErrorMessage(error = it) },
|
||||
onSnackbarShown = onSnackbarShown,
|
||||
onRefresh = onRefreshRequest,
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = shoppingListViewModel::onAddItemClicked) {
|
||||
FloatingActionButton(onClick = onAddItemClicked) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = stringResource(id = R.string.shopping_list_screen_add_icon_content_description),
|
||||
@@ -122,31 +161,24 @@ internal fun ShoppingListScreen(
|
||||
}
|
||||
ShoppingListItemEditor(
|
||||
state = state,
|
||||
onEditCancelled = { shoppingListViewModel.onEditCancel(itemState) },
|
||||
onEditConfirmed = {
|
||||
shoppingListViewModel.onEditConfirm(
|
||||
itemState,
|
||||
state
|
||||
)
|
||||
}
|
||||
onEditCancelled = { onEditCancel(itemState) },
|
||||
onEditConfirmed = { onEditConfirm(itemState, state) },
|
||||
)
|
||||
} else {
|
||||
ShoppingListItem(
|
||||
itemState = itemState,
|
||||
showDivider = index == firstCheckedItemIndex && index != 0,
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
|
||||
onCheckedChange = {
|
||||
shoppingListViewModel.onItemCheckedChange(itemState, it)
|
||||
},
|
||||
onDismissed = { shoppingListViewModel.deleteShoppingListItem(itemState) },
|
||||
onEditStart = { shoppingListViewModel.onEditStart(itemState) },
|
||||
onCheckedChange = { onItemCheckedChange(itemState, it) },
|
||||
onDismissed = { onDeleteItem(itemState) },
|
||||
onEditStart = { onEditStart(itemState) },
|
||||
)
|
||||
}
|
||||
} else if (itemState is ShoppingListItemState.NewItem) {
|
||||
ShoppingListItemEditor(
|
||||
state = itemState.item,
|
||||
onEditCancelled = { shoppingListViewModel.onAddCancel(itemState) },
|
||||
onEditConfirmed = { shoppingListViewModel.onAddConfirm(itemState) }
|
||||
onEditCancelled = { onAddCancel(itemState) },
|
||||
onEditConfirmed = { onAddConfirm(itemState) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -493,7 +525,7 @@ fun ShoppingListItem(
|
||||
}
|
||||
true
|
||||
}
|
||||
)
|
||||
),
|
||||
) {
|
||||
val shoppingListItem = itemState.item
|
||||
SwipeToDismiss(
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package gq.kirmanak.mealient.shopping_lists.ui.list
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||
import com.ramcosta.composedestinations.rememberNavHostEngine
|
||||
import gq.kirmanak.mealient.shopping_lists.ui.NavGraphs
|
||||
|
||||
@Composable
|
||||
fun MealientApp() {
|
||||
val engine = rememberNavHostEngine()
|
||||
val controller = engine.rememberNavController()
|
||||
|
||||
DestinationsNavHost(
|
||||
navGraph = NavGraphs.root,
|
||||
engine = engine,
|
||||
navController = controller,
|
||||
startRoute = NavGraphs.root.startRoute,
|
||||
)
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package gq.kirmanak.mealient.shopping_lists.ui.list
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import gq.kirmanak.mealient.ui.ActivityUiStateController
|
||||
import gq.kirmanak.mealient.ui.AppTheme
|
||||
import gq.kirmanak.mealient.ui.CheckableMenuItem
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ShoppingListsFragment : Fragment() {
|
||||
|
||||
@Inject
|
||||
lateinit var activityUiStateController: ActivityUiStateController
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||
): View {
|
||||
return ComposeView(requireContext()).apply {
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
setContent {
|
||||
AppTheme {
|
||||
MealientApp()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
activityUiStateController.updateUiState {
|
||||
it.copy(
|
||||
navigationVisible = true,
|
||||
searchVisible = false,
|
||||
checkedMenuItem = CheckableMenuItem.ShoppingLists,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ShoppingCart
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
@@ -14,49 +16,55 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.ramcosta.composedestinations.navigation.navigate
|
||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListsSummaryResponse
|
||||
import gq.kirmanak.mealient.shopping_list.R
|
||||
import gq.kirmanak.mealient.shopping_lists.ui.composables.getErrorMessage
|
||||
import gq.kirmanak.mealient.shopping_lists.ui.destinations.ShoppingListScreenDestination
|
||||
import gq.kirmanak.mealient.ui.AppTheme
|
||||
import gq.kirmanak.mealient.ui.Dimens
|
||||
import gq.kirmanak.mealient.ui.components.BaseScreenState
|
||||
import gq.kirmanak.mealient.ui.components.BaseScreenWithNavigation
|
||||
import gq.kirmanak.mealient.ui.components.LazyColumnWithLoadingState
|
||||
import gq.kirmanak.mealient.ui.preview.ColorSchemePreview
|
||||
import gq.kirmanak.mealient.ui.util.error
|
||||
|
||||
@RootNavGraph(start = true)
|
||||
@Destination(start = true)
|
||||
@Destination
|
||||
@Composable
|
||||
fun ShoppingListsScreen(
|
||||
navigator: DestinationsNavigator,
|
||||
navController: NavController,
|
||||
baseScreenState: BaseScreenState,
|
||||
shoppingListsViewModel: ShoppingListsViewModel = hiltViewModel(),
|
||||
) {
|
||||
val loadingState by shoppingListsViewModel.loadingState.collectAsState()
|
||||
val errorToShowInSnackbar = shoppingListsViewModel.errorToShowInSnackBar
|
||||
|
||||
LazyColumnWithLoadingState(
|
||||
loadingState = loadingState,
|
||||
emptyListError = loadingState.error?.let { getErrorMessage(it) }
|
||||
?: stringResource(R.string.shopping_lists_screen_empty),
|
||||
retryButtonText = stringResource(id = R.string.shopping_lists_screen_empty_button_refresh),
|
||||
snackbarText = errorToShowInSnackbar?.let { getErrorMessage(error = it) },
|
||||
onSnackbarShown = shoppingListsViewModel::onSnackbarShown,
|
||||
onRefresh = shoppingListsViewModel::refresh
|
||||
) { items ->
|
||||
items(items) { shoppingList ->
|
||||
ShoppingListCard(
|
||||
shoppingList = shoppingList,
|
||||
onItemClick = { clickedEntity ->
|
||||
val shoppingListId = clickedEntity.id
|
||||
navigator.navigate(ShoppingListScreenDestination(shoppingListId))
|
||||
}
|
||||
)
|
||||
BaseScreenWithNavigation(
|
||||
baseScreenState = baseScreenState,
|
||||
) { modifier ->
|
||||
LazyColumnWithLoadingState(
|
||||
modifier = modifier,
|
||||
loadingState = loadingState,
|
||||
emptyListError = loadingState.error?.let { getErrorMessage(it) }
|
||||
?: stringResource(R.string.shopping_lists_screen_empty),
|
||||
retryButtonText = stringResource(id = R.string.shopping_lists_screen_empty_button_refresh),
|
||||
snackbarText = errorToShowInSnackbar?.let { getErrorMessage(error = it) },
|
||||
onSnackbarShown = shoppingListsViewModel::onSnackbarShown,
|
||||
onRefresh = shoppingListsViewModel::refresh
|
||||
) { items ->
|
||||
items(items) { shoppingList ->
|
||||
ShoppingListCard(
|
||||
shoppingList = shoppingList,
|
||||
onItemClick = { clickedEntity ->
|
||||
val shoppingListId = clickedEntity.id
|
||||
navController.navigate(ShoppingListScreenDestination(shoppingListId))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,7 +96,7 @@ private fun ShoppingListCard(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_shopping_cart),
|
||||
imageVector = Icons.Default.ShoppingCart,
|
||||
contentDescription = stringResource(id = R.string.shopping_lists_screen_cart_icon),
|
||||
modifier = Modifier.height(Dimens.Large),
|
||||
)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:tint="?attr/colorPrimary"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M14.35,43.95Q12.85,43.95 11.8,42.9Q10.75,41.85 10.75,40.35Q10.75,38.85 11.8,37.8Q12.85,36.75 14.35,36.75Q15.85,36.75 16.9,37.8Q17.95,38.85 17.95,40.35Q17.95,41.85 16.9,42.9Q15.85,43.95 14.35,43.95ZM34.35,43.95Q32.85,43.95 31.8,42.9Q30.75,41.85 30.75,40.35Q30.75,38.85 31.8,37.8Q32.85,36.75 34.35,36.75Q35.85,36.75 36.9,37.8Q37.95,38.85 37.95,40.35Q37.95,41.85 36.9,42.9Q35.85,43.95 34.35,43.95ZM11.75,10.95 L17.25,22.35H31.65Q31.65,22.35 31.65,22.35Q31.65,22.35 31.65,22.35L37.9,10.95Q37.9,10.95 37.9,10.95Q37.9,10.95 37.9,10.95ZM10.25,7.95H39.7Q40.85,7.95 41.45,9Q42.05,10.05 41.45,11.1L34.7,23.25Q34.15,24.2 33.275,24.775Q32.4,25.35 31.35,25.35H16.2L13.4,30.55Q13.4,30.55 13.4,30.55Q13.4,30.55 13.4,30.55H37.95V33.55H13.85Q11.75,33.55 10.825,32.15Q9.9,30.75 10.85,29L14.05,23.1L6.45,7H2.55V4H8.4ZM17.25,22.35H31.65Q31.65,22.35 31.65,22.35Q31.65,22.35 31.65,22.35Z" />
|
||||
</vector>
|
||||
Reference in New Issue
Block a user