Decoupling Binding
This is an article part of a series. For objectives, fundamentals, project structure, and articles summary, see Android::Simplified
Repository: https://gitlab.com/migueltt/simpleandroid
Modern Android apps are quite simple to build nowadays — that is, if you follow the Android Architecture Component guidelines. It can be simplified even more if you follow these principles:
- An
Activity
orFragment
is just the glue for the Android system, the View (layouts), and ViewModels - Refactor all your UI related code into extension functions
- Do not create coroutines within your
Fragment
— the ViewModel should be the only one launching coroutines — the main reason is that coroutines should follow the ViewModel lifecycle - You can define ViewModels at the
Activity
,navigation-graph
, andFragment
level — this makes a lot easier on how data is shared and how coroutines will behave
The main objective is to have this architecture:
First, XML layouts contain all the UI design (until Jetpack Compose is finally production ready) — follow the common naming convention: fragment_test_main.xml
- Within the XML layout, use DataBinding only if you need to reference variables — otherwise, do not use the
<layout>..</layout>
tag and use ViewBinding instead — just make sure your appbuild.gradle
file specifies:
buildFeatures {
viewBinding true
dataBinding true
}
- Remember, whenever you use
<layout>..</layout>
you are adding a lot of code to synchronize how data is bind into your layout — if you really don’t need it, just use ViewBinding — btw: do not usefindViewById(..)
or kotlin synthetic id’s.
Second, name the related Fragment
using the same convention: FragmentTestMain
— yeah, kind of breaking the usual standards…
Third, create a FragmentTestMainBindingExt
file — this will include extension functions to decouple all UI related code from the Fragment
:
- When using DataBinding or ViewBinding a class is generated for your XML layout — it happens to follow the
<xml-filename>Binding
convention, and that’s why we namedFragmentTestMain
to ourFragment
— then all your files are easy to identify - Within this
FragmentTestMainBindingExt
file, start adding extension functions related to your binding class
Fourth, within your FragmentTestMain
always use the extension functions for the generated binding-class:
// Your traditional FragmentTestMain.kt file
package com.simpleandroid.modules.testui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.simpleandroid.databinding.FragmentTestMainBinding
class FragmentTestMain : Fragment() {
private lateinit var binder: FragmentTestMainBinding
private val viewModel: TestViewModel by viewModels() override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View =
FragmentTestMainBinding.inflate(
inflater, container, false
).apply {
binder = this
}.root
override fun onViewCreated(
view: View, savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState)
binder.setup(this)
viewModel.sample.observe(viewLifecycleOwner) {
// do something
}
}
}class TestViewModel : ViewModel() {
val _sample = MutableLiveData("test")
val sample: LiveData<String> = _sample
}
In another file:
// Decouple binding here: FragmentTestMainBindingExt.kt
package com.simpleandroid.modules.testui
import androidx.activity.addCallback
import androidx.core.view.GravityCompat
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import com.simpleandroid.R
import com.simpleandroid.databinding.FragmentTestMainBinding
import com.simpleandroid.ui.setupWithNavControllerExt
fun FragmentTestMainBinding.setup(
fragment: Fragment
) {
//reference all UI components through the binding
:
}
This decouples all your UI related code from the Fragment
itself and observe LiveData
from the ViewModel. That’s it, there’s nothing else to add to the Fragment
— it supports configuration-changes out of the box.
There may be exceptions for some edge-cases:
- Handling custom onBackPressed — always use
onBackPressedDispatcher
- Specific code when dealing with
Toolbar (top, bottom)
,Drawer
,FloatingActionButton
— to address this, check the UX Policies series
By following these principles, migrating over Jetpack Compose will be easier since all your UI is already decoupled from the Fragment
.
Now you are ready for the next step: Representing UI state using ViewModel and Repository.