I am trying to display a modal BottomSheet, which requires a parameter. This is how I show BottomSheet: bottomSheetState.animateTo(ModalBottomSheetValue.Expanded)
And this is my set-up:
#Composable
fun MainView() {
val navController = rememberNavController()
val modalBottomSheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden,
confirmStateChange = {
it != ModalBottomSheetValue.HalfExpanded
}
)
ModalBottomSheetLayout(
sheetState = modalBottomSheetState,
sheetContent = {
AnimationScreen()
},
sheetShape = RoundedCornerShape(topStart = Radius.l, topEnd = Radius.l)
) {
Scaffold(
bottomBar = {
BottomBar(navController)
}
) {
BottomBarMain(navController, modalBottomSheetState)
}
}
}
So, my question is: what is a good way to pass a parameter to this ModalBottomSheet.
I have a possible solution, but I don't know if it is a proper way to do it. Here how it looks like:
var animationId by remember { mutableStateOf(-1L) }
...
sheetContent = {
AnimationScreen(animationId)
}
...
BottomBarMain(navController, modalBottomSheetState) {
animationId = it
}
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 Have a ModalBottomSheet and I want to change a boolean value after clicking on the bottom sheet transparent background. How can I access this view click listener?
I don't want to use BottomSheetScaffold.
fun ScheduleJobFromQueuedBottomSheet(
viewModel: ProductViewModel,
onClickCancel: () -> Unit,
onQueuedScheduleRequestResultIsBack: () -> Unit,
) {
**var state by remember { mutableStateOf(false) }**
val bottomSheetState: ModalBottomSheetState =
rememberModalBottomSheetState(ModalBottomSheetValue.Expanded, confirmStateChange = {
it != ModalBottomSheetValue.HalfExpanded
}, animationSpec = TweenSpec(durationMillis = 300, delay = 10))
ModalBottomSheetLayout(
sheetState = bottomSheetState,
sheetShape = RoundedCornerShape(topStart = custom, topEnd = custom),
scrimColor = ModalBottomSheetDefaults.scrimColor.copy(alpha = 0.40f),
sheetContent = {
ReviewBookingScheduledBottomSheet(modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
viewModel = viewModel,
onClickCancel = {
onClickCancel.invoke()
}, onScheduledRequestResultIsBack = {
onQueuedScheduleRequestResultIsBack.invoke()
})
}
) {
}
}
I want to change the state value by clicking on the bottom sheet background (Not bottomsheet content)
Add one boolean mutableState variable and change this value when your bottom sheet state change
var isBottomSheetDismissed = remember { mutableStateOf(false) }
val bottomSheetState: ModalBottomSheetState =
rememberModalBottomSheetState(ModalBottomSheetValue.Expanded, confirmStateChange = {
isBottomSheetDismissed.value = it==ModalBottomSheetValue.Hidden
it != ModalBottomSheetValue.HalfExpanded
}, animationSpec = TweenSpec(durationMillis = 300, delay = 10))
In Compose observability is preferred over callbacks, thus there isn't an explicit listener available.
We can observe the transparent background click by observing the bottomSheetState state change when hidden:
fun ScheduleJobFromQueuedBottomSheet(
viewModel: ProductViewModel,
onClickCancel: () -> Unit,
onQueuedScheduleRequestResultIsBack: () -> Unit,
) {
var state by remember { mutableStateOf(false) }
val bottomSheetState: ModalBottomSheetState =
rememberModalBottomSheetState(ModalBottomSheetValue.Expanded, confirmStateChange = {
it != ModalBottomSheetValue.HalfExpanded
}, animationSpec = TweenSpec(durationMillis = 300, delay = 10))
LaunchedEffect(bottomSheetState) {
snapshotFlow { bottomSheetState.currentValue }
.filter { it == Hidden }
.collect { state = true }
}
ModalBottomSheetLayout(
sheetState = bottomSheetState,
sheetShape = RoundedCornerShape(topStart = custom, topEnd = custom),
scrimColor = ModalBottomSheetDefaults.scrimColor.copy(alpha = 0.40f),
sheetContent = {
ReviewBookingScheduledBottomSheet(modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
viewModel = viewModel,
onClickCancel = {
onClickCancel.invoke()
}, onScheduledRequestResultIsBack = {
onQueuedScheduleRequestResultIsBack.invoke()
})
}
) {
}
}
Unfortunately, this can't differentiate if the bottomSheetState change is due to click on the transparent background or due to other change by the state. If you have a use case that requires observing only for transparent background click but not other dismiss action, please leave a comment here: https://buganizer.corp.google.com/issues/256755735
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/
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...
}
}