A common challenge when writing Android apps is properly scoping objects: if they outlive the lifecycle in which they’re needed then we are wasting memory and they can be leaked, but if we recreate objects too soon then we could be duplicating work when we don’t need to.

The most common place that this manifests in is Activities and Fragments, since they’re destroyed and replaced by a new instance during events like configuration changes, which can be unintuitive, at first.

Moving logic out of Activities and Fragments into presenters (or view models) allows us to avoid having to restart operations during these re-creation events, but can be challenging to get right.

Let’s take a look at some of the existing approaches that allow us to create presenters that will live in the context of our views, and then I’ll share an alternate approach that I’ve used to ensure my presenters are properly bound to their view’s lifecycle.

This approach can also be extended to other objects, like Dagger Components, that should be bound to a certain lifecycle and, since the retained object can be any object, including those not tied to the Android framework, it provides a good mechanism for presenters that are multiplatform ready.

onRetainNonConfigurationInstance

One old, now deprecated solution to allow developers to keep objects across the tear-down and rebuild of a configuration change is the Activity#onRetainNonConfigurationInstance() method. By overriding this method in your activity you can specify some arbitrary object to be retained after the activity is destroyed, and provided to the new one.

class MyActivity : Activty() {

	private lateinit var presenter: MyPresenter

	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)

		presenter = lastNonConfigurationInstance as MyPresenter?
		    ?: MyPresenter()
	}

	override fun onRetainNonConfigurationInstance() = presenter
}

In this example, when the user rotates the device and causes a configuration change, the original activity will be destroyed, but onRetainNonConfigurationInstance() will return the existing presenter, allowing the replacement activity to share the same instance by retrieving it from lastNonConfigurationInstance.

This prevents the need to recreate the presenter, allowing the new activity to benefit from the existing work, and cache, performed during the previous activity’s lifecycle.

Even though this is deprecated it is still a viable option if you need a simple solution, but it has it’s limitations.

You can only specify a single object per activity to be retained, so if you have more state to retain then you’ll need to wrap them in a container, which can get quite complex, depending on the possible permutations.

Non-configuration instances are also limited to activities, so you can’t use it to bind objects to another lifecycle, like a fragment. For those you’ll have to use a different approach.

setRetainInstance

If you use fragments for at least some of your views, a more recent, but still deprecated, solution is Fragment’s setRetainInstance. By calling this method with the parameter set to true your custom fragment will be retained across activity re-creation, allowing you to get the same effect.

class MyPresenter : Fragment() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    retainInstance = true
    
    // start the heavy lifting
  }
}

The fragment can then be retrieved by ID or tag from the containing activity’s FragmentManager.

class MyActivity : AppCompatActivity() {

  private lateinit var presenter: MyPresenter

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    presenter =
        supportFragmentManager.findFragmentByTag("presenter") as MyPresenter?
        ?: MyPresenter().also {
          supportFragmentManager.beginTransaction()
              .add(it, "presenter")
              .commitNow()
        }
  }
}

While this may let you share objects during the full lifecycle of the fragment, it is a rather heavy-handed solution, since fragment is an extremely overloaded class, complete with view handling and a complex lifecycle.

AndroidX ViewModel

More recently, the recommendation from the Android team is to use an AndroidX ViewModel. ViewModels are designed to store and manage UI related data, and are scoped to the entire lifecycle of the component to which they’re attached. This means that they survive activity and fragment recreation events like configuration changes.

class MyPresenter : ViewModel() {
}

The AndroidX KTX libraries for activities and fragments even have some really convenience property delegates to make retrieving your existing ViewModel, or creating a new one, extremely easy.

class MyActivity : AppCompatActivity() {

  private val presenter: MyPresenter by viewModels()

}

Using a ViewModel provides a lighter-weight object in which you can put the code that drives your UI, making testing and lifecycle management easier.

The AndroidX ViewModel class is a huge win for developers, but requires that you inherit from a class that’s tightly bound to the Android framework, meaning they can’t easily be used for multiplatform projects, and adding complexity to testing and maintenance.

A Scoped Delegate

While the Android team recommends using ViewModels to retain objects during a configuration change, that doesn’t mean that that ViewModel needs to actually contain the code that drives your UI, it is simply an object that Android knows how to retain, and can therefore serve as a container to store a platform independent presenter.

Using an approach really similarly to the viewModels() function from the AndroidX KTX library, we can create a generic solution that allows you to retain any object across configuration changes without requiring any extra boiler-plate or platform specific code.

class MyActivity : AppCompatActivity() {

  private val presenter: MyPresenter by scoped { MyPresenter() }

}

Under the hood, this scoped property delegate uses an AndroidX ViewModel that simply wraps a value.

class ScopeViewModel<V>(val value: V) : ViewModel() {

  class Factory<V>(val valueFactory: () -> V) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T =
        ScopeViewModel(valueFactory()) as? T
  }

}

The ScopeViewModel contains a ViewModelProvider.Factory that uses a lambda to provide the value it should store. This ViewModel is cached in a Lazy property delegate, which uses the ScopedViewModel to retrieve and retain the value.

class LazyScopedValue<T>(
    private val storeProducer: () -> ViewModelStore,
    private val factoryProducer: () -> ViewModelProvider.Factory
) : Lazy<T> {
  private var cached: Any = NotSet

  override val value: T
    get() {
      val value = cached
      return if (value == NotSet) {
        val factory = factoryProducer()
        val store = storeProducer()
        val viewModel = ViewModelProvider(store, factory).get<ScopeViewModel<T>>()
        
        viewModel.value.also {
          cached = it as Any
        }
      } else {
        value as T
      }
    }

  override fun isInitialized() = cached != NotSet
}

This is almost identical to the ViewModelLazy returned by the viewModels function, but caches and returns the value stored in the ScopeViewModel as opposed to the ViewModel itself.

The last piece is an inlined extension function on ViewModelStoreOwner (ComponentActivity, Fragment, etc) to provide a nice public API, again copied from the viewModels source code.

inline fun <reified T> ViewModelStoreOwner.scoped(noinline creator: () -> T): Lazy<T> {
  return LazyScopedValue({ viewModelStore }, { ScopeViewModel.Factory(creator) })
}

This provides a nice, fluent call in your activities and fragments that allows you to scope any object to a lifecycle. Outside of presenters and platform independent view models, this provides a convenient place to store anything that needs to live within a certain lifecycle, like Dagger Components, without requiring manual management, or tying you directly to the Android framework.

You can get the full source code here.