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() }
) {
....
}
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??
Expected result:
Reusable ExtendedFloatingActionButton should expand or collapse within a reusable Scaffold when I scroll.
Current result:
ExtendedFloatingActionButton does not expand or collapse when I scroll the list in my Scaffold
I followed this tutorial, but it was not created with reusability in mind. The part I'm confused about is the listState varible within the section called "Joining Everything Together" because I'm not sure where within my code I need to declare it as I have done this a few times in different areas.
#Composable
fun MyReusableScaffold(scaffoldTitle: String, scaffoldFab: #Composable () -> Unit,
scaffoldContent: #Composable (contentPadding: PaddingValues) -> Unit) {
Scaffold(
topBar = { LargeTopAppBar( title = { Text(text = scaffoldTitle) } ) },
floatingActionButton = { scaffoldFab },
content = { contentPadding -> scaffoldContent(contentPadding = contentPadding) }
)
}
#Composable
fun MyFAB(listState: LazyListState) {
ExtendedFloatingActionButton(
text = { Text(text = "title") },
icon = { Icon(Icons.Filled.Add, "") },
expanded = listState.isScrollingUp()
)
}
#Composable
fun <T> MyLazyColumn(modifier: Modifier,
...
) {
val listState = rememberLazyListState()
LazyColumn(
state = listState
) {
...
}
}
#Composable
fun MyHomeScreen() {
MyScaffold(
scaffoldTitle = "title",
scaffoldFab = MyExtendedFAB(listState = LazyListState?)
scaffoldContent = { MyHomeScreenContent(contentPadding = it) },
)
}
#Composable
fun MyHomeScreenContent(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues()
) {
}
The expansion state of the ExtendedFloatingActionButton is managed by the expanded.
In your example the expanded state is derived on the listState of the LazyColumn. It means that LazyColumn and ExtendedFloatingActionButton have to use both the same listState.
For example you can do something like:
#Composable
fun MyHomeScreen() {
val listState = rememberLazyListState()
val expandedFab by remember {
derivedStateOf {
listState.firstVisibleItemIndex == 0
}
}
MyReusableScaffold(
scaffoldTitle = "title",
scaffoldFab = { MyFAB(expandedFab) },
scaffoldContent = { MyHomeScreenContent(contentPadding = it, listState = listState) },
)
}
fun MyFAB(expandedFab : Boolean) {
ExtendedFloatingActionButton(
//...
expanded = expandedFab
)
}
#Composable
fun MyLazyColumn(modifier: Modifier = Modifier,
listState: LazyListState
) {
LazyColumn(
state = listState
) { /** ... */ }
}
#Composable
fun MyHomeScreenContent(
modifier: Modifier = Modifier,
listState: LazyListState,
contentPadding: PaddingValues = PaddingValues()
) {
MyLazyColumn( listState = listState )
}
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 try to learn jetpack compose these days, and I have a simple project in jetpack compose, bottom sheet work in my project, but when I use bottom navigation, it is not work, I search in many website and stackoverflow especially, but I did not find any solution, I do not know what I missed? is there any idea?
#Composable
fun BSDataScreen() {
val modalBottomSheetState = rememberModalBottomSheetState(initialValue =
ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
ModalBottomSheetLayout(
sheetContent = {
SheetScreen()
},
sheetState = modalBottomSheetState,
sheetShape = RoundedCornerShape(topStart = 15.dp, topEnd = 15.dp),
sheetBackgroundColor = Color.White,
) {
Scaffold(
backgroundColor = Color.White,
) {
DataScreen(
scope = scope, state = modalBottomSheetState)}}}
#Composable
fun DataScreen(
scope: CoroutineScope,
state: ModalBottomSheetState
) {
val listOfData = listOf(
Data( painterResource(R.drawable.image1)),
Data(painterResource(R.drawable.image2)),
Data(painterResource(R.drawable.image3),)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
LazyColumn(
modifier = Modifier
) {
items(listOfData.size) { index ->
DataListItem(listOfData[index]) data: Data->
scope.launch {
state.show()
}
}}}}
#Composable
fun DataListItem(data: Data, onLongClick: (Data) -> Unit) {
val context = LocalContext.current
Column(
modifier = Modifier.padding(5.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(5.dp)
.combinedClickable(
onLongClick= {
onLongClick(data)
},)
) {
Image(
painter = data.painter,
contentDescription = null,)}}}
BottomNav:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MainScreen()
}
}
}
#Composable
fun MainScreen() {
val navController = rememberNavController()
Scaffold(
bottomBar = { BottomNavigationBar(navController) }
) {
Navigation(navController = navController)
}
}
#Composable
fun Navigation(navController: NavHostController) {
val modalBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
NavHost(navController, startDestination = NavigationItem.Data.route) {
composable(NavigationItem.Data.route) {
DataScreen(
scope = scope, state = modalBottomSheetState
)
}
composable(NavigationItem.Data2.route) {
Data2Screen()
}
composable(NavigationItem.Data3.route) {
Data3Screen()
}
composable(NavigationItem.Data4.route) {
Data4Screen()
}
composable(NavigationItem.Data5.route) {
Data5Screen()
}
}
}
#Composable
fun BottomNavigationBar(navController: NavController
) {
val items = listOf(
NavigationItem.Data,
NavigationItem.Data2,
NavigationItem.Data3,
NavigationItem.Data4,
NavigationItem.Data5
)
BottomNavigation(
backgroundColor = colorResource(id = R.color.white),
contentColor = colorResource(id = R.color.black)
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
items.forEach { item ->
BottomNavigationItem(
icon = {
Icon(
painterResource(id = item.icon),
contentDescription = null
)
},
selectedContentColor = colorResource(id = R.color.red),
unselectedContentColor = colorResource(id = R.color.blue),
alwaysShowLabel = true,
selected = currentRoute == item.route,
onClick = {
navController.navigate(item.route) {
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}})}}}
I think your problem is your bottom sheets appear under bottom bar. The same thing I solved is very simple.
#Composable
fun SettingView() {
val navController = rememberNavController()
val scaffoldState = rememberScaffoldState()
Scaffold(
bottomBar = {
BottomBar(navController = navController)
},
content = {
Box(modifier = Modifier.padding(it)) {
BottomNavGraph(
navController = navController,
scaffoldState = scaffoldState
)
}
}
)
}
content screens wrap with Box and padding to scaffold's PaddingValues.
Just wrap the whole screen with ModalBottomSheetLayout.
You can put the following code:
Scaffold(
bottomBar = { BottomNavigationBar(navController) }
) {
Navigation(navController = navController)
}
inside
ModalBottomSheetLayout(...){
Scaffold(
bottomBar = { BottomNavigationBar(navController) }
) {
Navigation(navController = navController)
}
}
Try using the Navigation Material library provided by Accompanist.
https://google.github.io/accompanist/navigation-material/