Composable screen with two or more BottomSheets - android

Through the question BottomSheetScaffold is overriding the background color of its parent it just came to my attention that when using using a BottomSheetScaffold we should place the content of the screen inside it (which, to me, is a bit odd).
Then the following question came to my mind. What should we do when a Screen has two or more Bottom Sheets?
sheetContent = {
when (condition) {
CONTENT_A -> { }
CONTENT_B -> { }
CONTENT_C -> { }
}
}
I don't think this is the best answer as long as each bottom sheet may have a different configuration or even one is a ModalBottomSheetLayout and one other a BottomSheetScaffold.

You can use the official framework from Accompanist by Google to use a navcontroller to switch between bottom sheets: https://google.github.io/accompanist/navigation-material/

You can do something like
val activeDialog = remember {
mutableStateOf("1")
}
sheet content
sheetContent = {
when(activeDialog.value) {
"1" -> {
Column {
Text(text = "Dialog1")
}
}
"2" -> {
Column {
Text(text = "Dialog2")
}
}
}
}
and use show
activeDialog.value = "1"
dialogsState.show()
This is two lines, but it works

Related

Compose navigation with shared component for multiple routes

Let's say I have this navigation structure:
That will look something like this with compose-navigation:
NavHost(navController = navController, startDestination = "login") {
composable("login") {
Text("Login Screen!")
}
navigation(route = "home", startDestination = "favorites") {
composable("favorites") {
Text("Favorites Screen!")
}
composable("search") {
Text("Search Screen!")
}
composable("profile") {
Text("Profile Screen!")
}
}
}
My issue is that I want to use compose-navigation to navigate to an "inner" screen inside home while still maintaining a shared component between all of home's routes (e.g. favorites, search & profile), this shared component may be a BottomAppBar for example. Please notice that the login for example doesn't have a bottom bar.
I have found multiple solutions to this problem but each and every one of them comes with a caveat that makes it difficult to use.
Make the home a route instead of a navigation and nest another NavHost inside of it.
This allows to do the following:
NavHost(navController = navController, startDestination = "login") {
composable("login") {
Text("Login Screen!")
}
composable(route = "home") {
val homeNavController = rememberNavController() //Second NavController!
Column {
NavHost(navController = homeNavController, startDestination = "favorites") {
composable("favorites") {
Text("Favorites Screen!")
}
composable("search") {
Text("Search Screen!")
}
composable("profile") {
Text("Profile Screen!")
}
}
}
BottomAppBar {
//Showing all items and onClick navigating
}
}
}
As you can see, because NavHost's builder lambda is a #Composable, we can just add a Column and show the BottomAppBar below the NavHost (which is basically the "inner" screen).
From what I've seen, this solution is error-prone because you'll have to maintain two distinct NavControllers that can't know anything about each other, also, I think its considered an anti-pattern.
Create a single Scaffold for the entire app, then we would be able to display the BottomBar if we're either one of favorites, search or profile.
val currentDestination = ... //String
Scaffold(
bottomBar = {
when (currentDestination) {
"favorites", "search", "profile" -> BottomAppBar {
//Showing all items and onClick navigating
}
else -> Unit
}
}
) {
NavHost(
navController = navController,
startDestination = "login",
modifier = Modifier.padding(it)
) {
composable("login") {
Text("Login Screen!")
}
navigation(route = "home", startDestination = "favorites") {
composable("favorites") {
Text("Favorites Screen!")
}
composable("search") {
Text("Search Screen!")
}
composable("profile") {
Text("Profile Screen!")
}
}
}
}
Here, because the BottomBar is composed whenever the app's current route is changed, we do a when and decide which composable to show (for example if it's login we don't show a BottomBar. This solution is overall good but it makes the screen separated from the Top/Bottom bar, thus creating more complexity when we'll add ViewModels to the mix because we'll have to inject the same instance to both the screen and the Top/Bottom bar composables (which is not that horrible but it does get really messy if you do anything extra).
And not really an option - Not using compose-navigation for the home route and just displaying the content based on a when statement (basically the same as no.1 but removing the NavHost and doing a "manual" check to decide which screen to draw). This just loses all of the navigation capabilities.
After a lot of thinking about this (relatively simple) problem, I'm surprised no one has yet to ask this, and also that Google doesn't have any documentation about a best-practice solution for this.
Would love to hear solutions regarding this. Thanks.

Jetpack compose - single Scaffold shared by multiple screens

This is a question about general navigation design in Jetpack compose which I find a bit confusing.
As I understand it, having multiple screens with each own Scaffold causes flickers when navigating (I definitely noticed this issue). Now in the app, I have a network observer that is tied to Scaffold (e.g. to show Snackbar when there is no internet connection) so that's another reason I'm going for a single Scaffold design.
I have a MainViewModel that holds the Scaffold state (e.g. top bar, bottom bar, fab, title) that each screen underneath can turn on and off.
#Composable
fun AppScaffold(
networkMgr: NetworkManager,
mainViewModel: MainViewModel,
navAction: NavigationAction = NavigationAction(mainViewModel.navHostController),
content: #Composable (PaddingValues) -> Unit
) {
LaunchedEffect(Unit) {
mainViewModel.navHostController.currentBackStackEntryFlow.collect { backStackEntry ->
Timber.d("Current screen " + backStackEntry.destination.route)
val route = requireNotNull(backStackEntry.destination.route)
var show = true
// if top level screen, do not show
topLevelScreens().forEach {
if (it.route.contains(route)) {
show = false
return#forEach
}
}
mainViewModel.showBackButton = show
mainViewModel.showFindButton = route == DrawerScreens.Home.route
}
}
Scaffold(
scaffoldState = mainViewModel.scaffoldState,
floatingActionButton = {
if (mainViewModel.showFloatingButton) {
FloatingActionButton(onClick = { }) {
Icon(Icons.Filled.Add, contentDescription = "Add")
}
}
},
floatingActionButtonPosition = FabPosition.End,
topBar = {
if (mainViewModel.showBackButton) {
BackTopBar(mainViewModel, navAction)
} else {
AppTopBar(mainViewModel, navAction)
}
},
bottomBar = {
if (mainViewModel.showBottomBar) {
// TODO
}
},
MainActivity looks like this
setContent {
AppCompatTheme {
var mainViewModel: MainViewModel = viewModel()
mainViewModel.coroutineScope = rememberCoroutineScope()
mainViewModel.navHostController = rememberNavController()
mainViewModel.scaffoldState = rememberScaffoldState()
AppScaffold(networkMgr, mainViewModel) {
NavigationGraph(mainViewModel)
}
}
}
Question 1) How do I make this design scalable? As one screen's FAB may have different actions from another screen's FAB. The bottom bar may be different between screens. The main problem is I need good a way for screens to talk to the parent Scaffold.
Question 2) Where is the best place to put the code under "LaunchedEffect" block whether it's ok here?
I found this StackOverflow answer that covers your question pretty well.
The key answers to your questions according to this answer are:
You define a data class that holds variables for each element that might change between the different screens that will be displayed inside the scaffold. This most probably will be at least the title:
data class ScaffoldViewState(
#StringRes val topAppBarTitle: Int? = null
)
Then, you store this data class using remember, so that a recomposition will be triggered whenever one value within the data class changes:
var scaffoldViewState by remember {
mutableStateOf(ScaffoldViewState())
}
Finally, you can assign the field within the data class to the title slot of the Scaffold.
Changing the variables of the data class should happen from the NavHost, as seen in the linked post.

Showing a BottomSheetScaffold with a BottomNavigation - Compose UI - Android

Using Compose UI, I have a bottom navigation bar and a Bottom Sheet,
so starting a "BottomSheetScaffold" from "Catalogue" screen is causing the "Bottom Nav Bar" to stay visible.
How can I show "BottomSheetScaffold" making it cover the whole Screen (covering the bottom nav. bar),
but keeping in mind to write the "BottomSheetScaffold" in the Compose Screen [Catalogue] itself, NOT on a higher level (Parent Activity Level) since it doesn't seem right
If I didn't misunderstood the question, you can wrap your content with a ModalBottomSheetLayout.
ModalBottomSheetLayout(
sheetState = modalBottomSheetState,
sheetContent = {
BottomSheetContent()
}
) {
Scaffold(...)
}
The result would be something like this:
It's not totally related to your question, but if you want to avoid the half-expanded state, you can do the following:
Declare the function below:
#ExperimentalMaterialApi
suspend fun ModalBottomSheetState.forceExpand() {
try {
animateTo(ModalBottomSheetValue.Expanded)
} catch (e: CancellationException) {
currentCoroutineContext().ensureActive()
forceExpand()
}
}
In your Bottom Sheet declaration, do the foollowing:
val modalBottomSheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden,
confirmStateChange = {
it != ModalBottomSheetValue.HalfExpanded
}
)
Call the following to show/hide the Bottom Sheet:
coroutineScope.launch {
if (modalBottomSheetState.isVisible) {
modalBottomSheetState.hide()
} else {
modalBottomSheetState.forceExpand()
}
}
A temporary solution that I use is passing lambda function that will change bottom bar visibility to compose screen

What is the equivalent of [NestedScrollView + RecyclerView] or [Nested RecyclerView (Recycler inside another recycler) in Jetpack compose

I want to create the following layout in Jetpack compose.
I've tried creating two lists inside a vertical scrollable Box but that's not possible as I got the this error:
"java.lang.IllegalStateException: Nesting scrollable in the same direction layouts like ScrollableContainer and LazyColumn is not allowed. If you want to add a header before the list of items please take a look on LazyColumn component which has a DSL api which allows to first add a header via item() function and then the list of items via items()."
I've tried creating two different lists inside a parent list by using the following code, but that doesn't work either.
#Composable
fun MainList() {
LazyColumn() {
item {
/* LazyRow code here */
}
item {
/* LazyColumn code here */
}
}
}
Now I'm clueless about what else could I try to achieve two lists (one vertical and one horizontal) on the same activity and keep the activity vertically scrollable too.
I think the best option, would be if the LazyVerticalGrid allows some sort of expand logic on each item, but looks like it's not supported yet (beta-03).
So I'm leaving here my solution using one single LazyColumn for the entire list and LazyRow for "My Books" section.
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
// My Books section
item {
Column(modifier = Modifier.fillMaxWidth()) {
Text("My Books")
LazyRow {
items(books) { item ->
// Each Item
}
}
}
}
// Whishlisted Books title
item {
Text("Whishlisted Books", style = MaterialTheme.typography.h4)
}
// Turning the list in a list of lists of two elements each
items(wishlisted.windowed(2, 2, true)) { item ->
Row {
// Draw item[0]
// Draw item[1]
}
}
}
Here is my gist with the full solution and the result is listed below.
You can do something like:
Column(Modifier.fillMaxWidth()) {
LazyRow() {
items(itemsList){
//.....
}
}
LazyColumn() {
items(itemsList2){
//..
}
}
}
or:
Column(Modifier.fillMaxWidth()) {
LazyRow() {
items(itemsList){
//....
}
}
LazyVerticalGrid(cells = GridCells.Fixed(2)) {
items(itemsList2.size){
//....
}
}
}
An alternative equivalent of nested RecyclerViews is nested LazyColumns, where the heights of the inner LazyColumns are specified or constant, and the inner LazyColumns are placed inside item {} blocks.
Unlike the accepted answer, this approach relies on the .height() modifier to avoid the "java.lang.IllegalStateException: Nesting scrollable in the same direction layouts like ScrollableContainer and LazyColumn is not allowed... " error. Also, this approach addresses the scenario of nested scrolling in the same direction.
Here is an example code and output.
#Composable
fun NestedLists() {
LazyColumn(Modifier.fillMaxSize().padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally) {
//Header for first inner list
item {
Text(text = "List of numbers:", style = MaterialTheme.typography.h5)
}
// First, scrollable, inner list
item {
// Note the important height modifier.
LazyColumn(Modifier.height(100.dp)){
val numbersList = (0 .. 400 step 4).toList()
itemsIndexed(numbersList) { index, multipleOf4 ->
Text(text = "$multipleOf4", style = TextStyle(fontSize = 22.sp, color = Color.Blue))
}
}
}
// Header for second inner list
item {
Text(text = "List of letters:", style = MaterialTheme.typography.h5)
}
// Second, scrollable, inner list
item {
// Note the important height modifier.
LazyColumn(Modifier.height(200.dp)) {
val lettersList = ('a' .. 'z').toList()
itemsIndexed(lettersList) { index, letter ->
Text(text = "$letter", style = TextStyle(color = Color.Blue, fontSize = 22.sp))
}
}
}
}
}

How to add marquee animation to icon titles on Bottom Navigation View?

I was wondering if there is a way that makes the bottom navigation bar icon titles to scroll like marquee. I have seen some apps like that and tried to find some ways but couldn't find any. so if anyone could help me i would appreciate it.
There is the simplest solution. Works on Material Components versions 1.0.0 - 1.2.0-alpha02.
fun BottomNavigationView.findAllLabels(): Sequence<TextView> {
return sequence {
children.forEach { bottomNavigationChild ->
if (bottomNavigationChild is ViewGroup) {
bottomNavigationChild.children.forEach {
val smallLabel = it.findViewById<TextView>(
com.google.android.material.R.id.smallLabel
)
yield(smallLabel)
val largeLabel = it.findViewById<TextView>(
com.google.android.material.R.id.largeLabel
)
yield(largeLabel)
}
}
}
}
}
// Your BottomNavigationView
bottomNavigationView.findAllLabels().forEach {
it.apply {
ellipsize = TextUtils.TruncateAt.MARQUEE
setSingleLine(true)
// For infinite loop, comment if it's not required
marqueeRepeatLimit = -1
}
}

Categories

Resources