Save state with Delegates

Michael Spitsin
4 min readJan 24, 2018

--

Or how to do it nicely with Kotlin

Today we gonna talk about saving state of objects or properties in fragments or activities. Every Android developer knows that this is a really important task, since activities in android will not live whole time, until you evidently close them. The system chooses activities that not active by now and closes them automatically with saving their state. When you go back to previous activity, that was closed system will restore it. Thus, as a programmer you have an opportunity to save and restore state of Activity, and if you will done it properly, user will not notice consequences of auto closing (ok, he/she will notice only white screen, while state is restoring, but it depends on performance of device and system, not on us).

So saving and restoring state is a significant task, that’s why in almost every application, you developing, you will use that. Now, let’s see how this mechanism looks in Fragment:

class SampleFragment : Fragment() {

private var desiredString: String = ""
private var location
: Location? = null

override fun
onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
savedInstanceState?.run {
desiredString
= getString(DESIRED_KEY) ?: ""
location = get(LOCATION_KEY) as? Location
}
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(DESIRED_KEY, desiredString)
outState.putParcelable(LOCATION_KEY, location)
}

companion object {
private const val DESIRED_KEY = "DESIRED_KEY"
private const val LOCATION_KEY
= "LOCATION_KEY"
}
}

As you can see, we have to use additional constants to indicate saving object in output bundle for ability to find it while restoring state. From that point if we, for instance, want to add new property to our saving state, we have to add new constant and two new lines in onSaveInstanceState and onCreate methods, which prompt for us as a place of coding optimization. And it’s true.

There are great amount of libraries, that provides such optimizations. For example, Android Annotations library provides a special annotation @InstanceState. You attach it to the field and inheritor of your fragment will generated with needed functionality. I will not tell about pros and cons of that approach. For me personally it is not such a good solutions, since you will generate whole new class, that you need to use from now, and you need to add additional @EFragment annotation. Plus, in Kotlin you need to hang additional annotations on your property, so that it will be visible for Java.

Also, there are a pretty good library IcePick, that generates special class for your activity. It will be aware of activity’s fields, that need to be saved. But you need to keep you fields package local in order to make this whole thing working.

That’s why in our team we introduced a small solution for that problem that’s:

  • Uses no code generation, but still concise
  • Gives to you ability to remain your properties private
  • Written on Kotlin and introduced for Kotlin

This solution: using Kotlin delegates for saving and restoring state.

Solution

Solution will be provided with examples and explanations for fragments. For activities you need to make almost same things.

First of all you need a BaseFragment to hook saving mechanism only once and not repeat it in every inheritor:

open class BaseFragment : Fragment() {    private val savable = Bundle()override fun onCreate(savedInstanceState: Bundle?) {
if(savedInstanceState != null) {
savable.putAll(savedInstanceState.getBundle("_state"))
}
super.onCreate(savedInstanceState)
}

override fun onSaveInstanceState(outState: Bundle) {
outState.putBundle("_state", savable)
super.onSaveInstanceState(outState)
}


protected fun <T> instanceState() = InstanceStateProvider.Nullable<T>(savable)
protected fun <T> instanceState(defaultValue: T) = InstanceStateProvider.NotNull(savable, defaultValue)
}

In that fragment we will have one bundle for all properties, that will be marked as “properties with saving/restoring state”. It kept in outState bundle, when onSaveInstanceState is called and taken from savedInstanceState while restoration of sate is happened. Both functions instanceState will be used in inheritors of BaseFragment.

class SampleFragment : BaseFragment() {
private var desiredString: String by instanceState("")
private var location: Location? by instanceState()

//...some code
}

Moreover, we can not write private var someString: String by instanceState() , it just won’t compile.

Now let’s see our InstanceStateProvider:

open class InstanceStateProvider<T>(private val savable: Bundle) {

private var cache: T? = null

operator fun
setValue(thisRef: Any?, property: KProperty<*>, value: T) {
cache = value
if (value == null) {
savable.remove(property.name)
return
}
when (value) {
is Int -> savable.putInt(property.name, value)
is Long -> savable.putLong(property.name, value)
is Float -> savable.putFloat(property.name, value)
is String -> savable.putString(property.name, value)
is Bundle -> savable.putBundle(property.name, value)
is Serializable -> savable.putSerializable(property.name, value)
is Parcelable -> savable.putParcelable(property.name, value)
}
}

protected fun getAndCache(key: String): T? = cache ?: (savable.get(key) as T?).apply { cache = this }

class
Nullable<T>(savable: Bundle) : InstanceStateProvider<T>(savable) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T? = getAndCache(property.name)
}

class NotNull<T>(savable: Bundle, private val defaultValue: T) : InstanceStateProvider<T>(savable) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = getAndCache(property.name) ?: defaultValue
}
}

As you can see there are two small inheritors of InstanceSaveProvider: Nullable and NotNull. They working with nullable and non null properties respectively. But main logic hidden in their common parent.

In main setValue operator of our delegate we caching our value, removing it from bundle, if we setting null and according to type of value using appropriate bundle method to put it.

That’s all you need to provide your project with easy save instance state mechanism, that is concise, not requires code generation or additional annotations. You need just to hang aforementioned delegates to desired properties and voila…your properties will be saved and restored properly!

Afterwords

Sometimes I’m amazed at the features of Kotlin. In this particular case Delegates helps us to work with Android Save Instance State nice and properly with minimum costs from the developer and also with help of brief, terse code.

--

--

Michael Spitsin
Michael Spitsin

Written by Michael Spitsin

Love being creative to solve some problems with an simple and elegant ways

Responses (3)