39

I am using android data binding library. If I want to make a view visible I can write something like this:

<TextView
            android:id="@+id/label_status"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="@{habitListViewModel.message}"
            app:visibility="@{habitListViewModel.hasError ? View.VISIBLE : View.GONE}" />

Is there an option to bind to a refresh property of swipeRefreshLayout in a similar (xml) way?

Currently I am setting it in code by calling setRefreshing(true/false) but would love to make it in xml to be consistent.

Entreco
  • 12,738
  • 8
  • 75
  • 95
daneejela
  • 13,081
  • 7
  • 38
  • 50

4 Answers4

94

No need to hack. The key is to look for the public methods in SwipeRefreshLayout documentation. In general, Databinding will look for the corresponding name without the set part. E.g. you'll find there:

The OnRefreshListener

Since OnRefreshListener is a public interface, with a single method, you can directly use this in your binding, like so:

app:onRefreshListener="@{() -> viewModel.onRefresh()}"

Updating the Status

For this one, you use the other public method, which translates to:

app:refreshing="@{viewModel.isLoading}"

All together, it can look something like this:

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

    <android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"          
        android:layout_height="match_parent" />

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

viewmodel - kotlin :

class ViewModel(
        private val provider: DataProvider
    ) : DataProvider.Callback {

    /* isLoading - Needs to be public for Databinding! */
    val isLoading = ObservableBoolean()

    /* onRefresh() - Needs to be public for Databinding! */
    fun onRefresh() {
        isLoading.set(true)
        provider.fetch(this)
    }

    fun onReady() = isLoading.set(false)

    fun onError(oops: Exception) = isLoading.set(false)
}

viewmodel - java:

public class ViewModel implements DataProvider.Callback {
    public ObservableBoolean isLoading = new ObservableBoolean();
    private DataProvider provider;

    MasterViewModel(@NonNull final DataProvider provider) {
        this.provider = provider;
    }

    /* Needs to be public for Databinding */
    public void onRefresh() {
        isLoading.set(true);
        provider.fetch(this);
    }

    public void onReady(List results){
        isLoading.set(false);
    } 

    public void onError(Exception oops){
        isLoading.set(false);
        Log.e("Stack", oops);
    }
}
Entreco
  • 12,738
  • 8
  • 75
  • 95
  • 2
    This is how you should use the data binding library and should be the accepted answer. – oldergod May 02 '17 at 00:55
  • 2
    app:onRefreshListener does not call method onRefresh in my case.It only gets the spinner spinning but does not call onRefresh. Did it work for you? – GotaloveCode Aug 24 '19 at 13:04
  • app:onRefreshListener only sets the listener. To actually start refreshing, you need to trigger the event. In this sample that is done by calling `isLoading.set(true)` – Entreco Aug 24 '19 at 20:14
  • 1
    @Entreco for me, onRefresh() is never called when I swipe down to refresh. So using your example, my problem is that `provider.fetch(this)` is never being called. – user1795832 Aug 26 '20 at 22:00
  • 1
    @user1795832 You have to set "viewModel" to it's current instance in your fragment (or activity). – The incredible Jan Oct 21 '20 at 13:01
  • @Entreco In principle your code works, but refreshing state is set much too fast to false if you use a RecyclerView with LiveData list. I use a web request to set the list from it's response body. This is done in no time. The refreshing indicator disappears, but the RecyclerView needs several seconds (doesn't matter how much items - tested with 5 and with 500) to update it's data afterwards. Ugly... – The incredible Jan Oct 28 '20 at 14:01
36

UPDATED: As databinding maps from xml attribute name to set{AttributeName}, you can just use app:refreshing, as databinding will successfully supply the value to setRefreshing method of SwipeRefreshLayout (which luckily for us exists and is public):

<android.support.v4.widget.SwipeRefreshLayout
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:refreshing="@{habitListViewModel.isLoading}">
    ...
    //ListView,RecyclerView or something
</android.support.v4.widget.SwipeRefreshLayout>

That's it! Note, that you can simply use @{true} or @{false} instead of @{habitListViewModel.isLoading}. Hope that helps.

romtsn
  • 11,704
  • 2
  • 31
  • 49
  • Sorry for late response...but, how will swipeRefreshLayout know what is expected behavior when setting app:refreshing value to true/false? Shouldn't I bind it somewhere to some behavior? How will it connect app:refreshing property with it's internal refreshing field that sets refreshing behavior? – daneejela Dec 07 '16 at 12:22
  • That's why I called it "a little hack" - the main thing here is that Android DataBinding is just looking for `set{Name}` method of the object when you bind to its property. So that's why we called our custom property `refreshing`. So when you bind to it some value it will just look for `setRefreshing` method of `SwipeRefreshLayout` object, which our object exactly contains, so the behavior will be the same as you would call `setRefreshing(true/false)` programmatically :) – romtsn Dec 07 '16 at 12:46
  • Now I understand! Smart :) Thanks for additional explanation! – daneejela Dec 07 '16 at 13:06
  • You're welcome:) This is an unusual case, so the explanation is must have :) – romtsn Dec 07 '16 at 13:13
  • 1
    No need for the attrs.xml, just use `android:refreshing="@{habitListViewModel.isLoading}"` – Entreco Apr 18 '17 at 20:48
4

Make sure you have set your binding.viewModel to its current instance in your fragment/Activity.

Here is what was missing in my case in the fragment.

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    FragmentDiscoverMoviesBinding binding =
            DataBindingUtil.inflate(inflater, R.layout.fragment_discover_movies, container, false);
    viewModel = obtainViewModel(getActivity());       //get the viewmodel from the viewmodelFactory
    binding.setViewModel(viewModel);

    View rootView = binding.getRoot();
    return rootView;
}

fragment_discover_movies.xml

<data>
    <variable
        name="viewModel"
        type="com.ajdi.yassin.popularmovies.ui.movieslist.discover.DiscoverMoviesViewModel" />
</data>

<android.support.v4.widget.SwipeRefreshLayout
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:onRefreshListener="@{()-> viewModel.refresh()}"
    app:refreshing="@{viewModel.isLoading}">
    ...
    //ListView,RecyclerView or something
</android.support.v4.widget.SwipeRefreshLayout>

And then in your viewmodel, define your refresh function.

    public class DiscoverMoviesViewModel extends ViewModel {

        public MutableLiveData<Boolean> isLoading = new MutableLiveData<>(true);
        .
        .
        .
        public void refresh(){
    
            isLoading.setValue(true);
            //do your task
        
        }
 }
Sonu Sourav
  • 2,926
  • 2
  • 12
  • 25
1

Just use this app:refreshing="@{booleanValueHere}". Data Binding will automatically detect the reference back to the isRefreshing function.

Viven
  • 627
  • 7
  • 14