Keep a reference to a `@Composable` function will ...
# compose
r
Keep a reference to a
@Composable
function will cause memory leak? I thought a @Composable function is stateless but looking at ComposableLambdaImpl there is a RecomposeScope in it. Does this mean we should not keep a
@Composable
longer than the scope it is in? Is there a workaround to keep a long live
@Composable
without having a leak? BTW by
@Composable
function I mean creating properties like
val foo = @Composable { ... }
s
RecomposeScope instances are cleared when lambda is not used, it is fine to hoist lambda instances to static scope
r
Thanks for the reply! However, I'm seeing memory leaks caused by composable lambdas, because I kept a composable lambda in view model and after rotate (on Android) it leaked previous Activity. Could you elaborate how composable lambda release the reference to RecomposeScope? I can see it will release the previous scope if the lambda is invoked again in a new composition, but what about when the lambda is not invoked in the new composition?
s
If you can reproduce the activity leaking in a separate project, please file a bug report Lambda might hold onto scope instance, but it should be released as well without retaining activity
m
@Raid Xu is it possible that you are capturing other variables in your lambda?
r
Let me do more investigations to make sure, thanks you all!
s
If you have a leak canary report, that might be helpful as well
r
Thanks. There is a LeakCanary report but I can only show part of it. ... (from a composable property) ├─ androidx.compose.runtime.internal.ComposableLambdaImpl instance │ Leaking: UNKNOWN │ Retaining 540 B in 24 objects │ ↓ ComposableLambdaImpl.scope │ ~~~~~ ├─ androidx.compose.runtime.RecomposeScopeImpl instance │ Leaking: UNKNOWN │ Retaining 499 B in 22 objects │ ↓ RecomposeScopeImpl.block │ ~~~~~ ├─ androidx.compose.runtime.internal.ComposableLambdaImpl$invoke$6 instance │ Leaking: UNKNOWN │ Retaining 447 B in 20 objects │ Anonymous subclass of kotlin.jvm.internal.Lambda │ ↓ ComposableLambdaImpl$invoke$6.$p6 ... (eventually leads to a Fragment) I'm not sure if this part alone can prove the existence of the problem. I'm still trying to create a minimal reproducible project, might take some time.
s
Hm, I think this might be valid, I see that we don't clear block on release
r
Hi, sorry for the late reply. Here is a minimal repro sample
Copy code
class MainActivity : ComponentActivity() {
    private val vm: ComposableLambdaViewModel by viewModels<ComposableLambdaViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Column {
                if (vm.invoked == 0) {
                    vm.composable.invoke(this@MainActivity)
                }
                Greeting("Compose")
            }
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

class ComposableLambdaViewModel : ViewModel() {
    val composable =
        @Composable { _: Any? ->
            Text("Hello from composable lambda")
            DisposableEffect(Unit) {
                invoked += 1
                onDispose { }
            }
        }

    var invoked = 0
}
If you launch this Activity then rotate, the Activity will be leaked. There are few key requirements for the repro: • Keeping the composable lambda property in view model • Pass a short-live object when invoking composable (in my sample, the activity itself) • After rotate, don't invoke the composable again It seems the RecomposeScope keeps a reference to parameters that passed to composable lambda (for computing changes?). I guess we can't really avoid passing short-live objects into composables, do you have any suggestion on how to workaround this?
s
Please file a bug to compose bug tracker with this repro, so it is not lost here
👍 1