Conductor: Kotlin - lateinit memory leak issues

Conductor should boldly advertise (I don’t think it’s made obvious enough for new users) that its instances survive configuration changes (stark contrast to Activity/Fragment/View) - this has huge implications for dagger + kotlin users.

class SalesController : BaseController, SalesView {
    @Inject lateinit var viewBinder: SalesController.ViewBinder
    @Inject lateinit var renderer: SalesRenderer
    @Inject lateinit var presenter: SalesPresenter

    lateinit private var component: SalesScreenComponent

    override var state = SalesScreen.State.INITIAL  //only property that I want to survive config changes

    fun onCreateView(): View {  /** lateinit variables are set here */ }
    fun onDestroyView() { /** lateinit variables need to be dereferenced here, or we have a memory leak  */ }
}

My lateinit properties are injected by dagger, and I need to set them to null in onDestroyView - or have a memory leak. This however is not possible in kotlin, as far as I am aware (without reflection). I could make these properties nullable, but that would defeat the purpose of Kotlin’s null safety.

I’m not quite sure how to solve this. Ideally there could be some type of annotation processor that would null out specific variables automatically in onDestroyView

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 5
  • Comments: 27 (11 by maintainers)

Most upvoted comments

Ultimately, this is the solution I’ve implemented, and I’m happy to report it works quite well. If I want a property to survive a configuration change, I just don’t add the by Ref(ref) delegate.

abstract class BaseController : Controller() {

    val ref = ResettableReferencesManager()

    @CallSuper
    override fun onDestroyView(view: View) {
        ref.reset()
        super.onDestroyView(view)
    }
}

/**
 * Manages the list of references to reset. This is useful in Conductor's controllers to automatically clear
 * references in [Controller.onDestroyView]. References scoped to the view's lifecycle must be cleared
 * because Conductor's [Controller]s survive configuration changes
 */
class ResettableReferencesManager {
    private val delegates = mutableListOf<Ref<*, *>>()

    fun register(delegate: Ref<*, *>) {
        delegates.add(delegate)
    }

    fun reset() {
        delegates.forEach {
            it.reset()
        }
    }
}

/**
 * Kotlin delegate to automatically clear (nullify) strong references.
 */
class Ref<in R, T : Any>(manager: ResettableReferencesManager) {
    init {
        manager.register(this)
    }

    private var value: T? = null

    operator fun getValue(thisRef: R, property: KProperty<*>): T =
            value ?: throw UninitializedPropertyAccessException(property.name)

    operator fun setValue(thisRef: R, property: KProperty<*>, t: T) {
        value = t
    }

    fun reset() {
        value = null
    }
}


abstract class ExampleController : BaseController() {
    @set:Inject var presenter: HistoryPresenter by Ref(ref)

    /**
     * Binds views w/ butterknife. Lateinit won't work with a delegate.
     */
    private var viewBinder: ExampleController.ViewBinder by Ref(ref)

    /**
     * Named injections won't work. use kotlin property `get()` to workaround it
     */
    private val swipeRefresh get() = viewBinder.swipeRefresh

    private val recycler get() = viewBinder.recycler

    val state: State = State.INITIAL_VALUE //will survive config changes
}

One could also write a ResetableReference without a “manager”. Just add a Controller LifecycleListener internally in ResetableReference.

Just throwing another thank you on there @ZakTaccardi. I had another solution going that I was stubborn about swapping out until today. Your resettable reference made things so much nicer!