Привет! Меня зовут Кирилл Розов. Я автор 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<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 }
}
}
}
Полный код можно найти здесь и использовать его на своих проектах.
agent10
Можно немного улучшить. Не всегда нужно биндинг делать только через Fragment.requireView(). Иногда нужно немного кастомного поведения. Для этого можно в конструктор FragmentViewBindingProperty передавать инициализатор, который по-умолчанию будет делать то что и сейчас, а при необходимости можно изменить это поведение.
kirich1409 Автор
Будет классно если опишешь такой сценарий. В голову приходит только использование не корневой View. Добавлю возможность в библиотеку.
Я пока только добавил возможность избавиться от рефлексии при создании ViewBinding.
agent10
Именно так и есть.
Мне это понадобилось когда пришлось сделать биндинг на корневой view кастомизированного UI для ExoPlayer. Там хитрая система с override layout в xml и прямого доступа к контролям через биндинг не будет. Т.е. как раз нужно делать биндинг через requireView().findViewById(..id..)
kirich1409 Автор
Добавлено в версии 0.3. Буду рад обратной связи