7

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
    }

}

1 Answer 1

11

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.

Sign up to request clarification or add additional context in comments.

4 Comments

I updated my question, thanks for this solution but I wanted something more abstract in the base fragment. See my edit, maybe you will have a better solution or fixing a small issue that I have right now with it.
hmm, then binding.viewModel gives an error, right? Since it doesn't have a setter setViewModel in the generic binding instance. But there is a generic setter, setVariable that you can use. I'll edit the answer. Could you try it and let me know if it works?
Thank you!, this line was what I was looking for :)
You're welcome! Great to here it worked. May be I'll use it as well. So thanks for the question! :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.