How to initialize and use UI elements - android

I am currently working on my first Android app using Kotlin. In my activity are a lot of UI elements which I use to show dynamic information (see example below). For performance reasons I learned:
"Define a variable in the class and initialize it in the onCreate()
method."
This is kind of messy and my question is: are there other techniques to fulfill the same task but have a cleaner code? The variables are used in other methods later.
class MainActivity : AppCompatActivity() {
private lateinit var text_1: TextView
private lateinit var text_2: TextView
private lateinit var text_3: TextView
private lateinit var text_4: TextView
[...]
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
text_1 = findViewById(R.id.text1)
text_2 = findViewById(R.id.text2)
text_3 = findViewById(R.id.text3)
text_4 = findViewById(R.id.text4)
[...]
}

From ViewBinding official docs:
View binding is a feature that allows you to more easily write code that interacts with views
First, enable ViewBinding in your module:
android {
...
buildFeatures {
viewBinding true
}
}
Then, if you're calling views from activity, you should:
private lateinit var binding: ResultProfileBinding
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
binding = ResultProfileBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
}
and then you use binding instance to call the views:
binding.name.text = viewModel.name
binding.button.setOnClickListener { viewModel.userClicked() }
If you are calling views from a fragment, you should do it like following to avoid leaks:
private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = ResultProfileBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

In Kotlin you just need to use the id directly without binding. The class will import this:
import kotlinx.android.synthetic.main.<your_layout_xml>.*
In this case it will import: kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
text_1.text = "Text1"
text_2.text = "Text2"
[...]
}

Related

Best practices for Fragments + ViewBinding

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.

Kotlin RecyclerView + fragment add retrofit2 response from api, with Drawer menu

I'm updating my knowledge of Android, and I want to make an app with a Drawer menu, call an api and display the values inside a fragment. Starting from the template created by android studio itself, I have followed this tutorial:https://howtodoandroid.com/mvvm-retrofit-recyclerview-kotlin/ but I have a problem when programming the MainActivity.
Android studio template create this fragment (only changes the name of fragments):
class CheckListFragment : Fragment() {
private var _binding: FragmentCheckListBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val checklistViewModel =
ViewModelProvider(this).get(CheckListViewModel::class.java)
_binding = FragmentCheckListBinding.inflate(inflater, container, false)
val root: View = binding.root
val textView: TextView = binding.textChecklist
checklistViewModel.text.observe(viewLifecycleOwner) {
textView.text = it
}
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
The issue is that I don't know how I should call the viewmodel in that section, since I've tried in different ways, reading and reading examples, but none is exactly what I need. If you need me to show more parts of the code (viewmodel, viewmodelfactory etc. I can add it without any problem, although they are practically the same as those described in the tutorial)
In that example, in the MainActivity class we have this:
class MainActivity : AppCompatActivity() {
private val TAG = "MainActivity"
private lateinit var binding: ActivityMainBinding
lateinit var viewModel: MainViewModel
private val retrofitService = RetrofitService.getInstance()
val adapter = MainAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel = ViewModelProvider(this, MyViewModelFactory(MainRepository(retrofitService))).get(MainViewModel::class.java)
binding.recyclerview.adapter = adapter
viewModel.movieList.observe(this, Observer {
Log.d(TAG, "onCreate: $it")
adapter.setMovieList(it)
})
viewModel.errorMessage.observe(this, Observer {
})
viewModel.getAllMovies()
}
But here not use fragments.
I would greatly appreciate help, or a link where I can see an example of this

ViewBinding pratices

Is there a more generic way to init this two initialization lines?
private var _binding: MyFragmentViewBinding? = null
private val binding get() = _binding!!
Should we call every time binding
binding.cancelButton.setOnClickListener { }
binding.homeButton.setOnClickListener { }
binding.aboutButton.setOnClickListener { }
Or to create class variable?
cancelButton = binding.cancelButton
binding.cancelButton.setOnClickListener{}
And, should we set binding = null in adapter?
I think it is more of a personal preference. I like to do it with extension and higher-order function
fun <T : ViewDataBinding> Fragment.getDataBinding(layout: Int, container: ViewGroup?): T {
val binding: T = DataBindingUtil.inflate(layoutInflater, layout, container, false)
binding.lifecycleOwner = viewLifecycleOwner
return binding
}
My Fragment looks like this.
class InviteFragment : Fragment() {
private lateinit var binding: FragmentInviteBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = getDataBinding(R.layout.fragment_invite, container)
return binding.apply {
inviteAll.setOnClickListener(onInviteAllClick)
// You can set as many click listener here
// or some initialization related to view such as
// setting up recycler view adapter and decorators
}.root
}
private val onInviteAllClick = View.OnClickListener {
// Invite users
}
}
By doing things like this your onCreateView will be more readable and never going to get very long.

Android Studio: Help ... LiveData keeps separate values from different fragments

Please help me with a problem.
I have the activity_main.xml set up with a score text box, a test button and a fragment container that swaps between 2 fragments.
The fragments are basically containers for buttons.
The "buttonTest" does exactly what "button1" from the fragment does (increments the score) but the test button (located on activity_main.xml) works and the fragment one ... does not
When viewing the logs ...i see that score does update when i click both buttons but with different values.
If i click "buttonTest" it adds 5 to score so score = 5. On another click it adds another 5 so score = 10
If i click in the fragment on "button1" ... calling the same method score is now 1 then 2 then 3.
If i now click "buttonTest" the score is 15.
The problem is that the LiveData keeps separate values depending on where the method was called.
Allso the MainActivity observer does not update on the fragment call... only on the activity one.
Please help.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val scoreSwitch = binding.scoreSwitch
binding.buttonTest.setOnClickListener { viewModel.adaugaTren(5) }
// score observer
viewModel.scor.observe(this, Observer { newScore ->
binding.txtPunctaj.text = newScore.toString()
//todo sterge LOG
Log.d("test","Scor Observer triggers")
})
.......
class MainViewModel: ViewModel(){
private val _score = MutableLiveData(0)
val score: LiveData<Int>
get() = _score
fun adaugaTren(valoare:Int) {
_scor.value = _scor.value?.plus(valoare)
listaTrenuri.add(valoare)
//testing
Log.d("test","Trenuri: ${scor.value}")
updateDisplay()
}
fun updateDisplay(){
// _trenuriDetinute.value = listaTrenuri.toString()
_trenuriDetinute.value = TextUtils.join(", ",listaTrenuri)
//testing
Log.d("test","UpdateDisplay: ${scor.value}")
}
...
}
class TrenuriFragment : Fragment() {
private lateinit var binding: FragmentTrenuriBinding
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
binding = DataBindingUtil.inflate(inflater,R.layout.fragment_trenuri, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initButtons()
}
fun initButtons(){
binding.button1.setOnClickListener { viewModel.adaugaTren(1)}
....
}
}
In TrenuriFragment use activityViewModels
// viewModels is scoped in fragment but you need activity
private val viewModel: MainViewModel by activityViewModels()

How to use ViewBinding with an abstract base class

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
}
}

Categories

Resources