Делаем Android View Binding удобным c Kotlin

—

от автора

Привет! Меня зовут Кирилл Розов. Я автор Telegram канала Android Broadcast. Очень люблю Kotlin и мне нравится с помощью его возможностей упрощать разработку. С такой задачей я недавно столкнулся, когда на новом Android проекте начали использовать View Binding.

image

Эта возможность появилась в Android Studio 3.6, но на самом деле она не совсем новая, а облегченный вариант Android Data Binding. Зачем столько усложнений? Проблема была в скорости — множество разработчиков использовали Android Data Binding только для генерации кода с ссылками на View и игнорировали другие возможности библиотеки. Поэтому чтобы ускорить генерацию кода сделали View Binding. Однако стандартный способ работы с ней — это дублирование кода от которого хочется избавиться.

Стандартный способ работы с View Binding

Разберем работу View Binding на примере Fragment. У нас есть layout ресурс с именем profile.xml (содержимое его неважно). Если мы хотим использовать ViewBinding, тогда в стандартном варианте это будет выглядеть так:

class ProfileFragment : Fragment(R.layout.profile) {      private var viewBinding: ProfileBinding? = null      override fun onViewCreated(view: View, savedInstanceState: Bundle?) {         super.onViewCreated(view, savedInstanceState)         viewBinding = ProfileBinding.bind(view)         // Используем созданный viewBinding     }      override fun onDestroyView() {         super.onDestroyView()         viewBinding = null     } }

Проблема здесь несколько:

  • Много лишнего кода
  • Копипаста: каждый Fragment будет иметь аналогичный кусок кода
  • Property viewBinding получается nullable и модифицируемым.

Давайте пробовать избавляться от этого с помощью Cилы Kotlin

Kotlin Delegated Property в бой

С помощь делегирования работы с property в Kotlin можно круто повторно использовать код и упростить некоторые задачи. Например, я применил это в случае с ViewBinding. Для этого я сделал свой делегат, который оборачивает создание ViewBinding и очистку его в нужный момент жизненного цикла:

class FragmentViewBindingProperty<T : ViewBinding>(     private val viewBinder: ViewBinder<T> ) : ReadOnlyProperty<Fragment, T> {      internal var viewBinding: T? = null     private val lifecycleObserver = BindingLifecycleObserver()      @MainThread     override fun getValue(thisRef: Fragment, property: KProperty<*>): T {         checkIsMainThread()         this.viewBinding?.let { return it }          val view = thisRef.requireView()         thisRef.viewLifecycleOwner.lifecycle.addObserver(lifecycleObserver)         return viewBinder.bind(view).also { vb -> this.viewBinding = vb }     }      private inner class BindingLifecycleObserver : DefaultLifecycleObserver {          @MainThread         override fun onDestroy(owner: LifecycleOwner) {             owner.lifecycle.removeObserver(this)             viewBinding = null         }     } }

и конечно же функцию-фабрику, чтобы не видеть как делегат создается:

inline fun <reified T : ViewBinding> Fragment.viewBinding(): ReadOnlyProperty<Fragment, T> {     return FragmentViewBindingProperty(DefaultViewBinder(T::class.java)) }

После небольшого рефакторинга с новыми возможностями я получил следующее:

class ProfileFragment() : Fragment(R.layout.profile) {      private val viewBinding: ProfileBinding by viewBinding()      override fun onViewCreated(view: View, savedInstanceState: Bundle?) {         super.onViewCreated(view, savedInstanceState)         // Используем созданный viewBinding     } }

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

Момент, когда что-то пошло не так…

В какой-то момент возникла необходимость чистить View следующим образом:

class ProfileFragment() : Fragment(R.layout.profile) {      private val viewBinding: ProfileBinding by viewBinding()      override fun onDestroyView() {         super.onDestroyView()         // Сбрасываем View из viewBinding     } }

Но в итоге я получил состояние, что моя ссылка на ViewBinding внутри делегируемого property уже была почищена. Попытка перенести очистку кода до вызова super.onDestroyView() не принесла успеха и я начал копаться в причинах. Виновником стала реализация вызова методов жизненного цикла у Fragment.viewLifecycleOwner.

Событие ON_DESTROY в Fragment.viewLifecycleOwner происходит до вызова Fragment.onDestroyView(), поэтому FragmentViewBindingProperty очищался раньше, чем я того ожидал. Решением стало отложить вызов операции очистки. Все вызовы жизненного цикла вызываются последовательно и на главном потоке, поэтому весь фикс свелся к откладыванию очистки с помощью Handler:

class FragmentViewBindingProperty<T : ViewBinding>(...) : ReadOnlyProperty<Fragment, T> {      internal var viewBinding: T? = null      private inner class BindingLifecycleObserver : DefaultLifecycleObserver {          private val mainHandler = Handler(Looper.getMainLooper())          @MainThread         override fun onDestroy(owner: LifecycleOwner) {             owner.lifecycle.removeObserver(this)             mainHandler.post { viewBinding = null }         }     } }

Полный код можно найти здесь и использовать его на своих проектах.

ссылка на оригинал статьи https://habr.com/ru/post/501158/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *