I'm playing around with Kotlin on Android and one thing makes me confused.
When I converted few Fragments from Java to Kotlin I got this:
class XFragment : Fragment() {
private var binding: FragmentXBinding? = null
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentUhfReadBinding.inflate(inflater, container, false)
return binding!!.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding!!.slPower.addOnChangeListener(this)
binding!!.btnClearTagList.setOnClickListener(this)
}
// ...
private fun updateUi(){
binding!!.someTextView.text = getSomeTextViewText()
binding!!.someSlider.value = getSomeSliderValue()
}
}
I can't make binding non-nullable, because it has to be initialized after XFragment class constructor, in onCreateView() or later.
So with this approach it has to be nullable and I have to put !! everywhere.
Is there some way to avoid these !!?
The official documentation suggests this strategy:
private var _binding: FragmentXBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
Ultimately, it becomes just like requireActivity() and requireContext(). You just need to remember not to use it in a callback that might get called outside the view lifecycle.
Note, you can create your view using the super-constructor layout parameter and then bind to the pre-existing view in onViewCreated. Then you might not even need to have it in a property. I rarely need to do anything with it outside onViewCreated() and functions directly called by it:
class XFragment : Fragment(R.layout.fragment_x) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentXBinding.bind(view)
binding.slPower.addOnChangeListener(this)
binding.btnClearTagList.setOnClickListener(this)
}
}
My Goal
I am trying to access the widget that was created inside my fragment using viewBinding.
What I have done / Info about my app
The language I am using is kotlin.
I have already added the code below into gradle:
buildFeatures{
dataBinding = true
viewBinding = true
}
I have tested binding.aTextView.setText("Code working.") inside my main activity and it works.
What's the problem
I have tested the setText code inside activity and it works. The problem right now is the same code when I move into the fragment it wouldn't work. And I am sure that the code has been executed as I putted a toast above it and the toast executed successfully which mean it should have at least reached that point before but not sure due to what reason there wasn't any changes.
My mainActivity Code:
class MainProgramActivity : AppCompatActivity() {
lateinit var binding: ActivityMainProgramBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainProgramBinding.inflate(layoutInflater)
setContentView(binding.root)
replaceFragment(FragmentMainPage())
}
private fun replaceFragment(fragment: Fragment){
val fragmentManager = supportFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()
fragmentTransaction.replace(R.id.fragmentContainerView,fragment)
fragmentTransaction.commit()
}
}
My fragment code:
class FragmentMainPage : Fragment(R.layout.fragment_main_page) {
lateinit var binding: FragmentMainPageBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Toast.makeText(getActivity(),"Text!",Toast.LENGTH_SHORT).show();
binding = FragmentMainPageBinding.inflate(layoutInflater)
binding.aTextView.setText("Code working") //<-- I want this code to make changes towards the textView
return super.onCreateView(inflater, container, savedInstanceState)
}
}
The aTextView itself is empty at the beginning, the expected result will be the aTextView to show "Code working".
I see two problems with your code. First, exactly what Michael pointed out. You're returning the super method when you should be returning the View you just created (binding.root). Second, you're currenly leaking your fragment. When you viewbind a fragment, you are supposed to set the variable to null in onDestroyView(), as per defined in the documentation.
class FragmentMainPage : Fragment(R.layout.fragment_main_page) {
private var _binding: FragmentMainPageBinding? = null
private val binding get() = _binding!! // non-null variable in order to avoid having safe calls everywhere
// create the view through binding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentMainPageBinding.inflate(layoutInflater, container, false)
return binding.root
}
// view already created, do whatever with it
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.aTextView.setText("Code working")
}
// clear the binding in order to avoid memory leaks
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Aim is to declare a base class
abstract class BaseDialog<T : ViewBinding> : AppCompatDialogFragment() {
lateinit var binding: T
}
and all child classes should extend this parent class
class ChildClass: BaseDialog<ChildClassViewBinding>() {
}
Then I want to inflate the binding in parent class and save it to binding property
This seems out of my scope of knowledge of kotlin
Is this really possible to do?
If I were to do this, I'd do it like this:
class BaseDialogFragment<T: ViewBinding>(private val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> T)
: AppCompatDialogFragment() {
var _binding: T? = null
val binding: T get() = _binding ?: error("Must only access binding while fragment is attached.")
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = bindingInflater(inflater, container, false)
return binding.root
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
with usage like:
class ChildClass: BaseDialog(ChildClassViewBinding::inflate) {
}
However, I would not do this in the first place (since there's a nice alternative). It can become messy pretty quickly to rely on inheritance for these kinds of things. What happens if you want to add some other features for a dependency injection framework, or some other common things you like to use? What if there's some features you like to use in some of your fragments but not all of them? And are you also creating base classes like this for Activity and non-Dialog Fragments?
These problems are why there's a programming axiom: "composition over inheritance".
Sometimes there's no choice but to use inheritance to avoid code duplication. But in the case of Fragments and Bindings, I don't think so. You can pass your layout reference to the super constructor, and use ViewBinding.bind() instead of inflate(). Since bindings rarely need to be accessed outside the onViewCreated function, you usually don't need a property for it.
class ChildClass: AppCompatDialogFragment(R.layout.child_class_view) {
override fun onViewCreated(view: View, bundle: Bundle?) {
super.onViewCreated(view, bundle)
val binding = ChildClassViewBinding.bind(view)
//...
}
}
If you do need a property for it, #EpicPandaForce has a library that makes it a one-liner and handles the leak-avoidance on destroy for you inside a property delegate.
Library here
Usage:
class ChildClass: AppCompatDialogFragment(R.layout.child_class_view) {
private val binding by viewBinding(ChildClassViewBinding::bind)
}
Create Base Fragment
abstract class BaseFragment<VB : ViewBinding> : Fragment() {
private var _bi: VB? = null
protected val bi: VB get() = _bi!!
abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> VB
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_bi = bindingInflater(inflater, container, false)
return _bi!!.root
}
override fun onDestroyView() {
super.onDestroyView()
_bi = null
}
}
In your Child Fragment
class HomeFragment : BaseFragment<HomeFragmentBinding>() {
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> HomeFragmentBinding
get() = HomeFragmentBinding::inflate
}
I'm trying to implement a BaseFragment in which I will pass the layout resource on it and it should outputs the binding to work in the fragment itself instead of need to do it everytime the fragment is extended.
For example I have this BaseFragment
open class BaseFragment(#LayoutRes contentLayoutId : Int = 0) : Fragment(contentLayoutId) {
private lateinit var onInteractionListener: OnFragmentInteractionListener
val toolbar : Toolbar?
get() {
return if(activity is BaseActivity)
(activity as BaseActivity).toolbar
else
null
}
override fun onAttach(context: Context) {
super.onAttach(context)
setOnInteractionListener(context)
}
...
In which I use like this
class A(): BaseFragment(R.layout.myFragment) { ... }
Now, if I use this I will need to do the definition of the binding class again in my onCreateView
class A(): BaseFragment(R.layout.myFragment) {
private lateinit var binding: MyFragmentBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.myFragment, container, false)
return binding.root
}
override fun onDestroy(){
binding = null
}
}
What I want to implement is that since I'm passwing the layout to my BaseFragment, I want my BaseFragment to handle the creation of the binding and just return me the binding in the fragment which I use to extend BaseFragment
What I want to have is something like this
class A(): BaseFragment(R.layout.myFragment) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.myTextView = ""
}
}
So my question is, how I can implement inside BaseFragment the onDestroy() and the onCreateView to always create a binding for me from the layout I'm passing in ?
I heard that I should use reflection but I'm not that sure on how to accomplish it
I didn't hear about the possibility to get the databinding just from a layout, but even if it's possible, I don't think that is the recommended way, because of two reasons:
Reflection is slow
It makes things more complicated than they are.
Instead of making magic with Reflection, you could do something like this:
abstract class BaseFragment<out VB: ViewDataBinding>(
private val layout: Int,
// Other Dependencies if wanted
) : Fragment() {
abstract val viewModel: ViewModel
// other variables that all fragments need
// This does not cause any memory leak, because you are not storing the binding property.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = DataBindingUtil.inflate<VB>(inflater, layout, container, false).apply {
lifecycleOwner = viewLifecycleOwner
setVariable(BR.viewModel, viewModel)
}.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// Do some basic work here that all fragments need
// like a progressbar that all fragments share, or a button, toolbar etc.
}
And then, when you still need the bindingProperty, I would suggest the following library (it handles all the onDestoryView stuff etc):
implementation 'com.kirich1409.viewbindingpropertydelegate:viewbindingpropertydelegate:1.2.2'
You can then use this like:
class YourFragment(yourLayout: Int) : BaseFragment<YourBindingClass>() {
private val yourBinding: YourBindingClass by viewBinding()
override val viewModel: YourViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// do binding stuff
}
}
Let me know if this worked for you.
Cheers
I started using ViewBinding. After searching for an example or some advice, I ended up posting this question here.
How do I use ViewBinding with an abstract base class that handles the same logic on views that are expected to be present in every child's layout?
Scenario:
I have a base class public abstract class BaseFragment. There are multiple Fragments that extend this base class. These Fragments have common views that are handled from the base class implementation (with the "old" findViewById()). For example, every fragment's layout is expected to contain a TextView with ID text_title. Here's how it's handled from the BaseFragment's onViewCreated():
TextView title = view.findViewById(R.id.text_title);
// Do something with the view from the base class
Now the ViewBinding API generates binding classes for each child Fragment. I can reference the views using the binding, but I can't use the concrete Bindings from the base class. Even if I introduced generics to the base class, there are too many types of fragment bindings So I discarded this solution for now.
What's the recommended way of handling the binding's views from the abstract base class? Are there any best practices? I didn't find a built-in mechanism in the API to handle this scenario in an elegant way.
When the child fragments are expected to contain common views, I could provide abstract methods that return the views from the concrete bindings of the Fragments and make them accessible from the base class. (For example protected abstract TextView getTitleView();). But is this an advantage rather than using findViewById()? Are there any other (better) solutions?
Hi I have created a blog post which covers view-binding in-depth, and also includes both composition patter/delegate pattern to implement view binding as well as using inheritance checkout from the link
checkout for complete code of BaseActivity and BaseFragment along with usage
👉Androidbites|ViewBinding
/*
* In Activity
* source : https://chetangupta.net/viewbinding/
* Author : ChetanGupta.net
*/
abstract class ViewBindingActivity<VB : ViewBinding> : AppCompatActivity() {
private var _binding: ViewBinding? = null
abstract val bindingInflater: (LayoutInflater) -> VB
#Suppress("UNCHECKED_CAST")
protected val binding: VB
get() = _binding as VB
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_binding = bindingInflater.invoke(layoutInflater)
setContentView(requireNotNull(_binding).root)
setup()
}
abstract fun setup()
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
/*
* In Fragment
* source : https://chetangupta.net/viewbinding/
* Author : ChetanGupta.net
*/
abstract class ViewBindingFragment<VB : ViewBinding> : Fragment() {
private var _binding: ViewBinding? = null
abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> VB
#Suppress("UNCHECKED_CAST")
protected val binding: VB
get() = _binding as VB
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = bindingInflater.invoke(inflater, container, false)
return requireNotNull(_binding).root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setup()
}
abstract fun setup()
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
For usage, advance pattern and antipattern checkout blog Androidbites|ViewBinding
I found an applicable solution for my concrete scenario and I want to share it with you.
Note that this is not an explanation on how ViewBinding works.
I created some pseudocode below. (Migrated from my solution using DialogFragments that display an AlertDialog). I hope it's almost correctly adapted to Fragments (onCreateView() vs. onCreateDialog()). I got it to work that way.
Imagine we have an abstract BaseFragment and two extending classes FragmentA and FragmentB.
First have a look at all of our layouts. Note that I moved out the reusable parts of the layout into a separate file that will be included later from the concrete fragment's layouts. Specific views stay in their fragment's layouts. Using a common layout is important for this scenario.
fragment_a.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- FragmentA-specific views -->
<EditText
android:id="#+id/edit_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#+id/edit_name">
<!-- Include the common layout -->
<include
layout="#layout/common_layout.xml"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLayout>
</RelativeLayout>
fragment_b.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- FragmentB-specific, differs from FragmentA -->
<TextView
android:id="#+id/text_explain"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#string/explain" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#+id/text_explain">
<!-- Include the common layout -->
<include
layout="#layout/common_layout.xml"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLayout>
</RelativeLayout>
common_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:parentTag="android.widget.RelativeLayout">
<Button
android:id="#+id/button_up"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/up"/>
<Button
android:id="#+id/button_down"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#id/button_up"
android:text="#string/down" />
</merge>
Next the fragment classes. First our BaseFragment implementation.
onCreateView() is the place where the bindings are inflated. We're able to bind the CommonLayoutBinding based on the fragment's bindings where the common_layout.xml is included. I defined an abstract method onCreateViewBinding() called on top of onCreateView() that returns the ViewBinding from FragmentA and FragmentB. That way I ensure that the fragment's binding is present when I need to create the CommonLayoutBinding.
Next I am able to create an instance of CommonLayoutBinding by calling commonBinding = CommonLayoutBinding.bind(binding.getRoot());. Notice that the root-view from the concrete fragment's binding is passed to bind().
getCommonBinding() allows to provide access to the CommonLayoutBinding from the extending fragments. We could be more strict: the BaseFragment should provide concrete methods that access that binding instead of make it public to it's child-classes.
private CommonLayoutBinding commonBinding; // common_layout.xml
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container,
#Nullable Bundle savedInstanceState) {
// Make sure to create the concrete binding while it's required to
// create the commonBinding from it
ViewBinding binding = onCreateViewBinding(inflater);
// We're using the concrete layout of the child class to create our
// commonly used binding
commonBinding = CommonLayoutBinding.bind(binding.getRoot());
// ...
return binding.getRoot();
}
// Makes sure to create the concrete binding class from child-classes before
// the commonBinding can be bound
#NonNull
protected abstract ViewBinding onCreateViewBinding(#NonNull LayoutInflater inflater,
#Nullable ViewGroup container);
// Allows child-classes to access the commonBinding to access common
// used views
protected CommonLayoutBinding getCommonBinding() {
return commonBinding;
}
Now have a look at one of the the child-classes, FragmentA.
From onCreateViewBinding() we create our binding like we would do from onCreateView(). In principle it's still called from onCreateVIew(). This binding is used from the base class as described above. I am using getCommonBinding() to be able to access views from common_layout.xml. Every child class of BaseFragment is now able to access these views from the ViewBinding.
That way I can move up all logic based on common views to the base class.
private FragmentABinding binding; // fragment_a.xml
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container,
#Nullable Bundle savedInstanceState) {
// Make sure commonBinding is present before calling super.onCreateView()
// (onCreateViewBinding() needs to deliver a result!)
View view = super.onCreateView(inflater, container, savedInstanceState);
binding.editName.setText("Test");
// ...
CommonLayoutBinding commonBinding = getCommonBinding();
commonBinding.buttonUp.setOnClickListener(v -> {
// Handle onClick-event...
});
// ...
return view;
}
// This comes from the base class and makes sure we have the required
// binding-instance, see BaseFragment
#Override
protected ViewBinding onCreateViewBinding(#NonNull LayoutInflater inflater,
#Nullable ViewGroup container) {
binding = FragmentABinding.inflate(inflater, container, false);
return binding;
}
Pros:
Reduced duplicate code by moving it to the base class. Code in all fragments is now much clearer, and reduced to the essentials
Cleaner layout by moving reusable views into a layout that's included via <include />
Cons:
Possibly not applicable where views can't be moved into a commonly used layout file
Views might need to be positioned differently between fragments/layouts
Many <included /> layouts would result in many Binding classes, nothing gained then
Requires another binding instance (CommonLayoutBinding). There is not only one binding class for each child (FragmentA, FragmentB) that provides access to all views in the view hierarchy
What if views can't be moved into a common layout?
I am strongly interested in how to solve this as best practice! Let's think about it: introduce a wrapper class around the concrete ViewBinding.
We could introduce an interface that provides access to commonly used views. From the Fragments we wrap our bindings in these wrapper classes. On the other hand, this would result in many wrappers for each ViewBinding-type. But we can provide these wrappers to the BaseFragment using an abstract method (an generics). BaseFragment is then able to access the views, or work on them using the defined interface methods. What do you think?
In conclusion:
Maybe it's simply an limitation of ViewBinding that one layout needs to have its own Binding-class. If you found a good solution in cases the layout can't be shared and needs to be declared duplicated in each layout, let me know please.
I don't know if this is best practice or if there are better solutions. But while this is the only known solution for my use case, it seems to be a good start!
Here is complete example of my BaseViewBindingFragment that:
does NOT require any abstract properties or functions,
it relies on Java reflection (not Kotlin reflection) - see fun createBindingInstance, where VB generic type argument is used
package app.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
import java.lang.reflect.ParameterizedType
/**
* Base application `Fragment` class with overridden [onCreateView] that inflates the view
* based on the [VB] type argument and set the [binding] property.
*
* #param VB The type of the View Binding class.
*/
open class BaseViewBindingFragment<VB : ViewBinding> : Fragment() {
/** The view binding instance. */
protected var binding: VB? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
createBindingInstance(inflater, container).also { binding = it }.root
override fun onDestroyView() {
super.onDestroyView()
binding = null
}
/** Creates new [VB] instance using reflection. */
#Suppress("UNCHECKED_CAST")
protected open fun createBindingInstance(inflater: LayoutInflater, container: ViewGroup?): VB {
val vbType = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0]
val vbClass = vbType as Class<VB>
val method = vbClass.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java)
// Call VB.inflate(inflater, container, false) Java static method
return method.invoke(null, inflater, container, false) as VB
}
}
With minifyEnabled true, to keep the generated ViewBinding classes, add this rule into your ProGuard file:
-keepclassmembers class * implements androidx.viewbinding.ViewBinding {
*;
}
Base Class will go like this
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity(){
protected lateinit var binding : VB
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = inflateLayout(layoutInflater)
setContentView(binding.root)
}
abstract fun inflateLayout(layoutInflater: LayoutInflater) : VB
}
Now in your activity where you want to use
class MainActivity : BaseActivity<ActivityMainBinding>(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.tvName.text="ankit"
}
override fun inflateLayout(layoutInflater: LayoutInflater) = ActivityMainBinding.inflate(layoutInflater)
}
now in onCreate just use binding as per use
I created this abstract class as a base;
abstract class BaseFragment<VB : ViewBinding> : Fragment() {
private var _binding: VB? = null
val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = inflateViewBinding(inflater, container)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
abstract fun inflateViewBinding(inflater: LayoutInflater, container: ViewGroup?): VB
}
Usage;
class HomeFragment : BaseFragment<FragmentHomeBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.textViewTitle.text = ""
}
override fun inflateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeBinding {
return FragmentHomeBinding.inflate(inflater, container, false)
}
}
Here is a little different version of the #Chetan's answer with usages.
I added the #CallSuper annotation and removed type casting.
ViewBindingActivity.kt
abstract class ViewBindingActivity<VB : ViewBinding> : AppCompatActivity() {
abstract val bindingInflater: (LayoutInflater) -> VB
private var _binding: VB? = null
protected val binding: VB get() = requireNotNull(_binding)
#CallSuper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_binding = bindingInflater.invoke(layoutInflater)
setContentView(binding.root)
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
ViewBindingFragment.kt
abstract class ViewBindingFragment<VB : ViewBinding>() : Fragment() {
protected abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> VB
private var _binding: VB? = null
protected val binding: VB get() = requireNotNull(_binding)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = bindingInflater.invoke(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Usage
Activity
class HomeActivity : ViewBindingActivity<ActivityHomeBinding>() {
override val bindingInflater: (LayoutInflater) -> ActivityHomeBinding
get() = ActivityHomeBinding::inflate
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
}
}
Fragment
class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentHomeBinding
get() = FragmentHomeBinding::inflate
}
Update feb 4 2021 : I have written an article after researching and getting inspiration from many sources. This article would be updated with my future experiences with view binding as our company has now ditched the synthetic binding by almost 80%.
I have also come up with a Base Class solution that uses effectively final variables. My main goal was to :
handle all the binding lifecycle in a base class
let child class provide the binding class instance without using that route on its own (for eg if i had an abstract function abstract fun getBind():T , the child class could implement it and call it directly. I didn't wanted that as that would make the whole point of keeping bindings in base class moot ,I believe )
So here it is. First the current structure of my app. The activities won't inflate themselves, the base class would do for them:
Child Activities and Fragments:
class MainActivity : BaseActivityCurrent(){
var i = 0
override val contentView: Int
get() = R.layout.main_activity
override fun setup() {
supportFragmentManager.beginTransaction()
.replace(R.id.container, MainFragment())
.commitNow()
syntheticApproachActivity()
}
private fun syntheticApproachActivity() {
btText?.setOnClickListener { tvText?.text = "The current click count is ${++i}" }
}
private fun fidApproachActivity() {
val bt = findViewById<Button>(R.id.btText)
val tv = findViewById<TextView>(R.id.tvText)
bt.setOnClickListener { tv.text = "The current click count is ${++i}" }
}
}
//-----------------------------------------------------------
class MainFragment : BaseFragmentCurrent() {
override val contentView: Int
get() = R.layout.main_fragment
override fun setup() {
syntheticsApproach()
}
private fun syntheticsApproach() {
rbGroup?.setOnCheckedChangeListener{ _, id ->
when(id){
radioBt1?.id -> tvFragOutPut?.text = "You Opt in for additional content"
radioBt2?.id -> tvFragOutPut?.text = "You DO NOT Opt in for additional content"
}
}
}
private fun fidApproach(view: View) {
val rg: RadioGroup? = view.findViewById(R.id.rbGroup)
val rb1: RadioButton? = view.findViewById(R.id.radioBt1)
val rb2: RadioButton? = view.findViewById(R.id.radioBt2)
val tvOut: TextView? = view.findViewById(R.id.tvFragOutPut)
val cbDisable: CheckBox? = view.findViewById(R.id.cbox)
rg?.setOnCheckedChangeListener { _, checkedId ->
when (checkedId) {
rb1?.id -> tvOut?.text = "You Opt in for additional content"
rb2?.id -> tvOut?.text = "You DO NOT Opt in for additional content"
}
}
rb1?.isChecked = true
rb2?.isChecked = false
cbDisable?.setOnCheckedChangeListener { _, bool ->
rb1?.isEnabled = bool
rb2?.isEnabled = bool
}
}
}
Base Activities and Fragments :
abstract class BaseActivityCurrent :AppCompatActivity(){
abstract val contentView: Int
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(contentView)
setup()
}
abstract fun setup()
}
abstract class BaseFragmentCurrent : Fragment(){
abstract val contentView: Int
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(contentView,container,false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setup()
}
abstract fun setup()
}
As you can see the children classes were always easy to scale as base activities would do all the heavy work. and Since synthetics were being used extensively, there was not much of a problem.
To use binding classes with the previously mentioned constraints I would:
Need the child classes to implement functions that would provide data back to the parent fragments. That's the easy part, simply creating more abstract functions that return child's Binding Class's Instance would do.
Store the child class's view binding in a variable (say val binding:T) such that the base class could nullify it in on destroy ad handle the lifecycle accordingly. A little tricky since the child's Binding class instance type is not known before hand. But making the parent as generic ( <T:ViewBinding>) will do the job
returning the view back to the system for inflation. again, easy because thankfully for most of the components, the system accepts an inflated view and having the child's binding instance will let me provide a view back to the system
Preventing the child class from using the route created in point 1 directly . think about it: if a child class had a function getBind(){...} that returns their own binding class instance, why won't they use that and instead use super.binding ? and what is stopping them from using the getBind() function in the onDestroy(), where the bindings should rather not be accessed?
So that's why I made that function void and passed a mutable list into it. the child class would now add their binding to the list that would be accessed by the parent. if they don't , it will throw an NPE . If they try to use it in on destroy or an other place, it will again throw an illegalstate exception . I also create a handy high order function withBinding(..) for easy usage.
Base Binding activity and fragment:
abstract class BaseActivityFinal<VB_CHILD : ViewBinding> : AppCompatActivity() {
private var binding: VB_CHILD? = null
//lifecycle
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(getInflatedLayout(layoutInflater))
setup()
}
override fun onDestroy() {
super.onDestroy()
this.binding = null
}
//internal functions
private fun getInflatedLayout(inflater: LayoutInflater): View {
val tempList = mutableListOf<VB_CHILD>()
attachBinding(tempList, inflater)
this.binding = tempList[0]
return binding?.root?: error("Please add your inflated binding class instance at 0th position in list")
}
//abstract functions
abstract fun attachBinding(list: MutableList<VB_CHILD>, layoutInflater: LayoutInflater)
abstract fun setup()
//helpers
fun withBinding(block: (VB_CHILD.() -> Unit)?): VB_CHILD {
val bindingAfterRunning:VB_CHILD? = binding?.apply { block?.invoke(this) }
return bindingAfterRunning
?: error("Accessing binding outside of lifecycle: ${this::class.java.simpleName}")
}
}
//--------------------------------------------------------------------------
abstract class BaseFragmentFinal<VB_CHILD : ViewBinding> : Fragment() {
private var binding: VB_CHILD? = null
//lifecycle
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = getInflatedView(inflater, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setup()
}
override fun onDestroy() {
super.onDestroy()
this.binding = null
}
//internal functions
private fun getInflatedView(
inflater: LayoutInflater,
container: ViewGroup?,
attachToRoot: Boolean
): View {
val tempList = mutableListOf<VB_CHILD>()
attachBinding(tempList, inflater, container, attachToRoot)
this.binding = tempList[0]
return binding?.root
?: error("Please add your inflated binding class instance at 0th position in list")
}
//abstract functions
abstract fun attachBinding(
list: MutableList<VB_CHILD>,
layoutInflater: LayoutInflater,
container: ViewGroup?,
attachToRoot: Boolean
)
abstract fun setup()
//helpers
fun withBinding(block: (VB_CHILD.() -> Unit)?): VB_CHILD {
val bindingAfterRunning:VB_CHILD? = binding?.apply { block?.invoke(this) }
return bindingAfterRunning
?: error("Accessing binding outside of lifecycle: ${this::class.java.simpleName}")
}
}
Child activity and fragment:
class MainActivityFinal:BaseActivityFinal<MainActivityBinding>() {
var i = 0
override fun setup() {
supportFragmentManager.beginTransaction()
.replace(R.id.container, MainFragmentFinal())
.commitNow()
viewBindingApproach()
}
private fun viewBindingApproach() {
withBinding {
btText.setOnClickListener { tvText.text = "The current click count is ${++i}" }
btText.performClick()
}
}
override fun attachBinding(list: MutableList<MainActivityBinding>, layoutInflater: LayoutInflater) {
list.add(MainActivityBinding.inflate(layoutInflater))
}
}
//-------------------------------------------------------------------
class MainFragmentFinal : BaseFragmentFinal<MainFragmentBinding>() {
override fun setup() {
bindingApproach()
}
private fun bindingApproach() {
withBinding {
rbGroup.setOnCheckedChangeListener{ _, id ->
when(id){
radioBt1.id -> tvFragOutPut.text = "You Opt in for additional content"
radioBt2.id -> tvFragOutPut.text = "You DO NOT Opt in for additional content"
}
}
radioBt1.isChecked = true
radioBt2.isChecked = false
cbox.setOnCheckedChangeListener { _, bool ->
radioBt1.isEnabled = !bool
radioBt2.isEnabled = !bool
}
}
}
override fun attachBinding(
list: MutableList<MainFragmentBinding>,
layoutInflater: LayoutInflater,
container: ViewGroup?,
attachToRoot: Boolean
) {
list.add(MainFragmentBinding.inflate(layoutInflater,container,attachToRoot))
}
}
You can pass the inflate method into the abstract class
class MainFragment :
BaseFragment<MainFragmentBinding>(MainFragmentBinding::inflate) { }
abstract class BaseFragment<T : ViewBinding>(
private val viewBindingInflater: (
inflater: LayoutInflater,
parent: ViewGroup?,
attachToParent: Boolean
) -> T
) : Fragment() {
lateinit var viewBinding: T
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewBinding = viewBindingInflater(inflater, container, false)
return viewBinding.root
}
}
I think that an easy response is to use bind method of the common class.
I know this won't work in ALL cases, but it will for views with similar elements.
If I have two layouts row_type_1.xml and row_type_2.xml to which they share common elements, I can then do something as:
ROW_TYPE_1 -> CommonRowViewHolder(
RowType1Binding.inflate(LayoutInflater.from(parent.context), parent, false))
Then for type 2, instead of creating another ViewHolder that receives its own Binding class, do something as:
ROW_TYPE_2 -> {
val type2Binding = RowType2Binding.inflate(LayoutInflater.from(parent.context), parent, false))
CommonRowViewHolder(RowType1Binding.bind(type2Binding))
}
If instead it is a subset of components, inheritance could be placed
CommonRowViewHolder: ViewHolder {
fun bind(binding: RowType1Holder)
}
Type2RowViewHolder: CommonRowViewHolder {
fun bind(binding: RowType2Holder) {
super.bind(Type1RowViewHolder.bind(binding))
//perform specific views for type 2 binding ...
}
}
inline fun <reified BindingT : ViewBinding> AppCompatActivity.viewBindings(
crossinline bind: (View) -> BindingT
) = object : Lazy<BindingT> {
private var initialized: BindingT? = null
override val value: BindingT
get() = initialized ?: bind(
findViewById<ViewGroup>(android.R.id.content).getChildAt(0)
).also {
initialized = it
}
override fun isInitialized() = initialized != null
}
This is slightly modified Kotlin version of great Chetan Gupta's answer.
Avoids using "UNCHECKED_CAST".
Activity
abstract class BaseViewBindingActivity<ViewBindingType : ViewBinding> : AppCompatActivity() {
protected lateinit var binding: ViewBindingType
protected abstract val bindingInflater: (LayoutInflater) -> ViewBindingType
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = bindingInflater.invoke(layoutInflater)
val view = binding.root
setContentView(view)
}
}
Fragment
abstract class BaseViewBindingFragment<ViewBindingType : ViewBinding> : Fragment() {
private var _binding: ViewBindingType? = null
protected val binding get() = requireNotNull(_binding)
protected abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> ViewBindingType
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = bindingInflater.invoke(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}