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