From a Google Codelab (can't remember which one), they adviced doing the following for fragments:
class MyFragment : Fragment() {
private var _binding: MyFragmentBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
_binding = MyFragmentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
And then accessing the views with e.g. binding.button1.
Is there a specific reason for doing it like this, with _binding and binding? Are there better methods? Perhaps an extension for Fragments - like a BaseFragment - to avoid code duplication.
It's not recommended to use BaseFragment or BaseActivity or BaseViewModel... it will just add boilerplate code to your project.
For binding you can just use it like this:
Declaration:
private var binding: MyFragmentBinding? = null
onCreateView:
binding = MyFragmentBinding.inflate(inflater, container, false)
binding?.root
Usage:
binding?.button...
binding?.text...
binding?.cardView...
onDestroyView:
binding = null
And everything is going to work just fine but we use the null check a lot (?) and it's making the code messy and we need to get a lot of null checks if we need something from a certain view, so we are sure that between onCreateView and onDestroyView, the binding is not null so we have _binding and binding:
private var _binding: MyFragmentBinding? = null
private val binding get() = _binding!!
We make _binding mutable with var so we can give it a value, and we make it nullable so we can clear it later.
And we have binding that have a custom getter so that means that each time we call binding it's going to return the latest value from _binding and force that it's not null with !!.
Now we seperate our variables, we have _binding to initialize and clear our binding, and we have binding that is immutable and not nullable to use it only for accessing views without null check ?
See this question for some answers about the reason why binding needs to be nullable in a fragment.
See this answer of mine where I linked some articles about the problems with BaseFragments. You can usually achieve the code reuse without the drawbacks of inheritance by using extension properties and functions.
Here is an example of a property delegate that takes care of releasing the ViewBinding reference when necessary and rebuilding it when necessary. If you use this, all you need is a single binding property. Example is from the article about this tool.
class FirstFragment: Fragment(R.layout.first_fragment) {
private val binding by viewBinding(FirstFragmentBinding::bind)
override fun onViewCreated(view: View, bundle: Bundle?) {
super.onViewCreated(view, bundle)
binding.buttonPressMe.onClick {
showToast("Hello binding!")
}
}
I just saw that CommonsWare has adressed this issue in this post.
Here is the parent class:
abstract class ViewBindingFragment<Binding : ViewBinding>(
private val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> Binding
) : Fragment() {
private var binding: Binding? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
return bindingInflater(inflater, container, false).apply { binding = this }.root
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
protected fun requireBinding(): Binding = binding
?: throw IllegalStateException("You used the binding before onCreateView() or after onDestroyView()")
protected fun useBinding(bindingUse: (Binding) -> Unit) {
bindingUse(requireBinding())
}
}
He then subclasses ViewBindingFragment like so:
class ListFragment :
ViewBindingFragment<TodoRosterBinding>(TodoRosterBinding::inflate) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
useBinding { binding ->
binding.items.layoutManager = LinearLayoutManager(context)
}
}
}
Though I am not sure it will eventually lead to less code, if useBinding { binding -> } needs to be called in several functions.
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
}
}
I am starting on a new project and I will be working in a team of 10 developers. I am setting up the base structure for our Android app. As I am working with a team and I want everyone to follow the same structure i.e creating ViewModel for each fragment and use data binding. How can I make it strict so that developers get an error if they don't create ViewModel for their Fragment?
So I have created the below BaseFragment:
abstract class BaseFragment<out VM : BaseViewModel, DB : ViewDataBinding> : Fragment() {
open lateinit var binding: DB
private fun init(inflater: LayoutInflater, container: ViewGroup?) {
binding = DataBindingUtil.inflate(inflater, getLayoutRes(), container, false)
}
#LayoutRes
abstract fun getLayoutRes(): Int
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
init(inflater, container)
super.onCreateView(inflater, container, savedInstanceState)
return binding.root
}
open fun refresh() {}
}
How can I improve it more?
One of the possible ways to improve your base fragment is to use reified like this:
protected inline fun <reified T : ViewModel> getViewModel(): T =
ViewModelProviders.of(this)[T::class.java]
and call is:
private val loginViewModel: LoginViewModel = getViewModel()
Useful links on this approach:http://www.albertgao.xyz/2018/05/22/3-ways-to-handle-view-model-creation-in-android-with-dagger-and-kotlin/
How does the reified keyword in Kotlin work?
https://proandroiddev.com/how-reified-type-makes-kotlin-so-much-better-7ae539ed0304
I've been struggling with this issue for a couple hours and fond no solution.
I'am building an android application using MVP pattern and DataBinding, and I wanted to reduce the boilerplate code of creating fragments with a specific binding, using a specific Presenter.
Therefore I declared the following abstract class:
abstract class AbstractBoundFragment : Fragment(), Screen {
protected abstract var presenter: Presenter<Screen>
protected abstract var binding: ViewDataBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = getBinding(inflater, container)
return binding.root
}
override fun onAttach(context: Context) {
super.onAttach(context)
presenter.attachScreen(this)
}
override fun onDetach() {
presenter.detachScreen()
super.onDetach()
}
protected abstract fun getBinding(inflater: LayoutInflater, container: ViewGroup?): ViewDataBinding
}
When I'm extending this class in a concrete fragment, I get the following type error:
"Var-property type is 'FragmentArtistListBinding', which is not a type of overridden protected abstract var binding: ViewDataBinding defined in xy.z.ui.utils.AbstractBoundFragment."
FragmentArtistListBinding is the class generated by android data binding, and it clearly extends ViewDataBinding.
I get the same error for the overridden presenter. ArtistsPresenter extends the abstract Presenter<Screen> class correctly.
class ArtistListFragment : AbstractBoundFragment(),
ArtistListScreen {
#Inject
override lateinit var presenter: ArtistsPresenter // type error here
override lateinit var binding: FragmentArtistListBinding // and type error here
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentArtistListBinding {
return FragmentArtistListBinding.inflate(inflater, container, false)
}
override fun showArtist(artistName: String) {
// do stuff
}
}
What am I missing here?
As opposed to a val, which is covariant, a var type is invariant. Think about the following case:
abstract class Base {
abstract var x: Any
}
class Derived : Base {
override var x: String
}
fun main() {
val base: Base = Derived()
base.x = 1
}
Base.x is Any, so why wouldn't you assign it an Int value of 1? Yet clearly this is wrong because you're doing it to an instance of Derived.