androidandroid-jetpack-composecompose-recomposition

Strange behavior of onDispose in Jetpack Compose


@Composable
fun AndroidViewContainer(modifier: Modifier = Modifier, int: () -> Int) {
    int() // for causing recomposition
    var view: View? = null

    SideEffect {
        println("Log sideeffect $view")
    }
    DisposableEffect(Unit) {
        onDispose {
            println("Log detached $view")
        }
    }

    AndroidView(
        modifier = modifier,
        factory = { context ->
            View(context).also {
                println("Log factory")
                view = it
            }
        },
        update = {
            println("Log: $view")
        }
    )
}

Having the code like above, I would assume that when first composition happens, the factory callback is getting called and view is getting set to be not null, but the View. After the recomposition, the var view: View? = null should be executed and override the View that was set before and value of the view should be null again.

This is true for SideEffect or update, those receive the null value. But when we leave the screen, the onDispose is getting called but not with null value but with the View that was set in factory callback.

And here comes the question: Do compose compiler somehow wraps it for the onDispose call? Why only onDispose still have this value and not other side effects?

Wrapping the view like var view: View? by remember { mutableStateOf(null) } will make it work like I expect, but I am just wondering of why the value is saved for the onDispose call.


Solution

  • When you specify a DisposableEffect (or LaunchedEffect) with Unit as key, what happens is that the DisposableEffect is only executed once when the Composable initially enters the composition. It keeps any variables in the state they were in when the DisposableEffect was called.

    This behavior can be demonstrated by this simplified code:

    @Composable
    fun MainComposable() {
        Column {
            var count by remember {
                mutableIntStateOf(0)
            }
    
            Button(onClick = { count++ }) {
                Text(text = "RECOMPOSE")
            }
    
            if (count <= 3) {
                AndroidViewContainer(count)
            }
        }
    }
    
    @Composable
    fun AndroidViewContainer(int: Int) {
        SideEffect {
            println("Log sideeffect $int")
        }
        DisposableEffect(Unit) {
            println("Log DisposableEffect attached with $int")
            onDispose {
                println("Log DisposableEffect detached with $int")
            }
        }
    }
    

    Logs:

    I  Log DisposableEffect attached with 0
    I  Log sideeffect 0
    I  Log sideeffect 1
    I  Log sideeffect 2
    I  Log sideeffect 3
    I  Log DisposableEffect detached with 0
    

    In comparison, the SideEffect is executed after every single successful recomposition and thus always will print the latest value.

    If you want the DisposableEffect to be updated whenever a variable changes, you need to specify that variable as a key:

    DisposableEffect(int) {
        //...
    }
    

    or use rememberUpdatedState to observe a variable in a way that does not require the DisposableEffect to restart every time the variable changes, what would be the case when providing that variable as key:

    @Composable
    fun AndroidViewContainer(modifier: Modifier = Modifier, int: Int) {
        val currentInt by rememberUpdatedState(int)
        SideEffect {
            println("Log sideeffect $int")
        }
        DisposableEffect(Unit) {
            println("Log DisposableEffect attached with $currentInt")
            val oldCounter = int
            onDispose {
                println("Log DisposableEffect detached with $currentInt")
            }
        }
    }