miecio
miecio

Reputation: 325

Problem with Databinding and MutableLiveData

NOVEMBER 2019 UPDATE - it is working as intended now on the latest version.

ORIGINAL POST:

I'm binding MutableLiveData to my SwipeRefreshLayout via publicly exposed function setRefreshing (app:refreshing in XML) and everything works fine by the time... But let in introduce my app architecture.

I have abstract ViewModel with MutableLiveData when I change its value according to refresh the status.

Then I have two ViewModels (let me name them FirstViewModel and SecondViewModel) inherited from this abstract, name it BaseRefreshViewModel. First I had two practically identical XML files, differing only with "data" node when in first XML I import FirstViewModel and in second - corresponding SecondViewModel.

I was horrible, so I merged this into one XML and import this BaseRefreshViewModel (list_layout.xml):

<?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="my.package.BaseRefreshViewModel" />
    </data>

    <androidx.coordinatorlayout.widget.CoordinatorLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/coordinator_layout">

        <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:refreshing="@{viewModel.isRefreshing}"
                android:id="@+id/swipe_layout">

            <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/station_list"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    app:adapter="@{viewModel.stations}"/>

        </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
    </androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

And then compiler start going crazy - it says:

Cannot find a setter for <androidx.swiperefreshlayout.widget.SwipeRefreshLayout app:refreshing> that accepts parameter type 'androidx.lifecycle.MutableLiveData'

If a binding adapter provides the setter, check that the adapter is annotated correctly and that the parameter type matches.

Ok, so I wrote my own BindingAdapter (changing of course to app:refresh in SwipeRefreshLayout):

@BindingAdapter("refresh")
fun setRefreshing(view: SwipeRefreshLayout, refreshing: Boolean) {
    view.isRefreshing = refreshing
}

Still the same issue, then I changed BindingAdapter to:

@BindingAdapter("refresh")
fun setRefreshing(view: SwipeRefreshLayout, refreshing: MutableLiveData<Boolean>) {
    refreshing.value?.let { view.isRefreshing }
}

And it starts compiling, but after run my app crash with error:

Caused by: java.lang.ClassCastException: java.lang.Boolean cannot be cast to androidx.lifecycle.MutableLiveData

No shit Sherlock... What is funny that when I change import in my XML file from BaseRefreshViewModel to FirstViewModel/SecondViewModel it starts compiling just fine even without my BindingAdapter (I can't leave it like this of course because I have a different list of object in ViewModels which I'm binding to my adapter).

Here is my ViewModel initialization in fragment:

lateinit var stationViewModel: FirstViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        stationViewModel = ViewModelProviders.of(requireActivity()).get(FirstViewModel::class.java)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        binding = DataBindingUtil.inflate(inflater, R.layout.list_layout, container, false)
        binding.viewModel = stationViewModel
        binding.lifecycleOwner = this
        return binding.root
    }

And ViewModel itself:

abstract class BaseRefreshViewModel(application: Application) : AndroidViewModel(application) {

    val isRefreshing = MutableLiveData<Boolean>().apply { value = false }


    val receiver = object : StatusReceiver.Receiver {
        override fun onReceiveResult(resultCode: Int, resultData: Bundle) {
            when (resultCode) {
                StatusReceiver.STATUS_RUNNING -> isRefreshing.value = true
                StatusReceiver.STATUS_IDLE -> isRefreshing.value = false
                StatusReceiver.STATUS_NO_CONNECTION -> isRefreshing.value = false
                StatusReceiver.STATUS_ERROR -> isRefreshing.value = false
            }
        }
    }

    abstract fun refresh()

}

How can I overpass this without going back to creating two XML files with different ViewModel imported?

I'm using Android Studio 3.5 Beta 5 just to take advantage with improved error messages with DataBinding.

UPDATE:

When I change MutableLiveData to ObservableBoolean() it compile and run fine... But I don't wanna stick with this, I want to use LiveData with its Lifecycle advantages. It's just shows how Databinding compiler is bugged right now I think.

SUMMARY:

WORKING (two different xml, practically the same)

WORKING (one xml file, but not LiveData)

NOT WORKING (one xml file with LiveData)

Upvotes: 2

Views: 10282

Answers (5)

Fernando Gallego
Fernando Gallego

Reputation: 4116

Worked for me after applying the plugin kotlin-kapt on a kotlin project

apply plugin: 'kotlin-kapt'

after that, the LiveData<T> is unwrapped into T when bound to fields.

For the record, I also include these libraries

    // Lifecycle
    implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-rc01'
    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-rc01'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-rc01'

Upvotes: 3

eZet
eZet

Reputation: 25

Another option is to use databinding 3.4.1, which works as expected.

    dataBinding {
        enabled = true
        version = "3.4.1"
    }

Upvotes: 1

slezadav
slezadav

Reputation: 6141

Encountered this error today too. One possible, but a little ugly workaround is to make your BindAdapter accept Object and then cast it to whatever you need.

Change @BindingAdapter("refresh") fun setRefreshing(view: SwipeRefreshLayout, refreshing: Boolean)

to @BindingAdapter("refresh") fun setRefreshing(view: SwipeRefreshLayout, refreshing: Object)

and then cast refreshing it to Boolean

Upvotes: 1

Ehsan Aminifar
Ehsan Aminifar

Reputation: 525

In your XML :

 <android.support.v4.widget.SwipeRefreshLayout
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 app:refreshing="@{viewModel.isLoading}"
 app:onRefreshListener="@{() -> viewModel.onRefresh()}">

        <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/station_list"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:adapter="@{viewModel.stations}"/>

    </android.support.v4.widget.SwipeRefreshLayout>

In your ViewModel :

    public MutableLiveData <Boolean >isLoading = new MutableLiveData ();

/* Needs to be public for Databinding */
public void onRefresh() {
    isLoading.setValue(true);
    // your logic
}

public void onError(Exception oops){
    isLoading.setValue(false);
    Log.e("Stack", oops);
}

Upvotes: 0

Faiizii Awan
Faiizii Awan

Reputation: 1710

Hi Hope you doing good.

The problem is in the app:refreshing="@{viewModel.isRefreshing}".

The attribute app:refershing accept only Boolean value. whereas you are trying to give it a LiveData value. which results in

Caused by: java.lang.ClassCastException: java.lang.Boolean cannot be cast to androidx.lifecycle.MutableLiveData

so what you can do is:

  1. Create a variable of Boolean type in Data tag

    <variable
        name="refreshing"
        type="Boolean" />
    
  2. Observe your MutableLiveData

  3. Set the Boolean variable to DataBinding variable in observer. yourBinding.setRefreshing(yourObserverBooleanVariable);

Note: I wrote it according to java syntax

Upvotes: 0

Related Questions