From 60054e51e8a13b4f3048cfb64f7379967c8cf0d5 Mon Sep 17 00:00:00 2001 From: mohitb117 Date: Thu, 5 Feb 2026 07:44:17 -0600 Subject: [PATCH] add alternate navigation --- app/build.gradle | 10 +- .../activities/AppDestinations.kt | 13 ++ .../activities/LaunchingActivity.kt | 203 ++++++++++++------ .../demo_omdb_api/ui/common/CommonUi.kt | 136 ++++++++++-- .../ui/favourites/FavouritesComposable.kt | 3 + .../demo_omdb_api/ui/feed/FeedComposables.kt | 81 +++++-- .../ui/search/SearchComposable.kt | 3 + build.gradle | 1 - 8 files changed, 338 insertions(+), 112 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 770a00d..2c9708e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,6 +5,7 @@ plugins { id "org.jetbrains.kotlin.plugin.compose" version "$kotlin_version" id "dagger.hilt.android.plugin" id "kotlin-parcelize" + id 'org.jetbrains.kotlin.plugin.serialization' version '2.0.21' } android { @@ -70,7 +71,6 @@ dependencies { implementation 'androidx.compose.material3.adaptive:adaptive-layout' implementation 'androidx.compose.material3.adaptive:adaptive-navigation' - // Jetpack Compose BOM implementation platform("androidx.compose:compose-bom:$compose_bom_version") implementation "androidx.compose.ui:ui" @@ -95,6 +95,14 @@ dependencies { implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" implementation "androidx.navigation:navigation-compose:$nav_version" + implementation("androidx.navigation:navigation-runtime-ktx:$nav_version") { + version { + // 'strictly' forces this version. No other version is allowed. + strictly nav_version + } + because("blah!") + } + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3" // Dagger Hilt implementation "com.google.dagger:hilt-android:$hilt_version" diff --git a/app/src/main/java/com/mohitb117/demo_omdb_api/activities/AppDestinations.kt b/app/src/main/java/com/mohitb117/demo_omdb_api/activities/AppDestinations.kt index b29e0f0..369d00c 100644 --- a/app/src/main/java/com/mohitb117/demo_omdb_api/activities/AppDestinations.kt +++ b/app/src/main/java/com/mohitb117/demo_omdb_api/activities/AppDestinations.kt @@ -7,7 +7,9 @@ import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Home import androidx.compose.ui.graphics.vector.ImageVector import com.mohitb117.demo_omdb_api.R +import kotlinx.serialization.Serializable +@Serializable enum class AppDestinations( @param:StringRes val label: Int, val icon: ImageVector, @@ -16,4 +18,15 @@ enum class AppDestinations( HOME(R.string.home, Icons.Default.Home, R.string.home), FAVORITES(R.string.favorites, Icons.Default.Favorite, R.string.favorites), Feed(R.string.feed, Icons.Default.DynamicFeed, R.string.feed) +} + +@Serializable +sealed class ItemDetailDestination { + @Serializable + data object Home: ItemDetailDestination() + + @Serializable + data class Details( + val name: String + ): ItemDetailDestination() } \ No newline at end of file diff --git a/app/src/main/java/com/mohitb117/demo_omdb_api/activities/LaunchingActivity.kt b/app/src/main/java/com/mohitb117/demo_omdb_api/activities/LaunchingActivity.kt index a868493..6603747 100644 --- a/app/src/main/java/com/mohitb117/demo_omdb_api/activities/LaunchingActivity.kt +++ b/app/src/main/java/com/mohitb117/demo_omdb_api/activities/LaunchingActivity.kt @@ -24,7 +24,6 @@ import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffo import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -35,6 +34,10 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.compose.rememberNavController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.toRoute import com.mohitb117.demo_omdb_api.activities.ui.theme.DEMO_OMDB_APITheme import com.mohitb117.demo_omdb_api.activities.ui.theme.Purple80 import com.mohitb117.demo_omdb_api.datamodels.SearchResult @@ -99,77 +102,37 @@ class LaunchingActivity : ComponentActivity() { onToggleMovieFavorited: suspend (SearchResult) -> Boolean = { false }, ) { var currentDestination by rememberSaveable { mutableStateOf(AppDestinations.HOME) } + val navController = rememberNavController() + val onItemTapped: (ItemDetailDestination.Details) -> Unit = { navController.navigate(it) } + val onDestinationChanged: (AppDestinations) -> Unit = { currentDestination = it } - NavigationSuiteScaffold( - navigationSuiteItems = { - AppDestinations.entries.forEach { - item( - icon = { - Icon(it.icon, stringResource(it.contentDescription)) - }, - modifier = Modifier, - label = { Text(stringResource(it.label)) }, - selected = it == currentDestination, - onClick = { currentDestination = it }, - ) - } - }) { - val searchResultError = (searchUiState as? Result.Failure)?.error - - Scaffold( - modifier = Modifier.fillMaxSize(), - topBar = { - Text( - modifier = Modifier - .fillMaxWidth() - .windowInsetsPadding(WindowInsets.statusBars) - .padding(horizontal = 16.dp, vertical = 8.dp), - text = searchResultError?.localizedMessage ?: stringResource(currentDestination.label), - color = if (searchResultError != null) Color.Red else Color.Unspecified, - ) - }, - bottomBar = { - if (currentDestination == AppDestinations.HOME) { - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .background(color = Purple80), - value = searchQuery, - onValueChange = { onSearchQueryChanged(it) }, - label = { Text(text = "Search Movie!") }) - } - } - ) { innerPadding -> - when (currentDestination) { - AppDestinations.HOME -> { - SearchMovieInfoLayout( - modifier = Modifier.padding(innerPadding), - searchQuery = searchQuery, - searchUiState = searchUiState, - isRefreshing = isRefreshing, - onRefresh = onRefresh, - isMovieFavourited = isMovieFavourited, - onToggleMovieFavorited = onToggleMovieFavorited, - ) - } + NavHost(navController, startDestination = ItemDetailDestination.Home) { + composable { + HomeComposable( + currentDestination = currentDestination, + searchUiState = searchUiState, + searchQuery = searchQuery, + onSearchQueryChanged = onSearchQueryChanged, + isRefreshing = isRefreshing, + onRefresh = onRefresh, + isMovieFavourited = isMovieFavourited, + onToggleMovieFavorited = onToggleMovieFavorited, + onItemTapped = onItemTapped, + favouritesUiState = favouritesUiState, + onDestinationChanged = onDestinationChanged, + ) + } - AppDestinations.Feed -> { - FeedComposableLayout( - modifier = Modifier.padding(innerPadding), - uiState = favouritesUiState, - isMovieFavourited = isMovieFavourited, - onToggleMovieFavorited = onToggleMovieFavorited, - ) - } + composable { backStackEntry -> + val movie: ItemDetailDestination.Details = backStackEntry.toRoute() - AppDestinations.FAVORITES -> { - FavouritesComposableLayout( - modifier = Modifier.padding(innerPadding), - uiState = favouritesUiState, - isMovieFavourited = isMovieFavourited, - onToggleMovieFavorited = onToggleMovieFavorited, - ) - } + val windowInset = WindowInsets.statusBars + Column( + modifier = Modifier + .fillMaxSize() + .windowInsetsPadding(windowInset), + ) { + Text("You tapped on ${movie.name}") } } } @@ -184,6 +147,7 @@ class LaunchingActivity : ComponentActivity() { onRefresh: () -> Unit = {}, isMovieFavourited: (SearchResult) -> Boolean = { false }, onToggleMovieFavorited: suspend (SearchResult) -> Boolean = { false }, + onItemTapped: (ItemDetailDestination.Details) -> Unit = {} ) { Column( modifier = modifier.fillMaxSize(), @@ -207,6 +171,7 @@ class LaunchingActivity : ComponentActivity() { isRefreshing = isRefreshing, onToggleMovieFavorited = onToggleMovieFavorited, isMovieFavourited = isMovieFavourited, + onItemTapped = onItemTapped, ) } else { @@ -221,6 +186,7 @@ class LaunchingActivity : ComponentActivity() { modifier: Modifier = Modifier, isMovieFavourited: (SearchResult) -> Boolean = { false }, onToggleMovieFavorited: suspend (SearchResult) -> Boolean = { false }, + onItemTapped: (ItemDetailDestination.Details) -> Unit = {} ) { Column( modifier = modifier.fillMaxSize(), @@ -233,6 +199,7 @@ class LaunchingActivity : ComponentActivity() { uiState = uiState, onToggleMovieFavorited = onToggleMovieFavorited, isMovieFavourited = isMovieFavourited, + onItemTapped = onItemTapped, ) } } @@ -243,6 +210,7 @@ class LaunchingActivity : ComponentActivity() { modifier: Modifier = Modifier, isMovieFavourited: (SearchResult) -> Boolean = { false }, onToggleMovieFavorited: suspend (SearchResult) -> Boolean = { false }, + onItemTapped: (ItemDetailDestination.Details) -> Unit = {} ) { Column( modifier = modifier.fillMaxSize(), @@ -255,6 +223,7 @@ class LaunchingActivity : ComponentActivity() { uiState = uiState, onToggleMovieFavorited = onToggleMovieFavorited, isMovieFavourited = isMovieFavourited, + onItemTapped = onItemTapped, ) } } @@ -281,4 +250,98 @@ class LaunchingActivity : ComponentActivity() { ) } } -} + + @Composable + private fun HomeComposable( + currentDestination: AppDestinations, + searchUiState: Result, + searchQuery: String, + onSearchQueryChanged: (String) -> Unit, + isRefreshing: Boolean, + onRefresh: () -> Unit, + isMovieFavourited: (SearchResult) -> Boolean, + onToggleMovieFavorited: suspend (SearchResult) -> Boolean, + onItemTapped: (ItemDetailDestination.Details) -> Unit, + favouritesUiState: Set, + onDestinationChanged: (AppDestinations) -> Unit + ) { + NavigationSuiteScaffold( + navigationSuiteItems = { + AppDestinations.entries.forEach { + item( + icon = { + Icon(it.icon, stringResource(it.contentDescription)) + }, + modifier = Modifier, + label = { Text(stringResource(it.label)) }, + selected = it == currentDestination, + onClick = { onDestinationChanged(it) }, + ) + } + }) { + val searchResultError = (searchUiState as? Result.Failure)?.error + + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + Text( + modifier = Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets.statusBars) + .padding(horizontal = 16.dp, vertical = 8.dp), + text = searchResultError?.localizedMessage ?: stringResource( + currentDestination.label + ), + color = if (searchResultError != null) Color.Red else Color.Unspecified, + ) + }, + bottomBar = { + if (currentDestination == AppDestinations.HOME) { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .background(color = Purple80), + value = searchQuery, + onValueChange = { onSearchQueryChanged(it) }, + label = { Text(text = "Search Movie!") }) + } + } + ) { innerPadding -> + when (currentDestination) { + AppDestinations.HOME -> { + SearchMovieInfoLayout( + modifier = Modifier.padding(innerPadding), + searchQuery = searchQuery, + searchUiState = searchUiState, + isRefreshing = isRefreshing, + onRefresh = onRefresh, + isMovieFavourited = isMovieFavourited, + onToggleMovieFavorited = onToggleMovieFavorited, + onItemTapped = onItemTapped, + ) + } + + AppDestinations.Feed -> { + FeedComposableLayout( + modifier = Modifier.padding(innerPadding), + uiState = favouritesUiState, + isMovieFavourited = isMovieFavourited, + onToggleMovieFavorited = onToggleMovieFavorited, + onItemTapped = onItemTapped, + ) + } + + AppDestinations.FAVORITES -> { + FavouritesComposableLayout( + modifier = Modifier.padding(innerPadding), + uiState = favouritesUiState, + isMovieFavourited = isMovieFavourited, + onToggleMovieFavorited = onToggleMovieFavorited, + onItemTapped = onItemTapped, + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mohitb117/demo_omdb_api/ui/common/CommonUi.kt b/app/src/main/java/com/mohitb117/demo_omdb_api/ui/common/CommonUi.kt index b3cd11e..68976ad 100644 --- a/app/src/main/java/com/mohitb117/demo_omdb_api/ui/common/CommonUi.kt +++ b/app/src/main/java/com/mohitb117/demo_omdb_api/ui/common/CommonUi.kt @@ -3,10 +3,10 @@ package com.mohitb117.demo_omdb_api.ui.common import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -14,17 +14,24 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -35,6 +42,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.mohitb117.demo_omdb_api.R +import com.mohitb117.demo_omdb_api.activities.ItemDetailDestination import com.mohitb117.demo_omdb_api.activities.ui.theme.DEMO_OMDB_APITheme import com.mohitb117.demo_omdb_api.activities.ui.theme.Purple40 import com.mohitb117.demo_omdb_api.datamodels.SearchResult @@ -42,6 +50,10 @@ import com.mohitb117.demo_omdb_api.datamodels.SearchResultsBody import com.mohitb117.demo_omdb_api.ui.favourites.FavouritesComposableContent import kotlinx.coroutines.launch +enum class ScrollingDirection { + UP, DOWN +} + @ExperimentalMaterial3Api @Composable fun MovieItemListComposable( @@ -49,23 +61,97 @@ fun MovieItemListComposable( searchResults: SearchResultsBody, isMovieFavourited: (SearchResult) -> Boolean = { false }, onToggleMovieFavorited: suspend (SearchResult) -> Boolean = { false }, + onItemTapped: (ItemDetailDestination.Details) -> Unit = {} ) { + val listState = rememberLazyListState() val searchItems = searchResults.Search.orEmpty() + val snackbarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() - if (searchItems.isEmpty()) { - Text(modifier = Modifier.fillMaxWidth(), text = "No results found") - } else { - LazyColumn( - modifier = Modifier.fillMaxSize(), - ) { - items( - items = searchItems, - ) { item -> - ListItem( - item = item, - isMovieFavourited = isMovieFavourited, - onToggleMovieFavorited = onToggleMovieFavorited, - ) + var fabNavigationState by rememberSaveable { + mutableStateOf(ScrollingDirection.DOWN) + } + + val isEnd by remember { + derivedStateOf { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == listState.layoutInfo.totalItemsCount - 1 } + } + + val isBegin by remember { + derivedStateOf { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.index == 0 } + } + + LaunchedEffect(isBegin) { + if (isBegin) { + snackbarHostState.showSnackbar("Top!") + } + } + + LaunchedEffect(isEnd) { + if (isEnd) { + snackbarHostState.showSnackbar("end of the list!") + } + } + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + floatingActionButton = { + ExtendedFloatingActionButton( + modifier = Modifier.wrapContentSize(), + onClick = { + // show snackbar as a suspend function + val previousValue = fabNavigationState + + val index = when (previousValue) { + ScrollingDirection.UP -> 0 + ScrollingDirection.DOWN -> searchItems.lastIndex + } + + coroutineScope.launch { + listState.scrollToItem(index) + } + + val newValue = when (previousValue) { + ScrollingDirection.UP -> ScrollingDirection.DOWN + ScrollingDirection.DOWN -> ScrollingDirection.UP + } + + fabNavigationState = newValue + } + ) { + val text = when { + isBegin -> ScrollingDirection.DOWN.name + isEnd -> ScrollingDirection.UP.name + else -> fabNavigationState.name + } + + Text(text) + } + } + ) { paddingValues -> + if (searchItems.isEmpty()) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(paddingValues), + text = "No results found" + ) + } else { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + ) { + items( + items = searchItems, + ) { item -> + ListItem( + item = item, + isMovieFavourited = isMovieFavourited, + onToggleMovieFavorited = onToggleMovieFavorited, + onItemTapped = onItemTapped, + ) + } } } } @@ -77,6 +163,7 @@ fun ListItem( item: SearchResult, isMovieFavourited: (SearchResult) -> Boolean = { false }, onToggleMovieFavorited: suspend (SearchResult) -> Boolean = { false }, + onItemTapped: (ItemDetailDestination.Details) -> Unit = {}, ) { val coroutineScope = rememberCoroutineScope() @@ -90,7 +177,10 @@ fun ListItem( .background( color = Purple40, shape = roundedCornerShape - ), + ) + .clickable { + onItemTapped(ItemDetailDestination.Details(item.title)) + }, verticalAlignment = Alignment.CenterVertically, ) { var isFavourited by rememberSaveable { mutableStateOf(false) } @@ -108,10 +198,10 @@ fun ListItem( .size(150.dp) .padding(5.dp) .border( - border = BorderStroke(borderWidth/8, Color.Gray), + border = BorderStroke(borderWidth / 8, Color.Gray), shape = RoundedCornerShape(borderWidth) ) - .padding(borderWidth/8) + .padding(borderWidth / 8) .clip(roundedCornerShape), contentDescription = "Poster", contentScale = ContentScale.Crop, @@ -133,7 +223,9 @@ fun ListItem( horizontalAlignment = Alignment.End, ) { Switch( - modifier = Modifier.wrapContentSize().padding(end = 5.dp), + modifier = Modifier + .wrapContentSize() + .padding(end = 5.dp), checked = isFavourited, onCheckedChange = { coroutineScope.launch { diff --git a/app/src/main/java/com/mohitb117/demo_omdb_api/ui/favourites/FavouritesComposable.kt b/app/src/main/java/com/mohitb117/demo_omdb_api/ui/favourites/FavouritesComposable.kt index 6adaec2..79618c0 100644 --- a/app/src/main/java/com/mohitb117/demo_omdb_api/ui/favourites/FavouritesComposable.kt +++ b/app/src/main/java/com/mohitb117/demo_omdb_api/ui/favourites/FavouritesComposable.kt @@ -3,6 +3,7 @@ package com.mohitb117.demo_omdb_api.ui.favourites import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.mohitb117.demo_omdb_api.activities.ItemDetailDestination import com.mohitb117.demo_omdb_api.datamodels.SearchResult import com.mohitb117.demo_omdb_api.datamodels.SearchResultsBody import com.mohitb117.demo_omdb_api.ui.common.MovieItemListComposable @@ -14,6 +15,7 @@ fun FavouritesComposableContent( uiState: Set, isMovieFavourited: (SearchResult) -> Boolean = { false }, onToggleMovieFavorited: suspend (SearchResult) -> Boolean = { false }, + onItemTapped: (ItemDetailDestination.Details) -> Unit = {}, ) { MovieItemListComposable( modifier = modifier, @@ -24,5 +26,6 @@ fun FavouritesComposableContent( ), isMovieFavourited = isMovieFavourited, onToggleMovieFavorited = onToggleMovieFavorited, + onItemTapped = onItemTapped, ) } diff --git a/app/src/main/java/com/mohitb117/demo_omdb_api/ui/feed/FeedComposables.kt b/app/src/main/java/com/mohitb117/demo_omdb_api/ui/feed/FeedComposables.kt index 8674948..0602ec8 100644 --- a/app/src/main/java/com/mohitb117/demo_omdb_api/ui/feed/FeedComposables.kt +++ b/app/src/main/java/com/mohitb117/demo_omdb_api/ui/feed/FeedComposables.kt @@ -5,7 +5,7 @@ package com.mohitb117.demo_omdb_api.ui.feed import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.gestures.TargetedFlingBehavior +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -16,16 +16,20 @@ import androidx.compose.foundation.pager.VerticalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -36,6 +40,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.mohitb117.demo_omdb_api.R +import com.mohitb117.demo_omdb_api.activities.ItemDetailDestination import com.mohitb117.demo_omdb_api.activities.ui.theme.DEMO_OMDB_APITheme import com.mohitb117.demo_omdb_api.activities.ui.theme.Purple40 import com.mohitb117.demo_omdb_api.datamodels.SearchResult @@ -48,6 +53,7 @@ fun FeedComposableContent( uiState: Set, isMovieFavourited: (SearchResult) -> Boolean = { false }, onToggleMovieFavorited: suspend (SearchResult) -> Boolean = { false }, + onItemTapped: (ItemDetailDestination.Details) -> Unit = {} ) { FeedListComposable( modifier = modifier, @@ -58,6 +64,7 @@ fun FeedComposableContent( ), isMovieFavourited = isMovieFavourited, onToggleMovieFavorited = onToggleMovieFavorited, + onItemTapped = onItemTapped, ) } @@ -68,37 +75,75 @@ fun FeedListComposable( searchResults: SearchResultsBody, isMovieFavourited: (SearchResult) -> Boolean = { false }, onToggleMovieFavorited: suspend (SearchResult) -> Boolean = { false }, + onItemTapped: (ItemDetailDestination.Details) -> Unit = {} ) { val searchItems = searchResults.Search.orEmpty() val pagerState = rememberPagerState(pageCount = { searchItems.size }) + val snackbarHostState = remember { SnackbarHostState() } - if (searchItems.isEmpty()) { - Text(modifier = Modifier.fillMaxWidth(), text = "No results found") - } else { - VerticalPager( - modifier = Modifier.fillMaxSize(), - state = pagerState, - beyondViewportPageCount = 1, - contentPadding = PaddingValues(bottom = 150.dp) - ) { - val item = searchItems[it] - FeedListItem( - item = item, - isMovieFavourited = isMovieFavourited, - onToggleMovieFavorited = onToggleMovieFavorited, + val isTop by remember { + derivedStateOf { + pagerState.currentPage == 0 && searchItems.size > 1 + } + } + + val isEnd by remember { + derivedStateOf { + pagerState.currentPage == searchItems.size - 1 && searchItems.size > 1 + } + } + + LaunchedEffect(isTop) { + if (isTop) { + snackbarHostState.showSnackbar("You've reached the top of the list!") + } + } + LaunchedEffect(isEnd) { + if (isEnd) { + snackbarHostState.showSnackbar("You've reached the end of the list!") + } + } + + Scaffold( + modifier = modifier, + snackbarHost = { SnackbarHost(snackbarHostState) } + ) { paddingValues -> + if (searchItems.isEmpty()) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(paddingValues), + text = "No results found" ) + } else { + VerticalPager( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + state = pagerState, + beyondViewportPageCount = 1, + contentPadding = PaddingValues(bottom = 150.dp) + ) { page -> + val item = searchItems[page] + FeedListItem( + item = item, + isMovieFavourited = isMovieFavourited, + onToggleMovieFavorited = onToggleMovieFavorited, + onItemTapped= onItemTapped, + ) + } } } } - @Composable fun FeedListItem( modifier: Modifier = Modifier, item: SearchResult, isMovieFavourited: (SearchResult) -> Boolean = { false }, onToggleMovieFavorited: suspend (SearchResult) -> Boolean = { false }, + onItemTapped: (ItemDetailDestination.Details) -> Unit = {} ) { val coroutineScope = rememberCoroutineScope() @@ -112,7 +157,7 @@ fun FeedListItem( .background( color = Purple40, shape = roundedCornerShape - ), + ).clickable { onItemTapped(ItemDetailDestination.Details(item.title)) }, ) { var isFavourited by rememberSaveable { mutableStateOf(false) } diff --git a/app/src/main/java/com/mohitb117/demo_omdb_api/ui/search/SearchComposable.kt b/app/src/main/java/com/mohitb117/demo_omdb_api/ui/search/SearchComposable.kt index da97fbd..d4dc7b7 100644 --- a/app/src/main/java/com/mohitb117/demo_omdb_api/ui/search/SearchComposable.kt +++ b/app/src/main/java/com/mohitb117/demo_omdb_api/ui/search/SearchComposable.kt @@ -12,6 +12,7 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import com.mohitb117.demo_omdb_api.activities.ItemDetailDestination import com.mohitb117.demo_omdb_api.datamodels.SearchResult import com.mohitb117.demo_omdb_api.datamodels.SearchResultsBody import com.mohitb117.demo_omdb_api.ui.common.MovieItemListComposable @@ -25,6 +26,7 @@ fun SearchComposableContent( onRefresh: () -> Unit = {}, isMovieFavourited: (SearchResult) -> Boolean = { false }, onToggleMovieFavorited: suspend (SearchResult) -> Boolean = { false }, + onItemTapped: (ItemDetailDestination.Details) -> Unit = {} ) { val scrollState = rememberScrollState() @@ -41,6 +43,7 @@ fun SearchComposableContent( searchResults = searchUiState.value, isMovieFavourited = isMovieFavourited, onToggleMovieFavorited = onToggleMovieFavorited, + onItemTapped = onItemTapped, ) } diff --git a/build.gradle b/build.gradle index 7fa2440..29ee89a 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,6 @@ buildscript { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$ksp_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" - classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" } }