private lateinit var activityMainBinding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityMainBinding = ActivityMainBinding.inflate(inflater)
setContentView(activityMainBinding.root)
}
private val activityMainBinding by viewBinder(ActivityMainBinding::inflate)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(activityMainBinding.root)
}
- We have an inline delegate + extension function thanks to the lazy (more on that)
- We access ActivityMainBinding's function inflate via reference
- Magically inflated
Let's take a look at our viewBinder function
inline fun <T : ViewBinding> Activity.viewBinder(
crossinline bindingInflater: (LayoutInflater) -> T) =
lazy(LazyThreadSafetyMode.NONE) {
bindingInflater.invoke(layoutInflater)
}
We have an inline function of course to minimize the overhead, then we have another function as a parameter that takes the LayoutInflater as a parameter and returns the generic T which is our ViewBinding.
Our lazy delegate provided by Kotlin is how we get the by in our viewBinder and we use LazyThreadSafeMode.NONE since the view can't be accessed on other threads and thus we don't need the lazy to create even more overhead with the synchronizations.
We marked our bindingInflater function as a crossinline since it's called in another scope of a function (lazy), and then we just invoke it (which means inflating our view binding) with a parameter of layoutInflater which is context.getLayoutInflater in the background.
Now we have to write only 2 lines of code instead of 3.
But that's too much, 2 lines of code, nope, we can do better, we are lazy.
Le few moments later and here we are.
private val activityMainBinding by viewBinding(ActivityMainBinding::inflate)
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
}
fun <T : ViewBinding> AppCompatActivity.viewBinding(
bindingInflater: (LayoutInflater) -> T,
beforeSetContent: () -> Unit = {}) =
ActivityViewBindingDelegate(this, bindingInflater, beforeSetContent)
class ActivityViewBindingDelegate<T : ViewBinding>(
private val activity: AppCompatActivity,
private val viewBinder: (LayoutInflater) -> T,
private val beforeSetContent: () -> Unit = {}
) : ReadOnlyProperty<AppCompatActivity, T>, LifecycleObserver
class ActivityViewBindingDelegate<T : ViewBinding>(
private val activity: AppCompatActivity,
private val viewBinder: (LayoutInflater) -> T,
private val beforeSetContent: () -> Unit = {}
) : ReadOnlyProperty<AppCompatActivity, T>, LifecycleObserver {
private var activityBinding: T? = null
init {
activity.lifecycle.addObserver(this)
}
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun createBinding() {
initialize()
beforeSetContent()
activity.setContentView(activityBinding?.root)
activity.lifecycle.removeObserver(this)
}
private fun initialize() {
if (activityBinding == null) {
activityBinding = viewBinder(activity.layoutInflater)
}
}
override fun getValue(thisRef: AppCompatActivity, property: KProperty<*>): T {
ensureMainThread()
initialize()
return activityBinding!!
}
private fun ensureMainThread() {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw IllegalThreadStateException("View can be accessed only on the main thread.")
}
}
}
private var activityBinding: T? = null
init {
activity.lifecycle.addObserver(this)
}
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun createBinding() {
initialize()
beforeSetContent()
activity.setContentView(activityBinding?.root)
activity.lifecycle.removeObserver(this)
}
private fun initialize() {
if (activityBinding == null) {
activityBinding = viewBinder(activity.layoutInflater)
}
}
- initialize() - self explanatory
- beforeSetContent() - which takes place before setContentView(), this is useful for all the Dagger component injections and other stuff you need to do before setContentView happens
- Then we set the content view
- We remove the lifecycle observer???? yeah we do because setContentView has to happen only once, call it magic, call it true.
We override the
override fun getValue(thisRef: AppCompatActivity, property: KProperty<*>): T {
ensureMainThread()
initialize()
return activityBinding!!
}
C'mon man, already... it was supposed to be simple, it isn't..... not yet.
We have to ensure access only on the main thread and then do initialization phase in case it's a null (process death enters the chat).
and now we can do
private val activityMainBinding by viewBinding(ActivityMainBinding::inflate) {
//initialize Dagger voodoo or something else before setContentView
}
private val activityMainBinding by viewBinding(ActivityMainBinding::inflate) {
//initialize Dagger voodoo
}
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
}
class TestFragment : Fragment() {
private var testFragmentBinding: TestFragmentBinding? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
testFragmentBinding = TestFragmentBinding.inflate(inflater, container, false)
return testFragmentBinding?.root
}
override fun onDestroyView() {
super.onDestroyView()
testFragmentBinding = null
}
}
But why set it to null?
Fragments can be detached (when that happens the view is the only one that gets killed) but they'll still live, except if we don't destroy the view, mister memory leak appears.
class TestFragment : Fragment(R.layout.test_fragment) {
private val testFragmentBinding by viewBinding(TestFragmentBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//do your stuff
}
}
fun <T : ViewBinding> Fragment.viewBinding(
viewBindingFactory: (View) -> T,
disposeEvents: T.() -> Unit = {}) =
FragmentViewBindingDelegate(this, viewBindingFactory, disposeEvents)
Similar to our activity case, but instead, our parameter here is a view and we have disposeEvents which is invoked on an object and this FragmentViewBindingDelegate inspired by Zhuinden's post
class FragmentViewBindingDelegate<T : ViewBinding>(private val fragment: Fragment,
private val viewBinder: (View) -> T,
private val disposeEvents: T.() -> Unit = {}) : ReadOnlyProperty<Fragment, T>, LifecycleObserver {
init {
fragment.observeLifecycleOwnerThroughLifecycleCreation {
lifecycle.addObserver(this@FragmentViewBindingDelegate)
}
}
private inline fun Fragment.observeLifecycleOwnerThroughLifecycleCreation(crossinline viewOwner: LifecycleOwner.() -> Unit) {
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
viewLifecycleOwnerLiveData.observe(this@observeLifecycleOwnerThroughLifecycleCreation, Observer { viewLifecycleOwner ->
viewLifecycleOwner.viewOwner()
})
}
})
}
private var fragmentBinding: T? = null
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun disposeBinding() {
fragmentBinding?.disposeEvents()
fragmentBinding = null
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
ensureMainThread()
val binding = fragmentBinding
if (binding != null) {
return binding
}
val lifecycle = fragment.viewLifecycleOwner.lifecycle
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
throw IllegalStateException("Fragment views are destroyed.")
}
return viewBinder(thisRef.requireView()).also { fragmentBinding = it }
}
}
It's kinda obnoxious to look at first, but let's take a dive,
We have our Fragment again that's holding the property, our viewBinder which accepts a View as a parameter and disposeEvents function (more on that later).
init {
fragment.observeLifecycleOwnerThroughLifecycleCreation {
lifecycle.addObserver(this@FragmentViewBindingDelegate)
}
}
private inline fun Fragment.observeLifecycleOwnerThroughLifecycleCreation(crossinline viewOwner: LifecycleOwner.() -> Unit) {
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
viewLifecycleOwnerLiveData.observe(this@observeLifecycleOwnerThroughLifecycleCreation, Observer { viewLifecycleOwner ->
viewLifecycleOwner.viewOwner()
})
}
})
}
We need to listen to the lifecycle somehow ... we need to know when the viewLifecycle is created so that we can do our bindings, thankfully Fragments have
viewLifecycleOwnerLiveData
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
ensureMainThread()
val binding = fragmentBinding
if (binding != null) {
return binding
}
val lifecycle = fragment.viewLifecycleOwner.lifecycle
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
throw IllegalStateException("Fragment views are destroyed.")
}
return viewBinder(thisRef.requireView()).also { fragmentBinding = it }
}
return viewBinder(thisRef.requireView()).also { fragmentBinding = it }
But wait, there's more.
You wanted to do dispose events, clean ups.
If you try onDestroyView() or onDestroy() you'll get IllegalStateException that views are destroyed and you'll think onPause and onStop are the perfect place to do disposes, they are but not quite your use case?
Hey, we got that case covered
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun disposeBinding() {
fragmentBinding?.disposeEvents()
fragmentBinding = null
}
private val testFragmentBinding by viewBinding(TestFragmentBinding::bind){
//do the dispose here
this.recyclerView.adapter = null
}
buildFeatures {
viewBinding = true
}
why not just only use the binding in the onCreateView method? Then it doesn't need to be destroyed later...
ReplyDeleteIt needs to be, or you'll face memory leaks...
DeleteSee, the tricky thing about this is because fragments are put on the backstack once they are their views need to be nulled cause they keep a reference to it, if you don't do that you'll get memory leaks because the fragment's view is the only thing that's destroyed but not the fragment itself.
You can read more here
https://speakerdeck.com/amanda_hinchman/a-brief-history-of-memory-leaks
Hey is it possible to use these view binding extensions in base classes? Like if i have a base fragment that is handling all the lifecycle functions like oncreateview, onViewCreated, etc , can I use viewbinding in a manner that child fragment provides just the layoutRes , say R.layout.item_child_frag and parent class could inflate the binding class (ItemChildFragBinding.class) ,pass the view in oncreateview and create a global binding instance that could be used by child fragment? I want to keep the child class as lean as possible
ReplyDelete