9

I have Portable project in Visual Studio 2015 with a ListView that gets populated with some data via an API call through the following function refreshData:

async Task refreshData()
{
    myListView.BeginRefresh();
    var apiCallResult = await App.Api.myApiGetCall();            
    myListView.ItemsSource = apiCallResult;
    myListView.EndRefresh();    
}

refreshData() is called in

protected override void OnAppearing()
{
    base.OnAppearing();
    refreshData();
}

Everything is working fine except on Android where the refresh indicator is not stopping or disappearing on EndRefresh() when the page is initially loaded. The page is in a TabbedPage so I can go to a different tab and then return to this page and the refresh indicator properly starts and stops with completion of my API call.

Why is refresh is not stopping when the page initially loads on Android? Any help would be appreciated.

Note: This works perfectly fine when I run on iOS.

So far I've tried:

  1. replacing myListView.BeginRefresh() with myListView.IsRefreshing = true and myListView.EndRefresh() with myListView.IsRefreshing = false

  2. Using Device.BeginInvokeOnMainThread(() => {//update list and endRefresh}).

  3. Using async void refreshData() instead of async Task refreshData().

DerFlickschter
  • 1,059
  • 1
  • 9
  • 18

3 Answers3

4

Personally I can get this problem when I start ListView refreshing in the Page Contructor and stop it after the data is loaded. Sometimes (quite often) Xamarin.Forms ListView doesn't cancel refreshing animation.

I believe you faced with a quite common issue with Android SwipeRefreshLayout: it may not stop refreshing animation after setRefreshing(false) called. Native Android developers use the following approach:

swipeRefreshLayout.post(new Runnable() {
    @Override
        public void run() {
            mSwipeRefreshLayout.setRefreshing(refreshing);
    }
});

Interestingly, Xamarin.Forms uses this approach when it sets initial refreshing status (code); however, it is not enough. You need a custom renderer:

public class ExtendedListViewRenderer : ListViewRenderer
{
    /// <summary>
    /// The refresh layout that wraps the native ListView.
    /// </summary>
    private SwipeRefreshLayout _refreshLayout;

    public ExtendedListViewRenderer(Android.Content.Context context) : base(context)
    {
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _refreshLayout = null;
        }
        base.Dispose(disposing);
    }

    protected override void OnElementChanged(ElementChangedEventArgs<ListView> e)
    {
        base.OnElementChanged(e);
        _refreshLayout = (SwipeRefreshLayout)Control.Parent;
    }

    protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == ListView.IsRefreshingProperty.PropertyName)
        {
            // Do not call base method: we are handling it manually
            UpdateIsRefreshing();
            return;
        }
        base.OnElementPropertyChanged(sender, e);
    }

    /// <summary>
    /// Updates SwipeRefreshLayout animation status depending on the IsRefreshing Element 
    /// property.
    /// </summary>
    protected void UpdateIsRefreshing()
    {
        // I'm afraid this method can be called after the ListViewRenderer is disposed
        // So let's create a new reference to the SwipeRefreshLayout instance
        SwipeRefreshLayout refreshLayoutInstance = _refreshLayout;

        if (refreshLayoutInstance == null)
        {
            return;
        }

        bool isRefreshing = Element.IsRefreshing;
        refreshLayoutInstance.Post(() =>
        {
            refreshLayoutInstance.Refreshing = isRefreshing;
        });
    }
}
Mikalai Daronin
  • 8,590
  • 2
  • 35
  • 47
2

You need to follow MVVM Pattern

On your ViewModel you need to:

  • Implement INotifyPropertyChanged

  • Define properties like:

     private bool _IsRefreshing;
     public bool IsRefreshing
     {
         get { return _IsRefreshing; }
         set { SetProperty(ref _IsRefreshing, value; } 
         /*So every time the property changes the view is notified*/
     }
    
  • Define the method that fetch your data, in your case refreshData()

  • Toggle the IsRefreshing true/false when needed

On your Page you need to:

  • Bind the listview itemSource to a VM property with the SetPropertyValue

  • Bind ListView.IsRefreshing to ViewModel's IsRefreshing:

    MyListView.SetBinding<MyViewModel>(ListView.IsRefreshing, vm => vm.IsRefreshing);

Here is a great article talking about INotifyPropertyChanged

Thom
  • 663
  • 7
  • 19
Ingenator
  • 222
  • 3
  • 9
  • Why has this answer received a down vote? This is how it should be done, even though the code shared uses an additional framework. But the idea is to make a one way binding for the `IsRefreshing` property, and make it implement `INotifyPropertyChanged`, and set it to false after your code that updates your list. – Eli May 02 '19 at 18:06
  • @Elisabeth that's true, but I can confirm that this can lead to an endless refreshing indicator in a tabbed page. I have it inside a `Task.Run(async () => await //..` and changing the "IsRefreshing" bool property of the VM thats bound to the ListView will sometimes not propergate the change event to the View. What Ingenator described is as he said just a "MVVM pattern". Nothing more. – Csharpest Jul 09 '19 at 14:07
  • @Csharpest Beware when updating in a task. If you have run off the main thread or didn’t complete the work, it is when your property change will never propagate to the view. Use ‘Device.BeginInvokeOnMainThread’ to update. – Eli Jan 08 '20 at 18:46
2

Try this:

async void refreshData()
{
    Device.BeginInvokeOnMainThread(() => {
        myListView.BeginRefresh();
        var apiCallResult = await App.Api.myApiGetCall();
        myListView.ItemsSource = apiCallResult;
        myListView.EndRefresh();
    });
}

Apparently, there is no need for "Task" anymore.

If the error occurs only in Android, it's possible that it's just the way Android handles threads. It does not allow threads to change visual elements directly. Sometimes when we try to do that it throws a silent exception and the code action have no practical effect.

Sayo Babalola
  • 990
  • 14
  • 25
Diego Rafael Souza
  • 5,241
  • 3
  • 23
  • 62