Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic form of binding data android

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
    }

}
like image 976
Anton Makov Avatar asked Sep 02 '19 22:09

Anton Makov


1 Answers

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.

like image 135
Oya Canli Avatar answered Oct 19 '22 22:10

Oya Canli