1

I'm using java.time for some part of my app's functionality and I'm getting re-compositions unexpectedly. Consider the codes below with some Java classes.

RootJavaClass

public class RootJavaClass {
     public static AnotherJavaClass getNewInstance() {
         return new AnotherJavaClass("Hello There");
     }
}

AnotherJavaClass

public class AnotherJavaClass {
     public String data = "";
     public AnotherJavaClass(String param) {
         data = param;
     }
 }

My Parent Scope Composable

@Composable
internal fun ParentComposable(
    modifier: Modifier = Modifier
) {

    var stateLessObject = RootJavaClass.getNewInstance()
    var mutableStateObject by remember { mutableStateOf(stateLessObject)}

    Column(
        modifier = Modifier.fillMaxSize()
    ) {
        DoesNotReadAnyMutableState(stateLessObject = stateLessObject) {
            mutableStateObject = it
        }

        ReadsAMutableState(mutableStateObject)
    }
}

and a child composable inside the parent composable

@Composable // this composable is being re-composed unwantedly
fun DoesNotReadAnyMutableState(
    stateLessObject : AnotherJavaClass,
    onButtonClick : (AnotherJavaClass) -> Unit
) {
    Button(
        onClick = {
            onButtonClick(AnotherJavaClass("GoodBye"))
        },
    ) {
        Text(
            text = stateLessObject.data
        )
    }
}

@Composable
fun ReadsAMutableState(
    mutableStateObject: AnotherJavaClass
) {
    Text(
        text = mutableStateObject.data
    )
}

Why does the DoesNotReadAnyMutableState composable is being re-composed? even if it doesn't read the mutableState object?, this does not happen with ordinary classes even with a simple String.

This only happens when I use static referenced object, set it to be remembered as the initial value of the mutable state object and modify that object ( as you can see in the lambda callback )

Edit:

I made some changes referencing the answer from another post regarding Smart Re-composition, credits to @Thracian Smart Re-composition

I created my own non-inlined Column{..} scope

@Composable
fun MyOwnColumnScope(content: @Composable () -> Unit) {
    Column(
        modifier = Modifier.fillMaxSize()
    ) {
        content()
    }
}

and used it this way

MyOwnColumnScope {
    DoesNotReadAnyMutableState(stateLessObject = stateLessObject) {
        mutableValue = it
    }

    ReadsAMutableState(mutableValue)
}

but it still triggers the un-wanted recomposition.

I'm not quite sure if there's a special case within the usage of statics, or if this is a bug within the snapshot \ compose framework?

z.g.y
  • 5,512
  • 4
  • 10
  • 36

1 Answers1

1

It's because Column doesn't create a recomposition scope. And on each recomposition you create new instance of stateLessObject and pass it to DoesNotReadAnyMutableState Composable.

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. Column, Row and Box are inline functions and because of that they don't create scopes.

If you change your Column into the Composable above it won't be recomposed.

@Composable
private fun MyColumn(modifier: Modifier,content: @Composable ()->Unit) {
    Column(modifier=modifier){
        content()
    }
}

I did further testing. First, when it's Kotlin class it definitely requires non-inlined function to have scoped function to not recompose DoesNotReadAnyMutableState

@Composable // this composable is being re-composed unwantedly
fun DoesNotReadAnyMutableState(
    stateLessObject: AnotherJavaClass,
    onButtonClick: (AnotherJavaClass) -> Unit
) {
    Button(
        onClick = {
            onButtonClick(AnotherJavaClass("GoodBye"))
        },
    ) {
        Text(
            text = stateLessObject.data
        )
    }
    Box(
        modifier = Modifier
            .background(getRandomColor())
            .fillMaxWidth()
            .height(20.dp)
    )
}

@Composable
fun ReadsAMutableState(
    mutableStateObject: AnotherJavaClass
) {
    Text(
        text = mutableStateObject.data
    )
}


@Composable
internal fun ParentComposable(
    modifier: Modifier = Modifier
) {

    var stateLessObject = MyData("Column")
    var mutableStateObject by remember { mutableStateOf(stateLessObject) }

    Column(
    ) {
        DoesNotReadAnyMutableState(stateLessObject = stateLessObject) {
            mutableStateObject = it
        }

        ReadsAMutableState(mutableStateObject)
    }
}

@Composable
internal fun ParentComposable2(
    modifier: Modifier = Modifier
) {

    var stateLessObject = MyData("MyColumn")
    var mutableStateObject by remember { mutableStateOf(stateLessObject) }

    MyColumn() {
        DoesNotReadAnyMutableState(stateLessObject = stateLessObject) {
            mutableStateObject = it
        }

        ReadsAMutableState(mutableStateObject)
    }
}

class MyData(val data: String)

Result

enter image description here

Then changed MyData class to one below and created another one that returns same instance

class MyData() {
    val anotherJavaClass: AnotherJavaClass
        get() = AnotherJavaClass("")
}

class MyData2() {
    val anotherJavaClass = AnotherJavaClass("")
}

And parent classes to

@Composable
internal fun ParentComposable(
    modifier: Modifier = Modifier
) {

    var stateLessObject = MyData().anotherJavaClass

    LogCompositions(" ParentComposable() ${stateLessObject.hashCode()}")

    var mutableStateObject by remember { mutableStateOf(stateLessObject) }

    MyColumn() {

        LogCompositions(" ParentComposable() MyColumn scope ${stateLessObject.hashCode()}")

        DoesNotReadAnyMutableState(stateLessObject = stateLessObject) {
            mutableStateObject = it
        }

        ReadsAMutableState(mutableStateObject)
    }
}

@Composable
internal fun ParentComposable2(
    modifier: Modifier = Modifier
) {

    var stateLessObject = MyData2().anotherJavaClass
    var mutableStateObject by remember { mutableStateOf(stateLessObject) }
    LogCompositions(" ParentComposable2() ${stateLessObject.hashCode()}")

    MyColumn() {
        LogCompositions(" ParentComposable2() scope ${stateLessObject.hashCode()}")

        DoesNotReadAnyMutableState(stateLessObject = stateLessObject) {
            mutableStateObject = it
        }

        ReadsAMutableState(mutableStateObject)
    }
}

LogComposition is

class Ref(var value: Int)

// Note the inline function below which ensures that this function is essentially
// copied at the call site to ensure that its logging only recompositions from the
// original call site.
@Composable
inline fun LogCompositions(msg: String) {
    val ref = remember { Ref(0) }
    SideEffect { ref.value++ }
    println("$msg, recomposition: ${ref.value}")
}

Result

enter image description here

When non-inlined function used it doesn't recompose parent but java class with or without static causing scope to trigger recomposition but i don't know why yet.

You can check my other answers about scoped/smart recomposition below

Jetpack Compose Smart Recomposition

Why does mutableStateOf without remember work sometimes?

How can I launch recomposition when a specified Flow changed in Jetpack Compose?

When will Jetpack Compose launch recomposition and what will be recomposition?

There are very good articles in the link i suggest you to check out.

Thracian
  • 43,021
  • 16
  • 133
  • 222
  • Thank you @Thracian, I tried to apply your scoped/smart re-composition, and it still triggers the un-wanted `re-composition` pass, also, like I mentioned, when I don't use `static references`(e.g String, Int objects etc) it performs a `smart recomposition` without having to implement a `non-inlined` `Column{...}` scope. Is there any special cases with `statics`? or is this a bug within the `snapshot` framework? – z.g.y Jul 16 '22 at 01:39
  • Based on a podcast I watched where Chuck Jazdzewski said (based on the context I understand), when the `snapshot` sees a state value changed, it will send a `notification` to the `composer` and it will trigger a `re-composition` to those `composables` that observes that `state`, so does `compose` observes `statics` same way it observes `mutableState` objects? – z.g.y Jul 16 '22 at 01:47
  • I run some tests and added them as sample. Even without static accessing java class triggers recomposition inside scope. But using an non-inline function creates a scope thus it limits composition inside it if you check examples you will see that lines or logs above `MyColumn`is not recomposed or invoked – Thracian Jul 16 '22 at 05:02
  • Honestly, i have no idea. I had never tried using java classes before. – Thracian Jul 16 '22 at 07:19