I recently started to learn view models and data binding in Kotlin. I created a sample project where I created several fragments in one activity. I was interested to know how to implement generic fragment with data binding with viewmodel. I'm not sure if it's possible or if I'm on the right path. I searched online and find a few clues like but there is no a complete solution.
link1
link2
What I have done so far I created an abstract BaseFragment.
abstract class BaseFragment<V : BaseViewModel> : Fragment()
{
lateinit var binding: FragmentHomeBinding
lateinit var viewModel : V
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, getContentView(), container, false)
val view = binding.root
//here data must be an instance of the class MarsDataProvider
viewModel = ViewModelProviders.of(this).get(viewModel.javaClass)
setupUI()
binding.viewModel = viewModel
return view
}
abstract fun setupUI()
abstract fun getContentView() : Int
}
This is the code of the HomeFragment
class HomeFragment : BaseFragment<HomeViewModel>() {
override fun setupUI() {
viewModel.errorMessage.observe(this, Observer {
errorMessage -> if(errorMessage != null) showError(errorMessage) else hideError()
})
}
override fun getContentView(): Int {
return R.layout.fragment_home
}
private var errorSnackbar: Snackbar? = null
private fun showError(@StringRes errorMessage:Int){
Log.d("anton","showError")
errorSnackbar = Snackbar.make(binding.root, errorMessage, Snackbar.LENGTH_INDEFINITE)
errorSnackbar?.setAction(R.string.retry, viewModel.errorClickListener)
errorSnackbar?.show()
}
private fun hideError(){
Log.d("anton","hideError")
errorSnackbar?.dismiss()
}
}
here is one of the xml layouts of the fragments that I have
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="app.series.com.series4go.viewmodels.HomeViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:mutableVisibility="@{viewModel.getLoadingVisibility()}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/post_list"
android:layout_width="0dp"
android:layout_height="0dp"
app:adapter="@{viewModel.getPostListAdapter()}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
I'm not sure how to change
lateinit var binding: FragmentHomeBinding
in BaseFragment that it will be generic because I need to initialize with
binding = DataBindingUtil.inflate(inflater, getContentView(), container, false)
Edit
After playing around with this code I came to this:
BaseFragment:
abstract class BaseFragment<V : BaseViewModel, T : ViewDataBinding> : Fragment()
{
lateinit var binding: T
lateinit var viewModel : V
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, getContentView(), container, false)
val view = binding.root
viewModel = ViewModelProviders.of(this).get(getViewModelClass())
setupUI()
bindViewToModel()
return view
}
abstract fun setupUI()
abstract fun getContentView() : Int
abstract fun getViewModelClass() : Class<V>
abstract fun bindViewToModel()
}
HomeFragment
class HomeFragment : BaseFragment() {
override fun bindViewToModel() {
binding.viewModel = viewModel
}
override fun setupUI(){
binding.postList.layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
//here data must be an instance of the class MarsDataProvider
viewModel.errorMessage.observe(this, Observer { errorMessage ->
if (errorMessage != null) showError(errorMessage) else hideError()
})
}
override fun getContentView(): Int {
return R.layout.fragment_home
}
private var errorSnackbar: Snackbar? = null
private fun showError(@StringRes errorMessage:Int){
Log.d("anton","showError")
errorSnackbar = Snackbar.make(binding.root, errorMessage, Snackbar.LENGTH_INDEFINITE)
errorSnackbar?.setAction(R.string.retry, viewModel.errorClickListener)
errorSnackbar?.show()
}
private fun hideError(){
Log.d("anton","hideError")
errorSnackbar?.dismiss()
}
override fun getViewModelClass(): Class<HomeViewModel> {
return HomeViewModel::class.java
}
}
The only thing that I don't like in this solution is the function bindViewToModel, every fragment which extends the base fragment will need to implement it the same way across all fragments. Not sure how to move it to the base fragment as the basefragment does not know about any of the variables of the layout (because it's abstract).
I will be happy to know if there are places to improve this design or to fix this issue.
Thanks
Edit 2
Following the solution of @Oya Canlı I managed to remove the abstract bindViewToModel this is the final code in case someone will be interested to use it.
BaseFragment:
abstract class BaseFragment<V : BaseViewModel, T : ViewDataBinding> : Fragment()
{
lateinit var binding: T
lateinit var viewModel : V
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, getContentView(), container, false)
val view = binding.root
viewModel = ViewModelProviders.of(this).get(getViewModelClass())
setupUI()
binding.setVariable(BR.viewModel, viewModel)
return view
}
abstract fun setupUI()
abstract fun getContentView() : Int
abstract fun getViewModelClass() : Class<V>
}
HomeFragment
class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>() {
override fun setupUI(){
binding.postList.layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
//here data must be an instance of the class MarsDataProvider
viewModel.errorMessage.observe(this, Observer { errorMessage ->
if (errorMessage != null) showError(errorMessage) else hideError()
})
}
override fun getContentView(): Int {
return R.layout.fragment_home
}
private var errorSnackbar: Snackbar? = null
private fun showError(@StringRes errorMessage:Int){
Log.d("anton","showError")
errorSnackbar = Snackbar.make(binding.root, errorMessage, Snackbar.LENGTH_INDEFINITE)
errorSnackbar?.setAction(R.string.retry, viewModel.errorClickListener)
errorSnackbar?.show()
}
private fun hideError(){
Log.d("anton","hideError")
errorSnackbar?.dismiss()
}
override fun getViewModelClass(): Class<HomeViewModel> {
return HomeViewModel::class.java
}
}
The generic type for databinding classes is ViewDataBinding. So you can get your binding instance as:
val binding = DataBindingUtil.inflate<ViewDataBinding>(
inflater, getContentView(), container, false)
But then you cannot set viewModel like binding.viewModel, since the instance of generic binding class won't have a setter called setViewModel. What you can use instead is the generic setter setVariable:
binding.setVariable(BR.viewModel, viewModel)
BR is a generated class which contains all the variables you used with data binding. This method above is not type-safe and it doesn't check whether the mentioned BR variable is indeed located in the specific binding class. That's why normally it is better to use specific setters. But when you don't know the specific binding class, like in your case, that's the way to go.
You can also use a similar approach for writing a reusable recyclerview adapter.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With