Screenshot
I just wanna click button can log ComposeableB().or liek this , For example, if you click ComposableA, ComposableB will start an animation instead of updating the data.
Although with Compose it is generally recommended to pass events to the app logic (like the ViewModel) instead of to the app UI (Thinking in Compose), here's how your code could look like if you really need to do that:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
#Composable
fun ComposableA() {
var addLogEntry by remember { mutableStateOf(false) }
Column {
Button(onClick = {
addLogEntry = true
}) {
Text(text = "Log")
}
ComposableB(addLogEntry = addLogEntry) {
addLogEntry = false
}
}
}
#Composable
fun ComposableB(
addLogEntry: Boolean,
onLogEntryAdded: () -> Unit
) {
if (addLogEntry) {
Log.d("Shadowmeld", "onAddLogEntry")
onLogEntryAdded()
}
}
Here you are passing a function as a second parameter (onLogEntryAdded, in lambda expression format) to ComposableB. This passed lambda expression will be called from ComposableB to modify state in ComposableA.
I believe there are better ways of doing this, like ComposableB being declared inside ComposableA (hoisting state to ComposableA) or, if that is not an option, passing the Button onClick event to a ViewModel that both ComposableA and ComposableB can observe.
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/
Please see my code:
#Composable
fun RecomposeLambdaTest() {
var state by remember {
mutableStateOf("1")
}
val stateHolder = remember {
StateHolder()
}
Column {
Button(onClick = {
state += "1"
}) {
Text(text = "change the state")
}
OuterComposable(state = state) {
stateHolder// just a reference to the instance outer the scope
}
}
}
#Composable
fun OuterComposable(state: String, onClick: () -> Unit) {
LogUtil.d("lambda hashcode: ${onClick.hashCode()}")
Column {
Text(text = state)
Button(onClick = onClick) {
Log.d("Jeck", "compose 2")
Text(text = "Text")
}
}
}
//#Stable
class StateHolder{
private var b = 2
}
Every time I click button, OuterComposable recompose, and log the lambda hashcode——always different! It means that a new lambda instance is created when recompose, everytime
and I uncomment the code in StateHolder and make it look like:
#Stable
class StateHolder{
private var b = 2
}
Every time I click button, OuterComposable recompose, and log the lambda hashcode——always the same! It means that when recompose, Composer reuse the lambda
So what' s under the hood?
Edit:
Ok, make it easier, Let's change the code like this:
val stateHolder = remember {
2
}
the result is lambda is reused.
make val to var, the lambda is created when every recompose.
So I think I know that: If the lambda refenence a valuable outer scope and the valuable is not stable, recreate lambda every time.
So the question is:
Why Compose compiler do this?
Why Compiler think the StateHolder before is not stable, it only contains a private var!?
An author met the same question, here is his article——6 Jetpack Compose Guidelines to Optimize Your App Performance
He said, private property still affact stability, it seems it is a Google team's choice.
I am new to Compose and Kotlin. I have an application using a Room database. In the frontend, there is a Composable containing an Icon Composable. I want the Icon resource to be set depending on the result of a database operation that is executed within a suspend function.
My Composable looks like this:
#Composable
fun MoviePreview(movie : ApiMoviePreview, viewModel: ApiMovieViewModel) {
Card(
modifier = ...
) {
Row(
modifier = ...
) {
IconButton(
onClick = {
//...
}) {
Icon(
imageVector =
// This code does not work, as isMovieOnWatchList() is a suspend function and cannot be called directly
if (viewModel.isMovieOnWatchlist(movie.id)) {
Icons.Outlined.BookmarkAdded
} else {
Icons.Filled.Add
}
,
contentDescription = stringResource(id = R.string.addToWatchlist)
)
}
}
}
}
The function that I need to call is a suspend function, because Room requires its database operations to happen on a seperate thread. The function isMovieOnWatchlist() looks like this:
suspend fun isMovieOnWatchlist(id: Long) {
return movieRepository.isMovieOnWatchlist(id)
}
What would the appropriate way be to achieve the desired behaviour? I already stumbled across Coroutines, but the problem is that there seems to be no way to just return a value out of the coroutine function.
A better approach would be to prepare the data so everything you need is in the data/value class rather than performing live lookup per row/item which is not very efficient. I assume you have 2 tables and you'd probably want a LEFT JOIN however all these details are not included.
With Room it even includes implementations that use the Flow api, meaning it will observe the data when information in either table changes and re-runs the original query to provide you with the new changed dataset.
However this is out of scope of your original question but should you want to explore this then here is a good start : https://developer.android.com/codelabs/basic-android-kotlin-training-intro-room-flow#0
To your original question. This is likely achievable with a LaunchedEffect and some observed MutableState<ImageVector?> object within the composable, something like:
#Composable
fun MoviePreview(
movie: ApiMoviePreview,
viewModel: ApiMovieViewModel
) {
var icon by remember { mutableStateOf<ImageVector?>(value = null) } // null or default icon until update by result below
Card {
Row {
IconButton(onClick = {}) {
icon?.run {
Icon(
imageVector = this,
contentDescription = stringResource(id = R.string.addToWatchlist))
}
}
}
}
LaunchedEffect(Unit) {
// execute suspending function in provided scope closure and update icon state value once complete
icon = if (viewModel.isMovieOnWatchlist(movie.id)) {
Icons.Outlined.BookmarkAdded
} else Icons.Filled.Add
}
}
The following code is from the project.
I find that fun MainScreen() add #Composable, and fun launchDetailsActivity doesn't add #Composable.
It make me confused. I think all function which apply to Compose should to add #Composable, why doesn't fun launchDetailsActivity add #Composable?
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
setContent {
ProvideWindowInsets {
ProvideImageLoader {
CraneTheme {
MainScreen(
onExploreItemClicked = { launchDetailsActivity(context = this, item = it) }
)
}
}
}
}
}
}
#Composable
fun MainScreen(onExploreItemClicked: OnExploreItemClicked) {
...
}
fun launchDetailsActivity(context: Context, item: ExploreModel) {
context.startActivity(createDetailsActivityIntent(context, item))
}
Function with #Composable is not just a function it tells the compose compiler that this is a UI element. Take this data and build a Widget with it.
So you have to determine when you will add #Composable based on whether this function draws something in the UI or not. In the non compose world you can think of this function like a View.
For example, this function takes a parameter name and builds a Text widget with the text "Hello $name" which you can see in the UI.
#Composable
fun Greeting(name: String) {
Text("Hello $name")
}
But
fun getName(firstName: String, lastName: String): String {
return "$firstName $lastName"
}
This function is not a composable function. It is not annotated with #Composable because it is not a widget, it shouldn't render anything in the UI. It just takes two parameters, Concatenates them, and returns the String.
In your case, MainScreen is the function that is rendering the Main screen of your app so it is a UI element. But function launchDetailsActivity doesn't draw anything in the UI. It just navigates from one activity to another activity.
Few things to remember:
Function with #Composable doesn't return anything.
You can't call a composable function from a non-composable function.
Unlike non-composable function composable function start with an Uppercase letter.
You can read this doc for details https://developer.android.com/jetpack/compose/mental-model
You need to mark view builder functions with #Composable, to be directly called from an other #Composable.
If you have a side effect function, it shouldn't be called directly from composable. It can be called from touch handlers, like click in your example, or using a side effect, like LaunchedEffect. Check out more about what's side effect in documentation.
#Composable
fun SomeView() {
// this is directly called from view builder and should be marked with #Composable
OtherView()
LaunchedEffect(Unit) {
// LaunchedEffect a side effect function, and as it's called
// from LaunchedEffect it can be a suspend fun (optionally)
handleLaunchedEffect()
}
Button(onClick = { handleButtonClick() }) { // or onClick = ::handleButtonClick
}
}
#Composable
fun OtherView() {
}
suspend fun handleLaunchedEffect() {
}
fun handleButtonClick() {
}
You should use #Composable if you are using calling another function annotated with #Composable that's it pretty simple.
Generally all #Composable functions starts with uppercase letter but some also start with lowercase like everything that starts with remember
So when you are using these functions inside another function you need to use #Composable else even android studio will yell at you because composable function can be invoked from another composable.
I'd like to know how I can change variables INSIDE a Composable via a method.
Of course i can do something like:
var test: String by remember { mutableStateOf("hello") }
and can change it like I want to, like (although it is a stupid example)
[stuff...].pointerInput(Unit) {
detectDragGestures(...)
{ change, dragAmount ->
test=dragamount.toString()
}
}
but how would i change the variable with some complicated method e.g.
[stuff...].pointerInput(Unit) {
detectDragGestures(...)
{ change, dragAmount ->
changeText(dragAmount)
}
}
I can only use methods outside of the composable to assign it to the value, aka
test=getMyNewTest(dragAmount)
But how can i change my 'fields' inside a composable, so that i can modify 'test' directly in my method?
If the method can see the a mutable variable (i.e. share a lexical scope) it can change it. For changeText to be able to change test, it must either have test in scope or receive a mutable reference to test as a parameter. Compose doesn't change this.
Any answer to this question outside of compose (e.g. class scopes, modules scopes, global scopes, closure capture, reference passing, etc.) works in compose as well.
For example, you could define the function as local to the composable function as,
#Composable
fun Example() {
var test by remember { mutableStateOf("hello") }
fun changeText(amount: Float) {
test = amount.toString()
}
...
[stuff...].pointerInput(Unit) {
detectDragGestures(...)
{ change, dragAmount ->
changeText(dragAmount)
}
}
...
}