Memory leak in activity enter transition coordinator after shared element transition

1.1k views Asked by At

I'm having a problem with memory leak in EnterTransitionCoordinator while using shared element transitions. Below you can see the app structure:

App structure

It has 2 screens, first is an Activity with DrawerLayout and few Fragments inside. One of them consists a list of photos and clicking specific photo triggers shared element transition to Fragment from ViewPager located in another Activity. I'm using custom SharedElementCallback when exiting and reentering these two Activitys for mapping a correct View for shared element transition. I based my code on this great blog post: https://android.jlelse.eu/dynamic-shared-element-transition-23428f62a2af

The problem is, that after swiping between ViewPager's items, Fragments are being destroyed, but the View used for shared element transition is being kept in Activity's ActivityTransitionState, specifically in EnterTransitionCoordinator. The same when reentering to Activity with DrawerLayout and then opening another Fragment. References to Views used for shared element transitions are still kept int Activitys even though Fragments were destroyed, which causes a memory leak.

My question: Is there a good way to avoid this memory leak?

1

There are 1 answers

2
Grzegorz Matyszczak On

I discovered that there's a method clearState() in EnterTransitionCoordinator, which should be called in Activity.onStop(). But since the Activity is not yet being stopped, Views from Fragments are being leaked. As a temporary workaround, I'm clearing that state manually on Fragment.onDestroyView() by calling this method with reflection. Below you can see the code:

/**
 * Works only for API < 28
 * https://developer.android.com/about/versions/pie/restrictions-non-sdk-interfaces
 */
fun Fragment.clearEnterTransitionState() {
    try {
        getActivityTransitionState()
            ?.getEnterTransitionCoordinator()
            ?.invokeClearStateMethod()
    } catch (e: Exception) {
        // no-op
    }
}

private fun Fragment.getActivityTransitionState() =
    Activity::class.java.getField("mActivityTransitionState", requireActivity())

private fun Any.getEnterTransitionCoordinator() = javaClass.getField("mEnterTransitionCoordinator", this)

private fun Any.invokeClearStateMethod() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
        javaClass.superclass?.invokeClearStateMethod(this)
    } else {
        javaClass.invokeClearStateMethod(this)
    }
}

private fun <T> Class<T>.getField(name: String, target: Any): Any? =
    getDeclaredField(name).run {
        isAccessible = true
        get(target)
    }

private fun <T> Class<T>.invokeClearStateMethod(target: Any) {
    getDeclaredMethod("clearState").apply {
        isAccessible = true
        invoke(target)
    }
}