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

    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). Даже искал место, где бы можно их искусственно засунуть, чтоб попробовать. Вот, нашёл =) Всем успехов!
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 6
    • +1
      никакого рефлекшена
      но delegated properties же используют рефлексию для получения kotlin.reflect.KProperty (соответственно еще появляется необходимость в зависимости org.jetbrains.kotlin:kotlin-reflect).
      • –1
        Нет, делегатам достаточно сокращённой рефлексии, которая доступна через stdlib.
        • 0

          kotlin.reflect.KProperty (а также KClass и остальные) лежат в kotlin stdlib, так что ничего подключать не надо. И если посмотреть декомпилированный код, то видно, что вся необходимая информация для создания KProperty, что используется в делегатах, встраивается в сгенерированный код.


          static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(TestFragment.class), "carName", "getCarName()Ljava/lang/String;")), (KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(TestFragment.class), "index", "getIndex()I"))};
          • 0
            а эта зависимость приносит в проект еще около 9к методов)
            • 0

              KProperty хоть и лежит в пакете рефлексии не требует подключения соответствующей либы, по крайне мере если используется только поле name и если память не изменяет геттер и сеттер. Вот если захочется использовать поля returnType, parameters и т.п. то да, придется подключать либку с рефлексией на 2мегабайта...

            • 0
              Добавлю, что свои делегаты лучше наследовать от котлиновских интерфейсов:

              interface ReadOnlyProperty<in R, out T> {
                  operator fun getValue(thisRef: R, property: KProperty<*>): T
              }
              
              interface ReadWriteProperty<in R, T> {
                  operator fun getValue(thisRef: R, property: KProperty<*>): T
                  operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
              }
              

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.