I have a NavigationBar and each of the items have their own separate navGraphs. One of the NavGraphs has a following scheme:
sealed class ShopDestination(val name: String) {
object Shop : ShopDestination("shop") {
object Category: ShopDestination("${super.name}/category")
class Listing(listingId: Long? = null) : ShopDestination("${super.name}/listing/${listingId ?: ("{" + ShopArguments.ListingId.name + "}")}")
class Product(productId: Long? = null) : ShopDestination("${super.name}/${productId ?: ("{" + RouteArguments.ProductId.name + "}")}")
}
and here's the graph:
.shopNavGraph(navController: NavController) {
navigation(
route = ShopDestination.Shop.name,
startDestination = ShopDestination.Shop.Category.name
) {
composable(ShopDestination.Shop.Category.name) {
ShopScreen({ navController.navigate(ShopDestination.Shop.Listing(it).name) })
}
composable(
route = ShopDestination.Shop.Listing().name,
arguments = listOf(
navArgument(ShopArguments.ListingId.name) {
type = NavType.LongType
}
),
) {
ListingComposable(
contract = hiltViewModel<ListingViewModel>(),
goBack = { navController.popBackStack() },
goToProduct = { navController.navigate(ShopDestination.Shop.Product(it).name) },
)
}
composable(
route = ShopDestination.Shop.Product().name, arguments = listOf(navArgument(
RouteArguments.ProductId.name
) {
type = NavType.LongType
})
) {
ProductScreen(
{ navController.popBackStack() },
{ navController.navigate(ShopDestination.Shop.Product(it).name) })
}
}
When a user click on a different NavigationBarItem and then goes back the shopNavGraph starts from the startDestination. I would like the NavigationBar to save the backstack and once the user clicks back on it, start from where it left of (for example Product). How do I achieve that? Here's also the NavigationBar if it helps in any way:
NavigationBar(containerColor = MaterialTheme.colorScheme.primary) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route ?: ""
items.forEach {
NavigationBarItem(
colors = NavigationBarItemDefaults.colors(indicatorColor = Color.Transparent),
icon = {
Row(verticalAlignment = CenterVertically) {
Icon(
imageVector = it.icon,
contentDescription = it.name,
tint = MaterialTheme.colorScheme.onPrimary
)
Text(
text = stringResource(id = it.titleResId).uppercase(),
modifier = Modifier.padding(start = dimensionResource(id = R.dimen.margin_normal)),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelMedium
)
}
},
alwaysShowLabel = true,
selected = currentRoute.contains(it.name),
onClick = {
navController.navigate(it.name)
}
Related
I've tried to implement the SwipeToDismiss behavior in my app and it works perfectly when I try to delete the last or the only item in the LazyColumn. However, if I try to delete an item that is not the last one the next one will swipe to start but not entirely off the screen.
https://streamable.com/4v2i0d
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun TasksListLayout(
modifier: Modifier = Modifier,
onNavigate: (UiEvent.Navigate) -> Unit,
viewModel: TaskViewModel = hiltViewModel()
) {
val tasks = viewModel.tasks.collectAsState(initial = emptyList())
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = true) {
viewModel.uiEvent.collect { event ->
when (event) {
is UiEvent.ShowSnackbar -> {
val result = scope.launch {
snackbarHostState.showSnackbar(
message = event.message, actionLabel = event.action, duration = SnackbarDuration.Long
)
}
if (result.equals(SnackbarResult.ActionPerformed)) {
viewModel.onEvent(TaskListEvent.OnUndoDeleteTask)
}
}
is UiEvent.Navigate -> onNavigate(event)
else -> Unit
}
}
}
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
floatingActionButton = {
FloatingActionButton(
onClick = { viewModel.onEvent(TaskListEvent.OnAddTask) },
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.background
) {
Icon(imageVector = Icons.Default.Add, contentDescription = "Add a task")
}
},
topBar = {
TopAppBar(
title = { Text("Planner") }, colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = MaterialTheme.colorScheme.background
)
)
},
) { padding ->
LazyColumn(
modifier = modifier
.fillMaxSize()
.padding(padding)
.padding(horizontal = 12.dp),
verticalArrangement = spacedBy(12.dp),
contentPadding = PaddingValues(vertical = 12.dp)
) {
items(tasks.value) { task ->
val currentTask by rememberUpdatedState(newValue = task)
val dismissState = rememberDismissState(confirmValueChange = {
if(it == DismissValue.DismissedToStart) {
viewModel.onEvent(TaskListEvent.OnDeleteTask(currentTask))
}
true
})
SwipeToDismiss(state = dismissState, directions = setOf(DismissDirection.EndToStart),
background = { },
dismissContent = {
TaskCard(
task = task, onEvent = viewModel::onEvent, modifier = modifier
)
})
}
}
}
}
In order to fix the issue I needed to add a key to the LazyColumn.
Changed from this:
items(tasks.value) {}
To this:
items(items = tasks.value, key = { task -> task.hashCode()}) {}
I just started using Jetpack Compose and wanted to try out the BottomNavigation. A basic implementation with three items was no problem. Now, one of the three screens should navigate to a detail screen when clicking on a list item. The problem that occurs, is that on the detail screen the bottom navigation item is not selected anymore.
Here's my implementation:
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
WhoHasItTheme {
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigationBar(
items = listOf(
BottomNavItem(
name = "Home",
route = Screen.Home.route,
icon = Icons.Default.Home
),
BottomNavItem(
name = "Search",
route = Screen.GameListScreen.route,
icon = Icons.Default.Search
),
BottomNavItem(
name = "Profile",
route = Screen.Profile.route,
icon = Icons.Default.Person
)
),
navController = navController,
onItemClick = {
navController.navigate(it.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
) {
Box(modifier = Modifier.padding(it)) {
Navigation(navController = navController)
}
}
}
}
}
}
#Composable
fun Navigation(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = Screen.Home.route
) {
composable(Screen.Home.route) {
HomeScreen()
}
composable(Screen.GameListScreen.route) {
GameListScreen(navController)
}
composable(
route = "${Screen.GameDetailScreen.route}/{gameId}",
arguments = listOf(navArgument("gameId") { type = NavType.IntType })
) {
GameDetailScreen()
}
composable(Screen.Profile.route) {
ProfileScreen()
}
}
}
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun BottomNavigationBar(
items: List<BottomNavItem>,
navController: NavController,
modifier: Modifier = Modifier,
onItemClick: (BottomNavItem) -> Unit
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
BottomNavigation(
modifier
) {
items.forEach { item ->
BottomNavigationItem(
selected = currentDestination?.hierarchy?.any { it.route == item.route } == true,
onClick = { onItemClick(item) },
selectedContentColor = MaterialTheme.colors.onSurface,
label = { Text(item.name) },
alwaysShowLabel = false,
icon = {
Column(horizontalAlignment = CenterHorizontally) {
if (item.badgeCount > 0) {
BadgeBox(
badgeContent = {
Text(item.badgeCount.toString())
}
) {
Icon(imageVector = item.icon, contentDescription = item.name)
}
} else {
Icon(imageVector = item.icon, contentDescription = item.name)
}
}
}
)
}
}
}
In my understanding currentDestination?.hierarchy should also include the root screen (GameListScreen). What am I understanding wrong here and how can I make it so screens below the root screen of each bottom navigation item still have their tab item as 'selected'?
How can I arrange the two inner BottomNav Items so that they are not so close to the "+" FAB?
I tried surrounding the forEach which displays the Items with a Row and use the Arrangement modifier like so:
Row(horizontalArrangement = Arrangement.SpaceBetween) { //Not working :(
items.forEach { item ->
BottomNavigationItem(
icon = { Icon(painterResource(id = item.icon), contentDescription = item.title) },
label = { Text(text = item.title) },
selectedContentColor = Color.White,
unselectedContentColor = Color.White.copy(0.4f),
alwaysShowLabel = true,
selected = currentRoute == item.route,
onClick = {
navController.navigate(item.route) {
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
Unfortunately thats not working
Arrangement.SpaceBetween works as expected - it adds a spacer between items:
Place children such that they are spaced evenly across the main axis, without free space before the first child or after the last child. Visually: 1##2##3
You need to let your Row know about FAB location. You can add a spacer with Modifier.weight in the middle of your row, for example like this:
items.forEachIndexed { i, item ->
if (i == items.count() / 2) {
Spacer(Modifier.weight(1f))
}
BottomNavigationItem(
// ...
You can use BottomAppBar & give it cutoutShape with a dummy item in the middle. It would give you your desired results.
Output:
Code Sample:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
BottomBarWithFabDem()
}
}
}
}
}
val items = listOf(
Screen.PickUp,
Screen.Explore,
Screen.Camera,
Screen.Favorites,
Screen.Profile
)
sealed class Screen(val route: String?, val title: String?, val icon: ImageVector?) {
object PickUp : Screen("pickup", "PickUp", Icons.Default.ShoppingCart)
object Explore : Screen("explore", "Explore", Icons.Default.Info)
object Camera : Screen("camera", null, null)
object Favorites : Screen("favorites", "Fav", Icons.Default.Favorite)
object Profile : Screen("profile", "Profile", Icons.Default.Person)
}
#Composable
fun BottomBarWithFabDem() {
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNav(navController)
},
floatingActionButtonPosition = FabPosition.Center,
isFloatingActionButtonDocked = true,
floatingActionButton = {
FloatingActionButton(
shape = CircleShape,
onClick = {
Screen.Camera.route?.let {
navController.navigate(it) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
Screen.Camera.route?.let { navController.navigate(it) }
},
contentColor = Color.White
) {
Icon(imageVector = Icons.Filled.Add, contentDescription = "Add icon")
}
}
) {
MainScreenNavigation(navController)
}
}
#Composable
fun MainScreenNavigation(navController: NavHostController) {
NavHost(navController, startDestination = Screen.Profile.route!!) {
composable(Screen.Profile.route) {}
composable(Screen.Explore.route!!) {}
composable(Screen.Favorites.route!!) {}
composable(Screen.PickUp.route!!) {}
composable(Screen.Camera.route!!) {}
}
}
#Composable
fun BottomNav(navController: NavController) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination
BottomAppBar(cutoutShape = CircleShape, modifier = Modifier.height(64.dp)) {
Row {
items.forEachIndexed { index, it ->
if (index != 2) {
// Main item
BottomNavigationItem(
icon = {
it.icon?.let {
Icon(
imageVector = it,
contentDescription = "",
modifier = Modifier.size(35.dp),
tint = Color.White
)
}
},
label = {
it.title?.let {
Text(
text = it,
color = Color.White
)
}
},
selected = currentRoute?.hierarchy?.any { it.route == it.route } == true,
selectedContentColor = Color(R.color.purple_700),
unselectedContentColor = Color.White.copy(alpha = 0.4f),
onClick = {}
)
} else {
// placeholder for center fab
BottomNavigationItem(
icon = {},
label = { },
selected = false,
onClick = { },
enabled = false
)
}
}
}
}
}
I'm building an application jetpack compose , after fetch some data from online source , i want to pass an id to as extras to the next screen so that i can call the next request api , but i'm facing two issues , the first issue is that showing me an error that composables can only be invoked from a composable context and the second issue is that i'm not sure wether i'm writing the correct code for calling the next screen , i appreciate any help , Thank you .
This is my code
val lazyPopularMoviesItems = movies.collectAsLazyPagingItems()
LazyVerticalGrid(cells = GridCells.Fixed(2)) {
items(lazyPopularMoviesItems.itemCount) { index ->
lazyPopularMoviesItems[index]?.let {
Card(elevation = 8.dp, modifier = Modifier
.height(200.dp)
.padding(10.dp)
.clickable {
// This is the function i want to call and pass extras with it
DetailsScreen(movieViewModel = movieViewModel, movieId = it.id)
}
.clip(RoundedCornerShape(8.dp))) {
Column {
Image(
painter = rememberImagePainter("http://image.tmdb.org/t/p/w500/" + it.backdrop_path),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.height(150.dp)
)
Text(
modifier = Modifier
.height(50.dp)
.padding(3.dp)
.fillMaxWidth(),
text = it.title,
fontSize = 15.sp,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
textAlign = TextAlign.Center,
color = androidx.compose.ui.graphics.Color.Black
)
}
}
}
}
}
MainActivity Code
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val movieViewModel : MovieViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
Scaffold(
backgroundColor = Color.Blue.copy(0.1f),
topBar = { TopAppBar(title = {Text(text = "Movie Flex")}, backgroundColor = Color.White, elevation = 10.dp)},
bottomBar = {
val items = listOf(
BarItems.Popular,
BarItems.Playing,
BarItems.Top,
BarItems.Upcoming
)
BottomNavigation(backgroundColor = Color.Gray) {
items.forEach { item ->
BottomNavigationItem(
icon = { Icon(painter = painterResource(id = item.icon), contentDescription = item.title)},
label = { Text(text = item.title)},
selectedContentColor = Color.White,
alwaysShowLabel = true,
selected = false,
unselectedContentColor = Color.White.copy(0.5f),
onClick = {
navController.navigate(item.route){
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
})
}
}
},
content = {
ScreenNavigation(navController,movieViewModel)
},
)
}
}
}
#OptIn(ExperimentalCoilApi::class)
#Composable
fun ScreenNavigation(navController: NavHostController,movieViewModel: MovieViewModel){
NavHost(navController = navController, startDestination = BarItems.Popular.route){
composable(route = BarItems.Popular.route){
PopularScreen(movies = movieViewModel.getPopular(), movieViewModel = movieViewModel)
}
composable(route = BarItems.Playing.route){
PlayingScreen(movies = movieViewModel.getPlaying())
}
composable(route = BarItems.Top.route){
TopRatedScreen(movies = movieViewModel.getTopRated())
}
composable(route = BarItems.Upcoming.route){
UpcomingScreen(movies = movieViewModel.getUpcoming())
}
}
}
Navigation Routing
sealed class BarItems(var route : String , var icon : Int , var title : String) {
object Popular : BarItems("popular", R.drawable.ic_baseline_remove_red_eye_24,"Popular")
object Playing : BarItems("playing",R.drawable.ic_baseline_remove_red_eye_24,"Playing")
object Top : BarItems("top",R.drawable.ic_baseline_remove_red_eye_24,"Top")
object Upcoming : BarItems("upcoming",R.drawable.ic_baseline_remove_red_eye_24,"Upcoming")
}
As you've pointed out, we'll need two things:
Handle the navigation. You can use navigation-compose. Have a look at the documentation
Trigger the navigation with either a LaunchedEffect or by launching a coroutine.
I was trying to implement jetpack compose bottomnavbar. But I encountered this problem. Whenever label don't get enough space it's overlapping the icon. Am I missing something? Is there any solution like truncating or shrinking text automatically?
compose_version = '1.0.0-beta09'
My Code
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
val items = listOf(
Screen.Profile,
Screen.FriendsList,
Screen.Notification,
Screen.Setting
)
Scaffold(
bottomBar = {
BottomNavigation(
backgroundColor = MaterialTheme.colors.surface,
elevation = 8.dp
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
items.forEachIndexed { index, screen ->
BottomNavigationItem(
icon = {
when (index) {
0 -> Icon(Icons.Filled.Person, "Profile")
1 -> Icon(
painterResource(id = R.drawable.ic_friends),
"Friends"
)
2 -> Icon(Icons.Filled.Notifications, "Notification")
else -> Icon(Icons.Filled.Settings, "Settings")
}
},
label = { Text(stringResource(screen.resourceId)) },
selected = currentRoute == screen.route,
selectedContentColor = MaterialTheme.colors.primary,
unselectedContentColor = Color.Black,
onClick = {
navController.navigate(screen.route) {
navController.graph.startDestinationRoute?.let {
popUpTo(it) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
) {
NavHost(navController, startDestination = Screen.Profile.route) {
composable(Screen.Profile.route) { Profile(navController) }
composable(Screen.FriendsList.route) { FriendsList(navController) }
composable(Screen.Notification.route) { FriendsList(navController) }
composable(Screen.Setting.route) { FriendsList(navController) }
}
}
}
}
You can add the property maxLines = 1 to the Text used in the label:
BottomNavigationItem(
/* your code */
label = { Text("Notification",
maxLines = 1,
overflow = TextOverflow.Ellipsis) /* optional */
}
)