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
Related
This is the composable hierarchy in my app:
HorizontalPager
↪LazyVerticalGrid
↪PostItem
↪AsyncImage
DropdownMenu
↪DropdownMenuItem
When swiping the HorizontalPager the AsyncImage inside my PostItem's are recomposing.
Removing the DropdownMenu fixes this and the PostItem is no longe recomposing and gives the wanted behavior.
The problem is that I have huge FPS drops when swiping through the HorizontalPager.
Why is DropdownMenu causing a recomposition when swiping the HorizontalPager?
var showMenu by remember { mutableStateOf(false) }
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }) {
DropdownMenuItem(onClick = {
}) {
Text(text = "Share")
}
}
Unfortunately, without seeing more code showing the rest of the structure, it's hard to say for sure what your problem is here.
A likely answer is that you haven't split up your Composable function enough. Something the docs hardly talk about is that the content portion of a lot of the built-in Composeables are inline functions which means that if the content recomposes the parent will as well. This is the most simple example of this I can give.
#Composable
fun foo() {
println("recompose function")
Box {
println("recompose box")
Column {
println("recompose column")
Row {
println("recompose row")
var testState by mutableStateOf("my text")
Button(
onClick = { testState = "new text" }
) {}
Text(testState)
}
}
}
}
output is:
recompose function
recompose box
recompose column
recompose row
Not only does this recompose the whole function it also recreates the testState causing it to never change values.
Again not 100% that this is your problem, but I would look into it. The solution would be to split my Row and row content into it's own Composable function.
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.
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
So what I am trying to achieve is:
A home composable that hosts a BottomNav Bar (Scaffold is used here )
Bottom Nav Bar is attached with 3 other composables
Each of the 3 composables has its own lazy column
Every item in the 3 lazy columns have a menu icon which when clicked opens the bottom sheet
I was able to achieve the above by enclosing the scaffold inside a ModalBottonSheetLayout with the help of this answer here: Jetpack Compose Scaffold + Modal Bottom Sheet
The issue:
The purpose of the bottom sheet is to show a menu item which when clicked should delete the item from the lazyColumn
so the bottom sheet needs to know the details of the item that was clicked to delete it
How can I achieve that?
The home composable does not have any information about the items inside the lazy columns present in the composables that it hosts.
Is there a different way to approach this?
Here is my code:
HomeComposable.kt
fun HomeComposable(homeViewModel: HomeViewModel,
navController: NavHostController) {
ModalBottomSheetLayout(
sheetContent = {
//get the id of the message here so it can be
//deleted
Button(
text = "delete Message")
onClick = deleteMessage(message.id
)
}
) {
Scaffold(
content = {
NavHost(navController = navController, startDestination =
WHATS_NEW_COMPOSABLE) {
composable(MESSAGES_COMPOSABLE) {
MessageItemsComposable(
homeViewModel,
navController,
bottomSheetState
)
}
}
}
)
MessageItemsComposable.kt
val messages : List<Messages> = getMessagesFromNetwork()
LazyColumn {
items(messages) { message ->
Row{
Text(message.title)
Icon(
onClick = {
bottomState.show()
//show a bottom sheet with an option to delete
}
) {
//Draw the icon
}
}
}
}
For that case, I would use a repository with a StateFlow. This repository would be a singleton and it's responsible of maintaining data and deleting them. Your composable list will listen to state changes and your bottomsheet will call delete() to delete the selected element.
class Repository<T> {
private val _state = MutableStateFlow<T>(initialState)
val state: StateFlow<T> = _state
fun delete(data: T) {
_state.remove(data)
}
}
The parent of you composable list should provide a callback. This callback should be invoked when the user clicks on a list item and that item is passed as parameter.
#Composable
fun DataList(onItemClick(item: T)) {
// list implementation with onItemCkick as onClick callback
}
When invoked, you can open the bottomsheet and pass the item to it.
How can I go about making a composable deep down within the render tree full screen, similar to how the Dialog composable works?
Say, for example, when a use clicks an image it shows a full-screen preview of the image without changing the current route.
I could do this in CSS with position: absolute or position: fixed but how would I go about doing this in Jetpack Compose? Is it even possible?
One solution would be to have a composable at the top of the tree that can be passed another composable as an argument from somewhere else in the tree, but this sounds kind of messy. Surely there is a better way.
From what I can tell you want to be able to draw from a nested hierarchy without being limited by the parent constraints.
We faced similar issues and looked at the implementation how Composables such as Popup, DropDown and Dialog function.
What they do is add an entirely new ComposeView to the Window.
Because of this they are basically starting from a blank canvas.
By making it transparent it looks like the Dialog/Popup/DropDown appears on top.
Unfortunately we could not find a Composable that provides us the functionality to just add a new ComposeView to the Window so we copied the relevant parts and made following.
#Composable
fun FullScreen(content: #Composable () -> Unit) {
val view = LocalView.current
val parentComposition = rememberCompositionContext()
val currentContent by rememberUpdatedState(content)
val id = rememberSaveable { UUID.randomUUID() }
val fullScreenLayout = remember {
FullScreenLayout(
view,
id
).apply {
setContent(parentComposition) {
currentContent()
}
}
}
DisposableEffect(fullScreenLayout) {
fullScreenLayout.show()
onDispose { fullScreenLayout.dismiss() }
}
}
#SuppressLint("ViewConstructor")
private class FullScreenLayout(
private val composeView: View,
uniqueId: UUID
) : AbstractComposeView(composeView.context) {
private val windowManager =
composeView.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
private val params = createLayoutParams()
override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
private set
init {
id = android.R.id.content
ViewTreeLifecycleOwner.set(this, ViewTreeLifecycleOwner.get(composeView))
ViewTreeViewModelStoreOwner.set(this, ViewTreeViewModelStoreOwner.get(composeView))
ViewTreeSavedStateRegistryOwner.set(this, ViewTreeSavedStateRegistryOwner.get(composeView))
setTag(R.id.compose_view_saveable_id_tag, "CustomLayout:$uniqueId")
}
private var content: #Composable () -> Unit by mutableStateOf({})
#Composable
override fun Content() {
content()
}
fun setContent(parent: CompositionContext, content: #Composable () -> Unit) {
setParentCompositionContext(parent)
this.content = content
shouldCreateCompositionOnAttachedToWindow = true
}
private fun createLayoutParams(): WindowManager.LayoutParams =
WindowManager.LayoutParams().apply {
type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
token = composeView.applicationWindowToken
width = WindowManager.LayoutParams.MATCH_PARENT
height = WindowManager.LayoutParams.MATCH_PARENT
format = PixelFormat.TRANSLUCENT
flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
}
fun show() {
windowManager.addView(this, params)
}
fun dismiss() {
disposeComposition()
ViewTreeLifecycleOwner.set(this, null)
windowManager.removeViewImmediate(this)
}
}
Here is an example how you can use it
#Composable
internal fun Screen() {
Column(
Modifier
.fillMaxSize()
.background(Color.Red)
) {
Text("Hello World")
Box(Modifier.size(100.dp).background(Color.Yellow)) {
DeeplyNestedComposable()
}
}
}
#Composable
fun DeeplyNestedComposable() {
var showFullScreenSomething by remember { mutableStateOf(false) }
TextButton(onClick = { showFullScreenSomething = true }) {
Text("Show full screen content")
}
if (showFullScreenSomething) {
FullScreen {
Box(
Modifier
.fillMaxSize()
.background(Color.Green)
) {
Text("Full screen text", Modifier.align(Alignment.Center))
TextButton(onClick = { showFullScreenSomething = false }) {
Text("Close")
}
}
}
}
}
The yellow box has set some constraints, which would prevent the Composables from inside to draw outside its bounds.
Using the Dialog composable, I have been able to get a proper fullscreen Composable in any nested one. It's quicker and easier than some of other answers.
Dialog(
onDismissRequest = { /* Do something when back button pressed */ },
properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = false, usePlatformDefaultWidth = false)
){
/* Your full screen content */
}
If I understand correctly you just don't want to navigate anywhere. Id something like this.
when (val viewType = viewModel.viewTypeGallery.get()) {
is GalleryViewModel.GalleryViewType.Gallery -> {
Gallery(viewModel, scope, installId, filePathModifier, fragment, setImageUploadType)
}
is GalleryViewModel.GalleryViewType.ImageViewer -> {
Row(Modifier.fillMaxWidth()) {
Image(
modifier = Modifier
.fillMaxSize(),
painter = rememberCoilPainter(viewType.imgUrl),
contentScale = ContentScale.Crop,
contentDescription = null
)
}
}
}
I just keep track of what type the view is meant to be. In my case I'm not displaying a dialog I'm removing my entire gallery and showing an image instead.
Alternatively you could just have an if(viewImage) condition below your call your and layer the 'dialog' on top of it.
After notice that, at least for now, we don't have any Composable to do "easy" fullscreen, I decided to implement mine one, mostly based on ideas from #foxtrotuniform6969 and #ntoskrnl. Also, I tried to do it most possible without to use platform dependent functions then I think this is very suiteable to Desktop/Android.
You can check the basic implementation in this GitHub repository.
By the way, the implementation idea was just:
Create a composable to wrap the target composables tree that can call an FullScreen composable;
Retrieve the full screen dimensions/size from a auxiliary Box matched to the root screen size using the .onGloballyPositioned() modifier;
Store the full screen size and all FullScreen composables created in the tree onto appropriated compositionLocalOf instances (see documentation).
I tried to use this in a Desktop project and seems to be working, however I didn't tested in Android yet. The repository also contains a example.
Feel free to navigate in the repository and sent a pull request if you can. :)