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??
Related
I'm fairly new to jetpack and android development and I'm trying to navigate between views. Coming from Swift I thought I could pass NavController to update the view.
My RootScreen looks like this
#Composable
fun RootScreen() {
val navigationController = rememberNavController()
Scaffold(
bottomBar = {
BottomBar(navigationController)
}
) {
NavHost(
navController = navigationController,
startDestination = NavigationItem.Home.route
) {
composable(NavigationItem.Home.route) {
HomeScreen(navController = navigationController)
}
composable(NavigationItem.Routes.route) {
RouteList()
}
composable(NavigationItem.Settings.route) {
Text("Settings")
}
}
}
}
From my HomeScreen I'm tapping a view that has a navigationAction where I am updating the route, but the view does not update.
#Composable
fun HomeScreen(navController: NavController) {
...
HomeCard(title = homeScreen[0].title,
image = painterResource(id = homeScreen[0].image),
frameHeight = 250,
frameWidth = 175,
navigationAction = {
navController.navigate(NavigationItem.Routes.route)
}
)
}
Here's where I'm clicking the action
#Composable
fun HomeCard(
title: String,
image: Painter,
frameHeight: Int,
frameWidth: Int,
navigationAction: () -> Unit
) {
Card(
elevation = 13.dp,
modifier = Modifier
.padding(top = 8.dp)
.clip(RoundedCornerShape(13.dp))
.height(frameHeight.dp)
.width(frameWidth.dp)
.clickable { navigationAction }
) {
....
}
Your screen is not getting navigated because the navigationAction never invoked.
#Composable
fun HomeCard(
navigationAction: () -> Unit
) {
Card(
modifier = Modifier
.clickable { navigationAction.invoke() }
) {
....
}
I am currently using a navigation drawer in jetpack compose, but I have encountered a problem. When I added the navigation drawer to the mainDrawerScreen, it was displaying for every screen I added, in my case it said "Main Menu" for every screen composable when navigated to. This is not ideal.
I decided to remove TopAppBar from mainDrawerScreen and create a seperat scaffold with the TopAppBar for each composable screen. However, now the navigation does not work. I tried to create a trailing lambda for navigation but might have done it the wrong way. Anyone got a suggestion to why it is not opening the navDrawer ? Appreciate the feedback!
MainDrawerSCreen:
#SuppressLint("UnusedMaterialScaffoldPaddingParameter")
#Composable
fun MainDrawerScreen() {
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
val scope = rememberCoroutineScope()
val navController = rememberNavController()
Scaffold(
scaffoldState = scaffoldState,
drawerContent = {
DrawerHeader()
DrawerLayout(scope = scope, scaffoldState = scaffoldState , navController = navController)
}
) {
Navigation(navController = navController) // TODO This controls the navigation between different screens
}
}
My seperate created TopAppBar:
#Composable
fun TopAppBar1(
scope: CoroutineScope,
scaffoldState: ScaffoldState,
text: String,
onIconClick: () -> Unit
) {
TopAppBar(
title = { Text(text = text, fontSize = 18.sp) },
navigationIcon = {
IconButton(onClick = {
scope.launch {
withContext(Dispatchers.IO) { // should I keep this coroutine on background thread ?
scaffoldState.drawerState.open()
}
}
}) {
Icon(
Icons.Filled.Menu, "Menu",
Modifier.clickable { onIconClick.invoke()})
}
},
backgroundColor = LightBlue,
contentColor = MaterialTheme.colors.onPrimary,
)
}
ProfileScreen:
#SuppressLint("UnusedMaterialScaffoldPaddingParameter")
#Composable
fun ProfileScreen1() {
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
val scope = rememberCoroutineScope()
Scaffold(
topBar = {
TopAppBar1(scope = scope, scaffoldState = scaffoldState, text = "Profile") {
scope.launch { scaffoldState.drawerState.currentValue } <----------- Trailing Lambda
}
},
content = {
ActualBackground() // Background
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) { // Content }
I show you what I did :
TopAppBar.kt :
#Composable
fun TopAppBarScreen(
title: String = "",
screen: String,
navController: NavController
) {
TopAppBar(
title = {
Text(
text = title,
style = Typography.h2
)
},
navigationIcon = {
IconButton(
onClick = {
navController.navigate(screen)
}
) {
Icon(Icons.Filled.ArrowBack, "backIcon")
}
},
backgroundColor = Color.Background,
contentColor = Color.BackgroundDarkGrey,
elevation = 0.dp,
)
}
Navigation.kt
#Composable
fun Navigation() {
val navController = rememberNavController()
val viewModel = hiltViewModel<CountrySelectorViewModel>()
val userViewModel = InputViewModel(LocalContext.current)
NavHost(
navController = navController,
startDestination = Screen.WELCOME_SCREEN
) {
composable(Screen.WELCOME_SCREEN) {
WelcomeScreen(navController = navController)
}
}
Screen :
object Screen {
const val WELCOME_SCREEN = "WelcomeScreen"
}
When I call my TopAppBar :
TopAppBarScreen(
screen = Screen.WELCOME_SCREEN,
navController = navController
)
Look at how the navigation works and it's the same if you are using a button to change screen.
I hope this can help you
#Composable
fun TopAppBar(
title: #Composable () -> Unit,
modifier: Modifier = Modifier,
navigationIcon: #Composable (() -> Unit)? = null,
actions: #Composable RowScope.() -> Unit = {},
backgroundColor: Color = MaterialTheme.colors.primarySurface,
contentColor: Color = contentColorFor(backgroundColor),
elevation: Dp = AppBarDefaults.TopAppBarElevation
)
actions: #Composable RowScope.() -> Unit = {}
Usage Scenario:
Using Compose Navigation to switch to different "screens", so the TopAppBar actions will be changed accordingly. Eg. Share buttons for content screen, Filter button for listing screen
Tried passing as a state to the TopAppBar's actions parameter, but having trouble to save the lambda block for the remember function.
val (actions, setActions) = rememberSaveable { mutableStateOf( appBarActions ) }
Want to change the app bar actions content dynamically. Any way to do it?
This the approach I used but I'm pretty new on compose, so I cannot be sure it is the correct approach.
Let's assume I have 2 screens: ScreenA and ScreenB
They are handled by MainActivity screen.
This is our MainActivity:
#ExperimentalComposeUiApi
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
#OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CoolDrinksTheme {
val navController = rememberNavController()
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
var appBarState by remember {
mutableStateOf(AppBarState())
}
Scaffold(
topBar = {
SmallTopAppBar(
title = {
Text(text = appBarState.title)
},
actions = {
appBarState.actions?.invoke(this)
}
)
}
) { values ->
NavHost(
navController = navController,
startDestination = "screen_a",
modifier = Modifier.padding(
values
)
) {
composable("screen_a") {
ScreenA(
onComposing = {
appBarState = it
},
navController = navController
)
}
composable("screen_b") {
ScreenB(
onComposing = {
appBarState = it
},
navController = navController
)
}
}
}
}
}
}
}
}
As you can see I'm using a mutable state of a class which represents the state of our MainActivity (where the TopAppBar is declared and composed), in this example there is the title and the actions of our TopAppBar.
This mutable state is set with a callback function called inside the composition of each screen.
Here you can see the ScreenA
#Composable
fun ScreenA(
onComposing: (AppBarState) -> Unit,
navController: NavController
) {
LaunchedEffect(key1 = true) {
onComposing(
AppBarState(
title = "My Screen A",
actions = {
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = null
)
}
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Default.Filter,
contentDescription = null
)
}
}
)
)
}
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Screen A"
)
Button(onClick = {
navController.navigate("screen_b")
}) {
Text(text = "Navigate to Screen B")
}
}
}
And the ScreenB
#Composable
fun ScreenB(
onComposing: (AppBarState) -> Unit,
navController: NavController
) {
LaunchedEffect(key1 = true) {
onComposing(
AppBarState(
title = "My Screen B",
actions = {
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Default.Home,
contentDescription = null
)
}
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null
)
}
}
)
)
}
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Screen B"
)
Button(onClick = {
navController.popBackStack()
}) {
Text(text = "Navigate back to Screen A")
}
}
}
And finally this is the data class of our state:
data class AppBarState(
val title: String = "",
val actions: (#Composable RowScope.() -> Unit)? = null
)
In this way you have a dynamic appbar declared in the main activity but each screen is responsable to handle the content of the appbar.
First you need to add navigation dependency on you jetpack compose projects.
You can read the doc from this https://developer.android.com/jetpack/compose/navigation
def nav_version = "2.4.1"
implementation "androidx.navigation:navigation-compose:$nav_version"
Then define your screen in sealed class:
sealed class Screen(var icon: ImageVector, var route: String) {
object ContentScreen: Screen(Icons.Default.Home, "home")
object ListingScreen: Screen(Icons.Default.List, "list")
}
and this is the navigation function look like
#Composable
fun Navigation(paddingValues: PaddingValues, navController: NavHostController) {
NavHost(navController, startDestination = Screen.ContentScreen.route, modifier = Modifier.padding(paddingValues)) {
composable(Screen.ContentScreen.route) {
//your screen content
}
composable(Screen.ListingScreen.route) {
//your listing screen here
}
}
}
Finally in your mainactivity class
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TestAppTheme {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
Scaffold(
topBar = {
TopAppBar(title = { Text(text = "main screen") }, actions = {
if (currentRoute == Screen.ContentScreen.route) {
//your share button action here
} else if (currentRoute == Screen.ListingScreen.route) {
//your filter button here
} else {
//other action
}
})
}
) {
Navigation(paddingValues = it, navController = navController)
}
}
}
}
I'm so sorry if the explanation to sort, because the limitation of my English
I have this structure:
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "auth"
) {
composable(
route = "auth"
) {
AuthScreen(
navController = navController
)
}
composable(
route = "profile"
) {
ProfileScreen(
navController = navController
)
}
}
When I first time open the app, I display a screen according to the authentication state:
if (!viewModel.isUserAuthenticated) {
AuthScreen(navController = navController)
} else {
ProfileScreen(navController = navController)
}
Which works fine. The problem comes, when I try to sing-in in the AuthScreen:
when(val response = authViewModel.signInState.value) {
is Response.Loading -> CircularProgressIndicator()
is Response.Success -> {
if (response.data) {
navController.navigate("profile")
Log.d(TAG, "Success")
}
}
is Response.Error -> Log.d(TAG, response.message)
}
The log statement prints "Success" but it doesn't navigate to the next ProfileScreen. How to solve this?
You can remove that if-else from the setContent. Instead, make ProfileScreen as the home destination and inside it you can check whether user is authenticated or not. If he is not, navigate to the AuthScreen
#Composable
fun ProfileScreen(navController: NavController) {
LaunchedEffect(Unit) {
if(!viewModel.isUserAuthenticated) {
navController.navigate("auth")
}
}
}
If user can logout from this screen (i.e. auth state can change), then instead of Unit use viewModel.isUserAuthenticated as the key for LaunchedEffect (assuming that isUserAuthenticated is a State)
Here is a much more detailed answer with code and demo if someone is looking
Code:
ComposeNavigationActivity.kt
class ComposeNavigationActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {Navigation()}
}
}
Composables
#Composable
fun MainScreen(navController: NavController) {
var inputFieldText by remember { mutableStateOf("") }
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.Red),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
TextField(
value = inputFieldText,
onValueChange = {
inputFieldText = it
},
modifier = Modifier.fillMaxWidth().padding(30.dp)
)
Spacer(modifier = Modifier.height(20.dp))
Button(
modifier = Modifier.padding(5.dp),
onClick = {
navController.navigate(Screen.DetailScreen.withArgs(inputFieldText))
}) {
Text(
text = "Navigate",
color = Color.White,
textAlign = TextAlign.Center,
fontSize = 20.sp
)
}
}
}
#Composable
fun DetailScreen(name: String?) {
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.Green),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
name?.let {
Text(
text = it,
color = Color.White,
textAlign = TextAlign.Center,
fontSize = 30.sp
)
}
}
}
A Sealed class to track routes
sealed class Screen(val route:String){
object MainScreen : Screen(route = "main_screen")
object DetailScreen : Screen(route = "detail_screen")
fun withArgs(vararg args:String) : String {
return buildString {
append(route)
args.forEach { arg ->
append("/$arg")
}
}
}
}
Navigation
#Composable
fun Navigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Screen.MainScreen.route) {
composable(route = Screen.MainScreen.route) {
MainScreen(navController = navController)
}
composable(
route = Screen.DetailScreen.route + "/{name}",
arguments = listOf(
navArgument("name") {
type = NavType.StringType
defaultValue = "Some Default"
nullable = true
}
)
) { entry ->
DetailScreen(name = entry.arguments?.getString("name"))
}
}
}
Output
so I am trying to create an app with Jetpack Compose. I have a Screen function which contains a Scaffold with no top app bar, a bottom bar for navigation and a floating action button set into the bottom bar. This all works fine.
However, when I add a NavHost to the scaffold's content, the whole thing stops working. It all works fine without a NavHost, and simply just the content being the composable function for a screen. I have tried with differing amounts of composable locations for the NavHost, different values for padding all to no avail.
What it looks like without a NavHost (i.e. how I want it to look)
Code:
sealed class Screen(val route: String, #DrawableRes val iconId: Int){
object Home : Screen("home", R.drawable.ic_home_24px)
object Stats : Screen("stats", R.drawable.ic_stats_icon)
object Add : Screen("add", R.drawable.ic_add_24px)
object Programs : Screen("programs", R.drawable.ic_programs_icon)
object Exercises : Screen("exercises", R.drawable.ic_exercises_icon)
}
#ExperimentalFoundationApi
#Preview
#Composable
fun Screen(){
val navController = rememberNavController()
Scaffold(
backgroundColor = OffWhite,
bottomBar = {
BottomBar(navController = navController)
},
floatingActionButton = {
FloatingActionButton(
onClick = {},
shape = CircleShape,
backgroundColor = Blue
) {
Icon(
painter = painterResource(id = R.drawable.ic_add_24px),
contentDescription = "Add",
tint = OffWhite,
modifier = Modifier
.padding(12.dp)
.size(32.dp)
)
}
},
isFloatingActionButtonDocked = true,
floatingActionButtonPosition = FabPosition.Center,
) {
HomeScreen()
// NavHost(
// navController = navController,
// startDestination = Screen.Home.route
// ){
// composable(Screen.Home.route){ HomeScreen() }
// composable(Screen.Stats.route){ HomeScreen() }
// composable(Screen.Programs.route){ HomeScreen() }
// composable(Screen.Exercises.route){ HomeScreen() }
// }
}
}
#Composable
fun BottomBar(
navController : NavController
){
val items = listOf(
Screen.Home,
Screen.Stats,
Screen.Add,
Screen.Programs,
Screen.Exercises
)
BottomAppBar(
backgroundColor = OffWhite,
cutoutShape = CircleShape,
content = {
BottomNavigation(
backgroundColor = OffWhite,
contentColor = OffWhite,
modifier = Modifier
.height(100.dp)
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
items.forEach { screen ->
val selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true
BottomNavigationItem(
icon = {
val iconSize = if (selected) 32.dp else 20.dp
Icon(
painter = painterResource(id = screen.iconId),
contentDescription = screen.route,
tint = Blue,
modifier = Modifier
.padding(12.dp)
.size(iconSize)
)
},
selected = selected,
onClick = {
//Navigate to selected screen
navController.navigate(screen.route) {
//Pop all from stack
popUpTo(navController.graph.findStartDestination().id){
saveState = true
}
//Avoid multiple copies of same screen on stack
launchSingleTop = true
//Restore state when reselecting a previously selected item
restoreState = true
}
},
alwaysShowLabel = false
)
}
}
}
)
}
What it looks like with the NavHost. The boxes in that image are the BottomBar failing to draw, as each box can be clicked on which takes me to BottomBar, BottomNavigationItem, Icon etc. Anyone have any idea whats going on here, and what I can do to fix it? Thanks
Edit: one thing I thought of was changing the 'selected' boolean value in fun BottomBar -> BottomNavigationItem to always be true, just to see if null values were affecting it but this did not change anything.
Error Message says Preview does not suppport ViewModels creation. Since, NavHost create viewmodels, does the error. So what I did to somewhat preview the scaffold and some screen, I separate the content of the scaffold.
Example:
#Composable
fun MainApp(
navController: NavController,
content: #Composable (PaddingValues) -> Unit
) {
StoreTheme {
Scaffold(
bottomBar = { BottomAppNavigationBar(navController) },
content = content
)
}
}
On Real App:
...
val navController = rememberNavController()
MainApp(navController) { innerPadding ->
NavHost(
navController = navController,
startDestination = BottomNavMenu.Screen1.route,
modifier = Modifier.padding(innerPadding)
) {
composable(...) { Screen1... }
composable(...) { Screen2... }
}
}
...
On Preview:
#Preview(showBackground = true)
#Composable
fun MainAppPreview() {
val navController = rememberNavController()
MainApp(navController) {
Screen1(navController)
}
}
Turns out my studio theme was hiding the error notification. After changing theme it says
java.lang.IllegalStateException: ViewModels creation is not supported in Preview
so I guess I need to whack it in my phone to test.