2

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)

  • BaseRefreshViewModel (isRefreshing: MutableLiveData)
    • FirstViewModel
      • first_list_layout.xml (import FirstViewModel)
    • SecondViewModel
      • second_list_layout.xml (import SecondViewModel)

WORKING (one xml file, but not LiveData)

  • BaseRefreshViewModel (isRefreshing: ObservableBoolean)
    • FirstViewModel
      • list_layout.xml (import BaseRefreshViewModel)
    • SecondViewModel
      • list_layout.xml (import BaseRefreshViewModel)

NOT WORKING (one xml file with LiveData)

  • BaseRefreshViewModel (isRefreshing: MutableLiveData)
    • FirstViewModel
      • list_layout.xml (import BaseRefreshViewModel)
    • SecondViewModel
      • list_layout.xml (import BaseRefreshViewModel)
miecio
  • 315
  • 1
  • 4
  • 19
  • post your layout file too for better understanding of the error – Faiizii Awan Jul 16 '19 at 07:21
  • @faiizii XML added. – miecio Jul 16 '19 at 07:26
  • did you intialize viewModel before setting it to layout ?? (binding.viewModel = stationViewModel before this line your viewModel should be intialized) i think you are setting null view model to binding – Faiizii Awan Jul 17 '19 at 04:49
  • I did initialize ViewModel in onCreate method... If I didn't, it won't be working at all, don't you think? But it working, I write this like 4 time already to You, but stops when I import this abstract BaseRefreshViewModel with LiveData instead FirstViewModel/SecondViewModel in XML - with ObservableBoolean working always so I'll stick with that solution for now until they fix bug (which is obviously is). – miecio Jul 17 '19 at 06:17
  • onCreateView method which you posted. where were you initialized stationViewModel? – Faiizii Awan Jul 17 '19 at 06:27
  • Like I wrote, in onCreate method, You didn't believe me if I wrote it only if I post it? Geez, here you go, question updated. – miecio Jul 17 '19 at 06:34
  • 1
    I also encountered this problem, temporarily min cost only use ObservableFiled instead of MutableLiveData in xml. Then, in the BaseViewModel change ObservableFiled by observe MutableLiveData, there is no other solution, only wait for the official fix. – xiangmao Aug 22 '19 at 02:29

5 Answers5

3

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'
Fernando Gallego
  • 4,064
  • 31
  • 50
  • `apply plugin: 'kotlin-kapt'` just worked! I was sure I am already using this but noticed I wasn't. Simple and too stupid for me! – sud007 Feb 19 '20 at 07:06
1

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

slezadav
  • 6,104
  • 7
  • 40
  • 61
1

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

    dataBinding {
        enabled = true
        version = "3.4.1"
    }
eZet
  • 25
  • 9
  • Yep, it is working on updated databinding lib. It broke some other things tho, but this is story for another time... – miecio Nov 07 '19 at 22:11
0

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

Faiizii Awan
  • 1,615
  • 1
  • 13
  • 28
  • But this is should be unnecessary and I'ts consistent, because when I split this into two XMLs and import there FirstViewModel/SecondViewModel it compile and works fine without creating variable - it extract Boolean from MutableLivea data while binding automatically just fine. Also see what I wrote later - I don't need to do what You write because in fact in BindingAdapter I get Boolean object - when I tried get MutableLiveData i get ClassCastException. – miecio Jul 16 '19 at 08:19
  • then please check did you call binding.setLifecycleOwner(this) ? – Faiizii Awan Jul 16 '19 at 09:37
  • This is correct but overkill. Just check if you have `apply plugin: 'kotlin-kapt'` set in the `app.gradle` NOT your top level one. – sud007 Feb 19 '20 at 07:07
0

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);
}
Ehsan Aminifar
  • 515
  • 4
  • 15