Pull to refresh

Избавляемся от библиотек сохранения состояния фрагмента с помощью чистого kotlin

Reading time2 min
Views6.3K
image

Android библиотеки вспомогательной кодогенерации, такие как Android Annotations или мой любимый Icepick, которые разработчики привыкли использовать для упрощения написания, не готовы были сразу подружиться с Kotlin-кодом, так как большинство из них требует держать поля с модификатором package private. Конечно, ничего страшного писать

@JvmField @State
internal var carName: String? = null

вместо

@State String carName;

Но лучше вспомнить, что Kotlin к нам пришёл для упрощением кода, а не наоборот.

Для этого мы воспользуемся механизмом делегатов. Нам потребуется следующий класс:

abstract class InstanceStateProvider<T>(protected val savable: Bundle) {

    protected var cache: T? = null

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        cache = value
        if (value == null) 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 Parcelable -> savable.putParcelable(property.name, value)
            // whatever you want
        }
    }
}

Он принимает на вход Bundle, в которое сохраняет поле, и переменную cache, чтоб не дёргать постоянно из Bundle.

Далее, для получения поля nallable и notnull реализации будут разичаться:

class Nullable<T>(savable: Bundle) : InstanceStateProvider<T>(savable) {
        operator fun getValue(thisRef: Any?, property: KProperty<*>): T? {
            if (cache != null) return cache
            if (!savable.containsKey(property.name)) return null
            return savable.get(property.name) as T
        }
    }

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

Для notNull полей потребуется передавать значение по умолчанию.
Теперь, во фрагменте мы создаём поле с пустым Bundle, которое будет хранить все наши поля, я разместил его в базовом фрагменте.

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)

Всё! Никакой кодогенерации, никакого рефлекшена, можно использовать приватные поля.

private var carName: String? by instanceState()
private var index by instanceState(0) 

p.s. Когда я читал про делегаты в Kotlin, меня тема сразу захватила, но я тогда не знал, как можно их применить, кроме очевидных ситуаций, для которых в стандартной библиотеке есть готовая реализация (lazy и observer). Даже искал место, где бы можно их искусственно засунуть, чтоб попробовать. Вот, нашёл =) Всем успехов!
Tags:
Hubs:
+7
Comments6

Articles