I saw that a new parameter has recently been added to the Material3 Top App Bar Composables on Jetpack Compose:
fun CenterAlignedTopAppBar(
...
scrollBehavior: TopAppBarScrollBehavior? = null
) {}
What I understood from the documentation is that this should enable us to implement the behaviour that the app bar hides at scrolling the content. However, I did not manage to implement this, as the only example I found on StackOverflow seems to no longer work on the latest version of Jetpack Compose and is giving the error No value passed for parameter 'state'.
Can anybody provide an example? What I want to achieve is a Scaffold, where as topBar a CenterAlignedTopAppBar is provided, that scrolls out on top of the screen if the scrollable content of the Scaffold is scrolled.
Thanks a lot for your help.
I finally managed to get it to work:
val topAppBarScrollState: TopAppBarScrollState = rememberTopAppBarScrollState()
val scrollBehavior = remember { TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarScrollState) }
CenterAlignedTopAppBar(
modifier = modifier,
title = { Text(text = stringResource(id = titleResource)) },
actions = {
IconButton(
onClick = { }
) {
Icon(
imageVector = Icons.Filled.Settings,
contentDescription = null,
)
}
},
scrollBehavior = scrollBehavior
)
This only seems to be necessary since Compose version 1.2.0-rc02 though, as on older versions the solution in the post linked in my answer still works.
Related
I am trying to show a list of installed apps and a checkbox next to each app. The scrolling is terrible, it's noticably laggy. It looks like i am doing something wrong but i can't figure it out. how can I improve it?
#Composable
fun AppList(infoList: MutableList<android.content.pm.ResolveInfo>) {
val ctx = LocalContext.current
LazyColumn {
items(infoList) { info ->
var isChecked by remember { mutableStateOf(false) }
var icon by remember { mutableStateOf(info.loadIcon(ctx.packageManager)) }
var label by remember { mutableStateOf( info.loadLabel(ctx.packageManager).toString()) }
Row(
verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()
) {
Image(
painter = rememberImagePainterMine(icon),
contentDescription = label,
contentScale = ContentScale.Fit,
modifier = Modifier.padding(16.dp)
)
// Display the app name
Text(
text =label,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.weight(1f)
)
// Display the checkbox on the right
Box(modifier = Modifier.clickable { isChecked = !isChecked }) {
Checkbox(
checked = isChecked,
onCheckedChange = { isChecked = it },
modifier = Modifier.padding(16.dp)
)
}
}
}
}
}
#Composable
fun rememberImagePainterMine(drawable: Drawable): Painter = remember(drawable) {
object : Painter() {
override val intrinsicSize: Size
get() = Size(drawable.intrinsicWidth.toFloat(), drawable.intrinsicHeight.toFloat())
override fun DrawScope.onDraw() {
drawable.setBounds(0, 0, size.width.toInt(), size.height.toInt())
drawable.draw(this.drawContext.canvas.nativeCanvas)
}
}
}
Are you running your app in release mode? It makes a big difference as to how smooth Jetpack Compose will be (due to the amount of debug code under the hood).
Google themselves explain it as follows:
If you're finding performance issues, make sure to try running your app in release mode. Debug mode is useful for spotting lots of problems, but it imposes a significant performance cost, and can make it hard to spot other code issues that might be hurting performance. You should also use the R8 compiler to remove unnecessary code from your app. By default, building in release mode automatically uses the R8 compiler.
Additionally, you should consider using keys with your LazyColumn to help Compose figure out if your data moves around or changes completely.
Without your help, Compose doesn't realize that unchanged items are just being moved in the list. Instead, Compose thinks the old "item 2" was deleted and a new one was created, and so on for item 3, item 4, and all the way down. The result is that Compose recomposes every item on the list, even though only one of them actually changed.
The solution here is to provide item keys. Providing a stable key for each item lets Compose avoid unnecessary recompositions. In this case, Compose can see that the item now at spot 3 is the same item that used to be at spot 2. Since none of the data for that item has changed, Compose doesn't have to recompose it.
A "genius" designer wants a screen with 2 app-bars and different scroll behaviors. The top one should appear only when user scrolls till the begin of the screen, the second one should appear even when user scrolls back a little. I have attached a picture with illustration of 3 states I want to achieve using Jetpack-Compose.
I wanted smth like following code (here I've used androidx.compose.material3):
val topAppBarState = rememberTopAppBarState()
val scrollOnlyInTopBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(topAppBarState)
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarState)
Scaffold(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection)
.nestedScroll(scrollOnlyInTopBehavior.nestedScrollConnection),
topBar = {
Column {
SearchBar(...)
CenterAlignedTopAppBar(
title = { Text(text = "Slide-out only in top") },
scrollBehavior = scrollOnlyInTopBehavior,
)
CenterAlignedTopAppBar(
title = { Text(text = "Slide-out event when scrolling back a little") },
scrollBehavior = scrollBehavior,
)
}
}
) {
LazyVerticalGrid(columns = Fixed(2), ...)
}
But obviously it doesn't work because Modifier.nestedScroll of the Scaffold can have only one nestedScrollConnection. Please suggest me some workaround with Compose to implement this screen.
Update:
I was wrong, it's possible to correctly setup several `nestedScroll` modifiers for single compose container, here I just did it in wrong way.
I just found mistake in the code above. I used single TopAppBarState for different Behavior-s, that's why they act similarly, and I wasn't able to achieve desired scroll effect. The effect reaches by using different TopAppBarState for each individual Behavior. Example below should work correctly:
val scrollOnlyInTopBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
Scaffold(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection)
.nestedScroll(scrollOnlyInTopBehavior.nestedScrollConnection),
topBar = {
Column {
SearchBar(...)
CenterAlignedTopAppBar(
title = { Text(text = "Slide-out only in top") },
scrollBehavior = scrollOnlyInTopBehavior,
)
CenterAlignedTopAppBar(
title = { Text(text = "Slide-out event when scrolling back a little") },
scrollBehavior = scrollBehavior,
)
}
}
) {
LazyVerticalGrid(columns = Fixed(2), ...)
}
I was trying to create a sample Tab View in Jetpack compose, so the structure will be like
Inside a Parent TabRow we are iterating the tab title and create Tab composable.
More precise code will be like this.
#OptIn(ExperimentalPagerApi::class)
#Composable
private fun MainApp() {
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.app_name)) },
backgroundColor = MaterialTheme.colors.surface
)
},
modifier = Modifier.fillMaxSize()
) { padding ->
Column(Modifier.fillMaxSize().padding(padding)) {
val pagerState = rememberPagerState()
val coroutineScope = rememberCoroutineScope()
val tabContents = listOf(
"Home" to Icons.Filled.Home,
"Search" to Icons.Filled.Search,
"Settings" to Icons.Filled.Settings
)
HorizontalPager(
count = tabContents.size,
state = pagerState,
contentPadding = PaddingValues(horizontal = 32.dp),
modifier = Modifier
.weight(1f)
.fillMaxWidth()
) { page ->
PagerSampleItem(
page = page
)
}
TabRow(
selectedTabIndex = pagerState.currentPage,
backgroundColor = MaterialTheme.colors.surface,
contentColor = MaterialTheme.colors.onSurface,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier
.pagerTabIndicatorOffset(pagerState, tabPositions)
.height(4.dp)
.background(
color = Color.Green,
shape = RectangleShape
)
)
}
) {
tabContents.forEachIndexed { index, pair: Pair<String, ImageVector> ->
Tab(
selected = pagerState.currentPage == index,
selectedContentColor = Color.Green,
unselectedContentColor = Color.Gray,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
},
text = { Text(text = pair.first) },
icon = { Icon(imageVector = pair.second, contentDescription = null) }
)
}
}
}
}
}
#Composable
internal fun PagerSampleItem(
page: Int
) {
// Displays the page index
Text(
text = page.toString(),
modifier = Modifier
.padding(16.dp)
.background(MaterialTheme.colors.surface, RoundedCornerShape(4.dp))
.sizeIn(minWidth = 40.dp, minHeight = 40.dp)
.padding(8.dp)
.wrapContentSize(Alignment.Center)
)
}
And coming to my question is whenever we click on the tab item, the inner content get recompose so weirdly. Im not able to understand why it is happens.
Am attaching an image of the recomposition counts below, please take a look that too, it would be good if you guys can help me more for understand this, also for future developers.
There are two question we have to resolve in this stage
Whether it will create any performance issue, when the view getting more complex
How to resolve this recompostion issue
Thanks alot.
… whenever we click on the tab item, the
inner content get recompose so weirdly. Im not able to understand why
it is happens...
It's hard to determine what this "weirdness" is, there could be something inside the composable your'e mentioning here.
You also didn't specify what the API is, so I copied and pasted your code and integrated accompanist view pager, then I was able to run it though not on an Android Studio with a re-composition count feature.
And since your'e only concerned about the Text and the Icon parameter of the API, I think that's something out of your control. I suspect the reason why your'e getting those number of re-composition count is because your'e animating the page switching.
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
Though 'm not able to try this on another Android Studio version with the re-composition feature, I think (though I'm not sure) scrolling to another page without animation will yield less re-composition count.
coroutineScope.launch {
pagerState.scrollToPage(index)
}
If it still bothers you, the best course of action is to ask them directly, though personally I wouldn't concerned much about this as they are part of an accepted API and its just Text and Icon being re-composed many times by an animation which is also fine IMO.
Now if you have some concerns about your PagerSampleItem stability(which you have a full control), based on the provided code and screenshot, I think your'e fine.
There's actually a feature suggested from this article to check the stability of a composable, I run it and I got this report.
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun PagerSampleItem(
stable page: Int
)
Everything about this report is within the article I linked.
Also, your Text and Icon are using String and ImageVector which is stable and immutable (marked by #Immutable) respectively.
So TLDR, IMO your code is fine, your PagerSampleItem is not re-composing in the screenshot.
I am developing an app widget with compose. I have a problem with getting string from resources. If I give text as a string like text = "Label", it is working properly. But if I give with stringResource, text is not shown, and I see this problem. Is there anyone who has faced this issue? Thank you
E/GlanceAppWidget: Error in Glance App Widget
java.lang.IllegalStateException: CompositionLocal LocalConfiguration not present
at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt.noLocalProvidedFor(AndroidCompositionLocals.android.kt:167)
at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt.access$noLocalProvidedFor(AndroidCompositionLocals.android.kt:1)
at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$LocalConfiguration$1.invoke(AndroidCompositionLocals.android.kt:47)
at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$LocalConfiguration$1.invoke(AndroidCompositionLocals.android.kt:44)
at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74)
at androidx.compose.runtime.LazyValueHolder.getCurrent(ValueHolders.kt:29)
at androidx.compose.runtime.LazyValueHolder.getValue(ValueHolders.kt:31)
at androidx.compose.runtime.ComposerImpl.resolveCompositionLocal(Composer.kt:1776)
at androidx.compose.runtime.ComposerImpl.consume(Composer.kt:1746)
at androidx.compose.ui.res.StringResources_androidKt.resources(StringResources.android.kt:78)
at androidx.compose.ui.res.StringResources_androidKt.stringResource(StringResources.android.kt:36)
class MarketWidget : GlanceAppWidget() {
#Composable
override fun Content() {
Column(
modifier = GlanceModifier
.fillMaxSize()
.background(color = Color.White)
.padding(8.dp)
) {
Text(
text = stringResource(id = R.string.app_name)
)
}
}
}
stringResource is part of Compose UI, so you can't use it in Glance. Glance's Text only takes resolved strings, you can retrieve it using LocalContext.current.
To change your example, this should work:
Text(text = LocalContext.current.getString(R.string.app_name))
Also, ensure that you're importing the Glance Text (androidx.glance.text.Text) and not the one from Material Compose UI (androidx.compose.material.Text)
I'm building Android app with Jetpack Compose. Got stuck while trying to implement BottomAppBar with BottomDrawer pattern.
Bottom navigation drawers are modal drawers that are anchored to the bottom of the screen instead of the left or right edge. They are only used with bottom app bars. These drawers open upon tapping the navigation menu icon in the bottom app bar.
Description on material.io, and direct link to video.
I've tried using Scaffold, but it only supports side drawer. BottomDrawer appended to Scaffold content is displayed in content area and BottomDrawer doesn't cover BottomAppBar when open. Moving BottomDrawer after Scaffold function doesn't help either: BottomAppBar is covered by some invisible block and prevents clicking buttons.
I've also tried using BottomSheetScaffold, but it doesn't have BottomAppBar slot.
If Scaffold doesn't support this pattern, what would be correct way to implement it? Is it possible to extend Scaffold component? I fear that incorrect implementation from scratch might create issues later, when I'll try to implement navigation and snackbar.
I think the latest version of scaffold does have a bottom app bar parameter
They (Google Devs) invite you in the Jetpack Compose Layouts pathway to try adding other Material Design Components such as BottomNavigation or BottomDrawer to their respective Scaffold slots, and yet do not give you the solution.
BottomAppBar does have its own slot in Scaffold (i.e. bottomBar), but BottomDrawer does not - and seems to be designed exclusively for use with the BottomAppBar explicitly (see API documentation for the BottomDrawer).
At this point in the Jetpack Compose pathway, we've covered state hoisting, slots, modifiers, and have been thoroughly explained that from time to time we'll have to play around to see how best to stack and organize Composables - that they almost always have a naturally expressable way in which they work best that is practically intended.
Let me get us set up so that we are on the same page:
class MainActivity : ComponentActivity() {
#ExperimentalMaterialApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LayoutsCodelabTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
LayoutsCodelab()
}
}
}
}
}
That's the main activity calling our primary/core Composable. This is just like in the codelab with the exception of the #ExperimentalMaterialApi annotation.
Next is our primary/core Composable:
#ExperimentalMaterialApi
#Composable
fun LayoutsCodelab() {
val ( gesturesEnabled, toggleGesturesEnabled ) = remember { mutableStateOf( true ) }
val scope = rememberCoroutineScope()
val drawerState = rememberBottomDrawerState( BottomDrawerValue.Closed )
// BottomDrawer has to be the true core of our layout
BottomDrawer(
gesturesEnabled = gesturesEnabled,
drawerState = drawerState,
drawerContent = {
Button(
modifier = Modifier.align( Alignment.CenterHorizontally ).padding( top = 16.dp ),
onClick = { scope.launch { drawerState.close() } },
content = { Text( "Close Drawer" ) }
)
LazyColumn {
items( 25 ) {
ListItem(
text = { Text( "Item $it" ) },
icon = {
Icon(
Icons.Default.Favorite,
contentDescription = "Localized description"
)
}
)
}
}
},
// The API describes this member as "the content of the
// rest of the UI"
content = {
// So let's place the Scaffold here
Scaffold(
topBar = {
AppBarContent()
},
//drawerContent = { BottomBar() } // <-- Will implement a side drawer
bottomBar = {
BottomBarContent(
coroutineScope = scope,
drawerState = drawerState
)
},
) {
innerPadding ->
BodyContent( Modifier.padding( innerPadding ).fillMaxHeight() )
}
}
)
}
Here, we've leveraged the Scaffold exactly as the codelab in the compose pathway suggests we should. Notice my comment that drawerContent is an auto-implementation of the side-drawer. It's a rather nifty way to bypass directly using the [respective] Composable(s) (material design's modal drawer/sheet)! However, it won't work for our BottomDrawer. I think the API is experimental for BottomDrawer, because they'll be making changes to add support for it to Composables like Scaffold in the future.
I base that on how difficult it is to use the BottomDrawer, designed for use solely with BottomAppBar, with the Scaffold - which explicitly contains a slot for BottomAppBar.
To support BottomDrawer, we have to understand that it is an underlying layout controller that wraps the entire app's UI, preventing interaction with anything but its drawerContent when the drawer is open. This requires that it encompasses Scaffold, and that requires that we delegate necessary state control - to the BottomBarContent composable which wraps our BottomAppBar implementation:
#ExperimentalMaterialApi
#Composable
fun BottomBarContent( modifier: Modifier = Modifier, coroutineScope: CoroutineScope, drawerState: BottomDrawerState ) {
BottomAppBar{
// Leading icons should typically have a high content alpha
CompositionLocalProvider( LocalContentAlpha provides ContentAlpha.high ) {
IconButton(
onClick = {
coroutineScope.launch { drawerState.open() }
}
) {
Icon( Icons.Filled.Menu, contentDescription = "Localized description" )
}
}
// The actions should be at the end of the BottomAppBar. They use the default medium
// content alpha provided by BottomAppBar
Spacer( Modifier.weight( 1f, true ) )
IconButton( onClick = { /* doSomething() */ } ) {
Icon( Icons.Filled.Favorite, contentDescription = "Localized description" )
}
IconButton( onClick = { /* doSomething() */ } ) {
Icon( Icons.Filled.Favorite, contentDescription = "Localized description" )
}
}
}
The result shows us:
The TopAppBar at top,
The BottomAppBar at bottom,
Clicking the menu icon in the BottomAppBar opens our BottomDrawer, covering the BottomAppBar and entire content space appropriately while open.
The BottomDrawer is properly hidden, until either the above referenced button click - or gesture - is utilized to open the bottom drawer.
The menu icon in the BottomAppBar opens the drawer partway.
Gesture opens the bottom drawer partway with a quick short swipe, but as far as you guide it to otherwise.
You may have to do something as shown below..
Notice how the Scaffold is called inside the BottomDrawer().
It's confusing though how the documentation says "They (BottomDrawer) are only used with bottom app bars". It made me think I have to look for a BottomDrawer() slot inside Scaffold or that I have to call BottomDrawer() inside BottomAppBar(). In both cases, I experienced weird behaviours. This is how I worked around the issue. I hope it helps someone especially if you are attempting the code lab exercise in Module 5 of Layouts in Jetpack Compose from the Jetpack Compose course.
#ExperimentalMaterialApi
#Composable
fun MyApp() {
var selectedItem by rememberSaveable { mutableStateOf(1)}
BottomDrawer(
modifier = Modifier.background(MaterialTheme.colors.onPrimary),
drawerShape = Shapes.medium,
drawerContent = {
Column(Modifier.fillMaxWidth()) {
for(i in 1..6) {
when (i) {
1 -> Row(modifier = Modifier.clickable { }.padding(16.dp)){
Icon(imageVector = Icons.Rounded.Inbox, contentDescription = null)
Text(text = "Inbox")
}
2 -> Row(modifier = Modifier.clickable { }.padding(16.dp)){
Icon(imageVector = Icons.Rounded.Outbox, contentDescription = null)
Text(text = "Outbox")
}
3 -> Row(modifier = Modifier.clickable { }.padding(16.dp)){
Icon(imageVector = Icons.Rounded.Archive, contentDescription = null)
Text(text = "Archive")
}
}
}
}
},
gesturesEnabled = true
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "Learning Compose Layouts" )
},
actions = {
IconButton(onClick = { /*TODO*/ }) {
Icon(Icons.Filled.Favorite, contentDescription = null)
}
}
)
},
bottomBar = { BottomAppBar(cutoutShape = CircleShape, contentPadding = PaddingValues(0.dp)) {
for (item in 1..4) {
BottomNavigationItem(
modifier = Modifier.clipToBounds(),
selected = selectedItem == item ,
onClick = { selectedItem = item },
icon = {
when (item) {
1 -> { Icon(Icons.Rounded.MusicNote, contentDescription = null) }
2 -> { Icon(Icons.Rounded.BookmarkAdd, contentDescription = null) }
3 -> { Icon(Icons.Rounded.SportsBasketball, contentDescription = null) }
4 -> { Icon(Icons.Rounded.ShoppingCart, contentDescription = null) }
}
}
)
}
}
}
) { innerPadding -> BodyContent(
Modifier
.padding(innerPadding)
.padding(8.dp))
}
}
}