I'm making an android app using Jetpack Compose and I have a problem. I used navigation to change the screen. I wanted to change firstRunLayout to loginLayout. You can see that i put that log.d code in order to check if this worked and I found out that logcat prints that successfully. I want to change the screen when I navigate. Is there something wrong with my code?
#Composable
fun setUpNavHost(navController: NavHostController) {
NavHost(navController = navController, startDestination = Screen.FirstRun.route) {
composable(route = Screen.FirstRun.route) {
firstRunLayout(navController)
Log.d("NavHost", "FirstRun")
}
composable(route = Screen.Login.route) {
loginLayout()
Log.d("NavHost", "Login")
}
}
}
p.s. I checked that there are Composable annotations at those layouts.
Button(onClick = {
Log.d("FirstRunActivity", "Button clicked")
navController.navigate(route = Screen.Login.route)
}, colors = ButtonDefaults.buttonColors(backgroundColor = Color(0xFFEF4746)), modifier = Modifier
.height(40.dp)
.width(300.dp), shape = RoundedCornerShape(50)) {
Text(text = "Continue", fontSize = 15.sp, color = Color.White)
}
I used the button to navigate. You can see navController.navigate above. Also, this is the way that this navController was declared.
class FirstRunActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MemorizableTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = Color(0xFFEF4746)
) {
val navController = rememberNavController()
setUpNavHost(navController = navController)
firstRunLayout(navController)
}
}
}
}
}
The problem is in your Activity, you call the navHost, then you call the firstRunLayout screen, so the firstRunLayout screen will be on the top of the navHost, you need to delete the call of firstRunLayout in your activity, so your code should look like this:
class FirstRunActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MemorizableTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = Color(0xFFEF4746)
) {
val navController = rememberNavController()
setUpNavHost(navController = navController)
}
}
}
}
}
Related
My code is below.
RootNavGraph.kt
#Composable
fun RootNavGraph(navController: NavHostController = rememberNavController()) {
NavHost(
navController = navController,
route = rootRoute,
startDestination = authGraphRoutePattern
) {
authGraph(
navigateToHome = {
navController.popBackStack()
navController.navigateToAppBarGraph()
}
authOtherScreen { navController.popBackStack() }
}
appBarGraph()
supplementSearchScreen()
}
}
SupplementSearch.kt
const val supplementSearchRoute = "supplement_search_route"
fun NavController.navigateToSupplementSearch(navOptions: NavOptions? = null) {
this.navigate(supplementSearchRoute, navOptions)
}
fun NavGraphBuilder.supplementSearchScreen() {
composable(route = supplementSearchRoute) {
SupplementSearchRoute()
}
}
authGraph() is for different login.
appBarGraph() is for bottom navigation menus.
As far as I read this Navigation document, I can place screens in NavHost like this
SomeAScreen()
AGraph()
BGraph()
SomeBScreen()
SomeCScreen()
SomeDScreen()
CGraph()
But I get NPE when I call like this:
#Composable
fun AddSupplementItem(
addSupplement: Vitamin.AddSupplement
) {
val isClicked = remember { mutableStateOf(false) }
Column(
modifier = Modifier
.width(97.dp)
.clickable {
isClicked.value = !isClicked.value
},
horizontalAlignment = Alignment.CenterHorizontally
) {
AsyncImage(
model = addSupplement.imageUrl,
modifier = Modifier
.fillMaxWidth()
.padding(5.dp)
.aspectRatio(1.46f)
.clip(RoundedCornerShape(16.dp)),
contentDescription = addSupplement.name,
contentScale = ContentScale.Crop
)
Text(
text = addSupplement.name,
fontSize = 13.sp,
color = Color.Gray
)
}
if (isClicked.value) {
val navController = rememberNavController()
navController.navigateToSupplementSearch() // NullPointException
}
}
It seems like SupplementSearchScreen is not registered to the graph.
Should I keep passing navcontroller from NavHost to that Screen?
NavHost(...){
// otherGraphs()
supplementSearchScreen(navController)
}
But it didn't work.
And also,
// the parent composable function is
fun SupplementGrid(vitaminList: List<Vitamin>) {
// list of vitamin item. and also use AddSupplement()
}
// And also it has parent composable function
#Composable
fun SupplementLayout(feedType: FeedType, supplements: List<Vitamin>) {
// call SupplementGrid()
}
// and finally,
#Composable
fun NutritionScreen(
// it uses LazyColumn and one of item is SupplementLayout()
)
How can I solve this issue??
I'm trying to implement https://google.github.io/accompanist/navigation-material/ and i want to expand modelsheet to custom height or more than half screen but i don't have any idea how to achieve it
Currently ModelBottomSheet
Wish to expand like this
Instead of using the show method of the ModalBottomSheetState, You can use the animateTo method. The show method will default to a half screen size modal. The animateTo(ModalBottomSheetValue.Expanded) will expand to the full size of the content. In the example i've used a BoxWithConstrains to get the screen size and set the size of the modal content to 80%.
I hope this helps!
#Composable
#Preview
fun BottomSheetDemo() {
val modalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
BoxWithConstraints {
val sheetHeight = this.constraints.maxHeight * 0.8f
val coroutineScope = rememberCoroutineScope()
Column {
Button(onClick = {
coroutineScope.launch { modalBottomSheetState.animateTo(ModalBottomSheetValue.Expanded) }
}) {
Text(text = "Expand")
}
Button(onClick = {
coroutineScope.launch { modalBottomSheetState.animateTo(ModalBottomSheetValue.Hidden) }
}) {
Text(text = "Collapse")
}
}
ModalBottomSheetLayout(
sheetBackgroundColor = Color.Red,
sheetState = modalBottomSheetState,
sheetContent = {
Box(modifier = Modifier.height(with(LocalDensity.current) { sheetHeight.toDp() })) {
Text(text = "This is some content")
}
}
) {}
}
}
EDIT:
If you want to use the material navigation, you will need a custom extension function. The difference in this function with the original is the skipHalfExpanded parameter. This on will make it possible to create bottom sheets larger then half screen.
#Composable
fun rememberBottomSheetNavigator(
animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec
): BottomSheetNavigator {
val sheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden,
animationSpec = animationSpec,
skipHalfExpanded = true
)
return remember(sheetState) {
BottomSheetNavigator(sheetState = sheetState)
}
}
The implementation itself will be something like this:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val bottomSheetNavigator = rememberBottomSheetNavigator()
val navController = rememberNavController(bottomSheetNavigator)
ModalBottomSheetLayout(bottomSheetNavigator) {
NavHost(navController, "home") {
composable(route = "home") {
Home(navController)
}
bottomSheet(route = "sheet") {
ModalDemo()
}
}
}
}
}
}
#Composable
fun Home(navController: NavController) {
val coroutineScope = rememberCoroutineScope()
Column {
Button(onClick = {
coroutineScope.launch { navController.navigate("sheet") }
}) {
Text(text = "Expand")
}
}
}
#Composable
fun ModalDemo() {
Column(Modifier.fillMaxWidth().height(700.dp).background(Color.Red), horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "This is some content")
}
}
I am trying to setup an application flow in which there is a main route/screen, followed by a home route/screen where the home screen contains a scaffold to setup bottom bar navigation.
I originally had the scaffold setup at the main (top level) route where the scaffold content was just the NavHost ie:
#Composable
fun MainScreen() {
val scope = rememberCoroutineScope()
val scaffoldState = rememberScaffoldState()
val bottomSheetNavigator = rememberBottomSheetNavigator()
val navController = rememberNavController(bottomSheetNavigator)
ModalBottomSheetLayout(bottomSheetNavigator) {
Scaffold(
scaffoldState = scaffoldState,
drawerGesturesEnabled = false,
drawerContent = {...},
bottomBar = {...}
) {
NavHost(
navController = navController,
startDestination = "tab1"
) {
tab1Graph(navController)
tab2Graph(navController)
tab3Graph(navController)
}
}
}
}
Which is fine I suppose, however since only my home route needs a scaffold, why have the scaffold at the top level instead of at the lower level in which its needed.
Here is my attempt to move the scaffold into the home screen:
fun NavGraphBuilder.homeGraph(
navController: NavController,
bottomSheetNavigator: BottomSheetNavigator
) {
composable("home") {
val scope = rememberCoroutineScope()
val scaffoldState = rememberScaffoldState()
ModalBottomSheetLayout(bottomSheetNavigator) {
Scaffold(
scaffoldState = scaffoldState,
drawerContent = {...},
bottomBar = {...}
) {
// Not entirely sure how to setup bottom nav tabs within the scaffold?
}
}
}
}
However I am lost at how to get the tab content to live inside the scaffold based on route. EG the same magic that happens when you embed the NavHost inside the scaffold.
I'm currently working on a project where I solved the same problem.
First, in the MainActivity I call my MainNavGraph, then in the main NavGraph, I call my HomeScreen Composable which contains the BottomNavGraph and the screens to display in this HomeScreen. Finally, in the BottomNavGraph I include everything related to the HomeScreen
MainActivity :
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
}
val yourViewModel = viewModel(modelClass = YourViewModel::class.java)
YourProjectTheme {
MainNavGraph(yourViewModel)
}
}
}
Main navigation
#Composable
fun MainNavGraph(
yourMainViewModel: YourMainViewModel?,
){
val navController = rememberNavController()
NavHost(
navController = NavController,
startDestination = "top_level_composable"
){
composable("top_level_composable"){
TopLevelComposable{
navController.navigate("home_screen")
}
}
composable("home_screen"){
home()
}
}
}
Home Screen
#Composable
fun HomeScreen(){
val homeNavController = rememberNavController()
val anotherViewModel = viewModel(modelClass = AnotherViewModel::class.java)
Scaffold(
...
...
bottomBar = { BottomNavigationBar(navController) }
content = { padding ->
Box(modifier = Modifier.padding(padding)){
HomeNavGraph(
navController = homeNavController,
anotherViewModel = anotherViewModel
)
}
}
)
}
HomeNavGraph
#Composable
fun HomeNavGraph(
navController: NavHostController,
anotherViewModel: AnotherViewModel
) {
NavHost(
navController = navController,
route = "home_nav",
startDestination = "welcome"
){
composable("welcome"){
WelcomeScreen(navController)
}
composable("posts"){
PostsScreen(navController, anotherViewModel)
}
composable("search"){
SearchScreen(navController)
}
composable("messages"){
MessagesScreen(navController)
}
composable("profile"){
ProfileScreen(navController)
}
}
}
BottomNavigation
#Composable
fun BottomNavigationBar(navController: NavController) {
val items = listOf(
"welcome",
"posts",
"search",
"messages",
"profile",
)
BottomNavigation(
backgroundColor = Color.White,
contentColor = Color.Black,
modifier = Modifier.clip(RoundedCornerShape(topEnd = 20.dp, topStart = 20.dp)),
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
items.forEach { item ->
BottomNavigationItem(
label = { Text(text = item) },
selectedContentColor = GWpalette.ImperialRed,
unselectedContentColor = GWpalette.Gunmetal,
alwaysShowLabel = false,
selected = currentRoute == item,
onClick = {
navController.navigate(item) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
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
}
}
)
}
}
}
You can see how to create Bottom Navigation Bar with Jetpack Compose here
https://johncodeos.com/how-to-create-bottom-navigation-bar-with-jetpack-compose/
You can conditionally use the Scaffold based on the current route:
val navController = rememberNavController()
val navBackStateEntry by navController.currentBackStackEntryAsState()
if (navBackStateEntry?.destination?.route == "my_route") {
Scaffold(...)
} else {
Text("No scaffold")
}
I am trying to create an app with JetPack Compose (first time) and I am having some problem with the navigation. The application has a bottomBar with 3 items that navigates to the selected screen.
This works, the problem is when I try to access one of the items of a LazyColumn that is in one of the screens. I would like to navigate to another screen (Profile) where the data of the selected item is displayed but I can't find a way to do it. No matter how I try to do it, I always get this "#Composable invocations can only happen from the context of a #Composable function".
Could someone help me by explaining how to do it? What I want is to learn how and why, not just copy.
Thanks
MainActivity
class MainActivity : ComponentActivity() {
#ExperimentalFoundationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val systemUiController = rememberSystemUiController()
SideEffect {
systemUiController.setStatusBarColor(color = PrimaryDark)
}
AppTheme() {
MainScreen()
}
}
}
#ExperimentalFoundationApi
#Composable
fun MainScreen() {
val navController = rememberNavController()
val navigationItems = listOf(Obras, Talleres, Ajustes)
Scaffold(bottomBar = {
BottomNavigationBar(
navController = navController,
items = navigationItems
)
}) {
NavigationHost(navController)
}
}
}
NavigationHost.kt
#ExperimentalFoundationApi
#Composable
fun NavigationHost(navController: NavHostController) {
NavHost(navController = navController, startDestination = Obras.route) {
composable(Obras.route) {
Pantalla1(navigateToProfile = { authorId ->
navController.navigate("${Profile.route}/$authorId")
})
}
composable(Talleres.route) {
Pantalla2()
}
composable(Ajustes.route) {
Pantalla3()
}
composable(
Profile.route + "/{authorId}",
arguments = listOf(navArgument("authorId") { type = NavType.StringType })
) { backStackEntry ->
val authorId = backStackEntry.arguments!!.getString("authorId")!!
Profile(authorId)
}
}
}
Pantalla1.kt
typealias AuthorId = String
#ExperimentalCoroutinesApi
#Composable
fun Pantalla1(navigateToProfile: (AuthorId) -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(
paddingValues = PaddingValues(
bottom = 50.dp
)
),
) {
AutoresInfo(navigateToProfile)
}
}
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun AutoresInfo(navigateToProfile: (AuthorId) -> Unit) {
var autoresList by remember {
mutableStateOf<List<Autor>?>(null)
}
JetFirestore(path = { collection("autores") },
queryOnCollection = { orderBy("nombre", Query.Direction.ASCENDING) },
onRealtimeCollectionFetch = { value, exception ->
autoresList = value.getListOfObjects()
}) {
autoresList?.let {
val grouped = it.groupBy { it.nombre[0] }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
grouped.forEach { (initial, autoresForInitial) ->
stickyHeader {
StickyHeaderAutores(initial = initial.toString())
}
items(autoresForInitial, key = { autor -> autor.nombre }) { autor ->
Surface(modifier = Modifier.clickable { navigateToProfile(autor.nombre) }) {
#OptIn(coil.annotation.ExperimentalCoilApi::class)
AutorCard(autor)
}
}
}
}
} ?: Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator(
color = Color.Red,
modifier = Modifier
.size(50.dp)
)
}
}
}
Step 1. Add argument to your profile navigation route. Check out documentation about navigating with arguments
composable(
Profile.route + "/{authorId}",
arguments = listOf(navArgument("authorId") { type = NavType.StringType })
) { backStackEntry ->
val authorId = backStackEntry.arguments!!.getString("authorId")!!
Profile(authorId)
}
Step 2. You need to pass down navigateToProfile function from your NavigationHost. You can replace AuthorId with Int(or an other type) if it's not String:
typealias AuthorId = String
#Composable
fun NavigationHost(navController: NavHostController){
NavHost(navController = navController, startDestination = Obras.route) {
composable(Obras.route) {
Pantalla1(navigateToProfile = { authorId ->
navController.navigate("${Profile.route}/$authorId")
})
}
...
}
#Composable
fun Pantalla1(navigateToProfile: (AuthorId) -> Unit) {
...
AutoresInfo(navigateToProfile)
...
}
#Composable
fun AutoresInfo(navigateToProfile: (AuthorId) -> Unit) {
...
items(autoresForInitial, key = { autor -> autor.nombre }) { autor ->
Surface(modifier = Modifier.clickable {
navigateToProfile(author.id)
}) {
AutorCard(autor)
}
}
...
}
Step 3. In you profile composable you need to fetch author by id. Not sure what's JetFirestore, you probably should use it.
#Composable
fun Profile(id: AuthorId) {
JetFirestore(
// fetch author by id
)
}
create a sealed class that contains the route like below
sealed class Screen(val route:String){
object MyListScreen:Screen("my_list_screen")
object MyDetailScreen:Screen("my_detail_screen")
}
create listItem with Row that receive a list and an event of click like below
#Composable
fun MyListItem(
yourList: YourList,
onItemClick:(yourList) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
onItemClick(yourList)
}
){
//something here
}
Inside your listScreen in LazyColoumn you can do this
LazyColumn(modifier = Modifier.fillMaxSize()){
items (state.yourfreshlist){mylist->
MyListItem(mylist = mylist, onItemClick = {
navController.navigate(Screen.MyDetailScreen.route+"/${mylist.something}")
}
)
}
}
your host will be like this
NavHost(
navController = navController,
startDestination = Screen.MyListScreen.route
) {
composable(
route = Screen.MyListScreen.route
) {
MyListScreen(navController)
}
composable(
route = Screen.MyDetailScreen.route + "/{anything here id for example}"
) {
MyDetailScreen()
}
}
For more information, you can browse the docs
How to show navigation icon (BackArrow or Menu) in TopAppBar using Scaffold based on actual position in NavController? I am using Navigating with Compose 1.0.0-alpha02. Below is a sample code with a description of how it should work
#Composable
fun App()
{
val navController = rememberNavController()
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = "App title") },
navigationIcon = {
/*
Check if navController back stack has more
than one element. If so show BackButton.
Clicking on that button will move back
*/
val canMoveBack = true
if (canMoveBack)
{
IconButton(onClick = {
// Move back
navController.popBackStack()
}) {
Icon(asset = Icons.Outlined.ArrowBack)
}
}
else
{
IconButton(onClick = {
// show NavDrawer
}) {
Icon(asset = Icons.Outlined.Menu)
}
}
},
)
},
bodyContent = {
AppBody(navController)
}
)
}
I thought about something like navController.backStack.size but I got error NavController.getBackStack can only be called from within the same library group (groupId=androidx.navigation).
And the second question, if I wanted to change the TopAppBar text do I have to hoist this text and give every "screen" possibility to change this text, or is there any easy built-in way to do this like in the standard View System?
Thanks to Abdelilah El Aissaoui I have got an idea of how to do it with one Scaffold and just changing bodyContent. In this method, we don't have to pass navController to any body element, everything is done in base App composable. Below is code which enables to navigate between two bodies (Lesson -> Student)
App:
#Composable
fun App(
viewModel: MainViewModel
)
{
val navController = rememberNavController()
val baseTitle = "" // stringResource(id = R.string.app_name)
val (title, setTitle) = remember { mutableStateOf(baseTitle) }
val (canPop, setCanPop) = remember { mutableStateOf(false) }
val scaffoldState: ScaffoldState = rememberScaffoldState()
navController.addOnDestinationChangedListener { controller, _, _ ->
setCanPop(controller.previousBackStackEntry != null)
}
// check navigation state and navigate
if (viewModel.navigateToStudents.value)
{
navController.navigate(route = STUDENT_SCREEN_ROUTE)
viewModel.studentsNavigated()
}
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = title) },
navigationIcon = {
if (canPop)
{
IconButton(onClick = {
navController.popBackStack()
}) {
Icon(asset = Icons.Outlined.ArrowBack)
}
}
else
{
IconButton(onClick = {
scaffoldState.drawerState.open()
}) {
Icon(asset = Icons.Outlined.Menu)
}
}
},
)
},
scaffoldState = scaffoldState,
drawerContent = {
DrawerContent()
},
bodyContent = {
AppBody(
viewModel = viewModel,
navController = navController,
setTitle = setTitle
)
}
)
}
AppBody
#Composable
fun AppBody(
viewModel: MainViewModel,
navController: NavHostController,
setTitle: (String) -> Unit,
)
{
NavHost(
navController,
startDestination = LESSON_SCREEN_ROUTE
) {
composable(route = LESSON_SCREEN_ROUTE) {
LessonBody(
viewModel = viewModel,
setTitle = setTitle
)
}
composable(
route = STUDENT_SCREEN_ROUTE
) {
StudentBody(
viewModel = viewModel,
setTitle = setTitle
)
}
}
}
In the ViewModel I use this pattern to navigate:
private val _navigateToStudents: MutableState<Boolean> = mutableStateOf(false)
val navigateToStudents: State<Boolean> = _navigateToStudents
fun studentsNavigated()
{
// here we can add any logic after doing navigation
_navigateToStudents.value = false
}
So when I want to navigate to the next fragment I just set _navigateToStudents.value = true
I was just trying to achieve exactly the same today. I think this code will answer both questions:
#Composable
fun NavigationScaffold(
title: String? = null,
navController: NavController? = null,
bodyContent: #Composable (PaddingValues) -> Unit
) {
val navigationIcon: (#Composable () -> Unit)? =
if (navController?.previousBackStackEntry != null) {
{
IconButton(onClick = {
navController.popBackStack()
}) {
Icon(Icons.Filled.ArrowBack)
}
}
} else {
// this can be null or another component
// If null, the navigationIcon won't be rendered at all
null
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = title.orEmpty())
},
navigationIcon = navigationIcon,
)
},
bodyContent = bodyContent
)
}
As you can see, you can provide a title as a String so you don't have to worry about passing a Text composable.
This Scaffold is the base of all my screens and to use it a simply have to write something like that:
#Composable
fun Home(navController: NavController) {
NavigationScaffold(
title = "Home",
navController = navController
) {
// Screen's content...
}
}