[Из песочницы] Делаем Android View Binding удобным c Kotlin
Привет! Меня зовут Кирилл Розов. Я автор Telegram канала Android Broadcast. Очень люблю Kotlin и мне нравится с помощью его возможностей упрощать разработку. С такой задачей я недавно столкнулся, когда на новом Android проекте начали использовать View Binding.
Эта возможность появилась в 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(
private val viewBinder: ViewBinder
) : ReadOnlyProperty {
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 Fragment.viewBinding(): ReadOnlyProperty {
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(...) : ReadOnlyProperty {
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 }
}
}
}
Полный код можно найти здесь и использовать его на своих проектах.