I am writing some pager code in jetpack compose and came to a situation where I need to change page number by button click. This is my event on button click:
onClick = {pagerState.scrollToPage(page=currentPager+1)}
but when I do this I get this error: Suspend function 'scrollToPage' should be called only from a coroutine or another suspend function
I got a solution to this by adding:
onClick = {GlobalScope.launch (Dispatchers.Main) {pagerState.scrollToPage(page=currentPager+1)}}
but still GlobalScope.launch is not recommended. Above onClick are called inside basic compose functions. How can I fix this issue in jetpack compose?
Go through this documentation : Accompanist Pager
Here is a raw code: Raw code for scroll to page
If you want to jump to a specific page, you either call call pagerState.scrollToPage(index) or pagerState.animateScrollToPage(index) method in a CoroutineScope:
val pagerState = rememberPagerState()
val scope = rememberCoroutineScope()
HorizontalPager(count = 10, state = pagerState) { page ->
// ...page content
}
// Later, scroll to page 2
scope.launch {
pagerState.scrollToPage(2)
}
You should use the code below to create a coroutines scope in your composable.
val coroutinesScope = rememberCoroutineScope()
Note that you can only call this inside a composable so you cannot create coroutinesScope inside your onClick() and have to initialize it on the top of your composable.
Related
Consider snippet below
fun doSomething(){
}
#Composable
fun A() {
Column() {
val counter = remember { mutableStateOf(0) }
B {
doSomething()
}
Button(onClick = { counter.value += 1 }) {
Text("Click me 2")
}
Text(text = "count: ${counter.value}")
}
}
#Composable
fun B(onClick: () -> Unit) {
Button(onClick = onClick) {
Text("click me")
}
}
Now when pressing "click me 2" button the B compose function will get recomposed although nothing inside it is got changed.
Clarification: doSomething is for demonstration purposes. If you insist on having a practical example you can consider below usage of B:
B{
coroutinScope.launch{
bottomSheetState.collapse()
doSomething()
}
}
My questions:
Why this lamda function causes recomposition
Best ways to fix it
My understanding of this problem
From compose compiler report I can see B is an skippable function and the input onClick is stable. At first I though its because lambda function is recreated on every recomposition of A and it is different to previous one. And this difference cause recomposition of B. But it's not true because if I use something else inside the lambda function, like changing a state, it won't cause recomposition.
My solutions
use delegates if possible. Like viewmode::doSomething or ::doSomething. Unfortunately its not always possible.
Use lambda function inside remember:
val action = remember{
{
doSomething()
}
}
B(action)
It seems ugly =)
3. Combinations of above.
When you click the Button "Click me 2" the A composable is recomposed because of Text(text = "count: ${counter.value}"). It happens because it recompose the scope that are reading the values that can change.
If you are using something like:
B {
Log.i("TAG","xxxx")
}
the B composable is NOT recomposed clicking the Button "Click me 2".
If you are using
B{
coroutinScope.launch{
Log.i("TAG","xxxx")
}
}
the B composable is recomposed.
When a State is read it triggers recomposition in nearest scope. And a scope is a function that is not marked with inline and returns Unit.
To use a coroutinScope you have to use rememberCoroutineScope that is a composable inline function. The the body of inline composable functions are simply copied into their call sites, such functions do not get their own recompose scopes.
To avoid it you can use:
B {
Log.i("TAG","xxxx")
}
and
#Composable
fun B(onClick: () -> Unit) {
val scope = rememberCoroutineScope()
Button(
onClick = {
scope.launch {
onClick()
}
}
) {
Text(
"click me ",
)
}
}
Sources and credits:
Thracian's answer: Jetpack Compose Smart Recomposition
What is “donut-hole skipping” in Jetpack Compose? post: https://www.jetpackcompose.app/articles/donut-hole-skipping-in-jetpack-compose
scoped recomposition: https://dev.to/zachklipp/scoped-recomposition-jetpack-compose-what-happens-when-state-changes-l78
You can use the LogCompositions composable described in the 2nd post to check the recomposition in your code.
Generally speaking, if you are using a property inside a lambda function that is unstable, it causes the child compose function unskippable and thus gets recomposed every time its parent gets recomposed. This is not something easily visible and you need to be careful with it. For example, the bellow code will cause B to get recomposed because coroutinScope is an unstable property and we are using it as an indirect input to our lambda function.
fun A(){
...
val coroutinScope = rememberCoroutineScope()
B{
coroutineScope.launch {
doSomething()
}
}
}
To bypass this you need to use remember around your lambda or delegation (:: operator). There is a note inside this video about it. around 40:05
There are many other parameters that are unstable like context. To figure them out you need to use compose compiler report.
Here is a good explanation about the why: https://multithreaded.stitchfix.com/blog/2022/08/05/jetpack-compose-recomposition/
I want to send a channel value from viewmodel and collect it in MainActivity for every compose component screen.In this context I found a solution like this.
I defined a channel in my core viewmodel like below
val snackBarErrorMessageChannel = Channel<SnackbarMessageEvent>()
val snackBarErrorMessage = snackBarErrorMessageChannel.receiveAsFlow()
Then I collect it in MainActivity onCreate method in setContent like below
LaunchedEffect(key1 = Unit) {
viewModel.snackBarErrorMessage.collect {
kutuphanemAppState.showSnackbar(
it.message,
it.duration,
it.type
)
}
}
When I collect it in my screen snackbar is showing but when I want to call for all compose screen it is not trigger. Is there any wrong? Where can I collect for all my compose screen?
so I wasted a good couple of days on this, and my deadline's tomorrow
basically, I have a mutableLiveData var which is a companion object,
and when the data is updated it calls the observe function inside the grid.
The observe function gets called fine, it does the print statements which you can see,
but it completely skips everything in the compose "items()" method.
Can someone please help me? I could not find anything useful online...
#ExperimentalFoundationApi
#Composable
fun ItemsGrid() {
LazyVerticalGrid(
cells = GridCells.Fixed(3),
contentPadding = PaddingValues(8.dp)
) {
mProductsByCategoryID.observe(viewLifecycleOwner,
{ products ->
println("DATA CHANGGED ${products[0].name}")
println(mProductsByCategoryID.value?.get(0)?.name)
items(products) {
println("INSIDE ITEMS for products ${it.name}") // never gets inside of here except for the first time the view loads
DemoCards(demo = it)
}
}
)
}
}
In Compose you shouldn't observe LiveData directly inside a #Composable, but observe it as State. Instead of callbacks to update UI, now we have Recomposition (#Composable function getting called automatically over and over again every time an observed value in the #Composable function changes).
More info here
Your code should look more like:
#Composable
fun ItemsGrid() {
val productsByCategory = mProductsByCategoryID.observeAsState()
LazyVerticalGrid(
cells = GridCells.Fixed(3),
contentPadding = PaddingValues(8.dp)
) {
//here goes code that does what your callback was doing before.
//when val productsByCategory.value changes, ItemsGrid()
//gets recomposed (called again) and this code will execute
}
}
Currently I'm using compose with navigation and viewmodels.
The code of my NavHost is the following
composable(MyRoute.name + "/{param}") { backStackEntry ->
val param = backStackEntry.arguments?.getString("id") ?: ""
val viewModel = hiltViewModel<MyViewModel>()
MyComposable(
viewModel = viewModel
)
}
The issue I'm facing is that viewModel.init is called an infinite number of times (I guess it is recomposition), but the viewModel is supposed to have only one instance that outlives the lifecycle of the composables.
Use a LaunchedEffect to run your network call.
See this for reference.
I want to use a LaunchEffect to the AndroidView for collecting data from the ViewModel Stateflow but I get an error. how can I fix it?
You can run the LaunchedEffect only inside a #Composable function, this means that your lamba should me annotated with #Composable () -> Unit in order to be compatibile. But I'm not pretty sure that is a good practice.
You are trying to use composable LaunchedEffect not inside composable scope.
Move launched effect outside of getMapAsync.
You can do something like.
#Composable
fun MapViewContainer {
...
var mapIsReady by remember { mutableStateOf(false) }
...
mapView.getMapAsync {
mapIsReady = true
...
}
...
if (mapIsReady) {
// do some compose things
}
}
}