Vahid Amiri
Vahid Amiri

Reputation: 11127

Call to async method is not awaited in Xamarin Android

I have a fragment that I use to return a list of articles received from a REST API. The REST API is async and I use await to wait for it to complete. When completed, I pass the data (that is List<Article>) to a variable called listofarticlesand use it to create an ArrayAdapter.

I do the above code in OnCreate method of this fragment that's supposed to run before the OnCreateView as far as I know.

My code works in an activity but not a fragment.

Problem: The call to awaitable REST method is not awaited.

When debugging line by line it goes like this:

Why is this happening and how to fix it??

public class LatestNewsFragment : Android.Support.V4.App.Fragment, ListView.IOnItemClickListener
{
    private Context globalContext = null;
    private List<Article> listofarticles;
    private ArrayAdapter<Article> listAdapter;

    public LatestNewsFragment()
    {
        this.RetainInstance = true;
    }

    public async override void OnCreate (Bundle savedInstanceState)
    {
        base.OnCreate (savedInstanceState);
        globalContext = this.Context;

        //Create a progress dialog for loading
        ProgressDialog pr = new ProgressDialog(globalContext);
        pr.SetMessage("Loading data");
        pr.SetCancelable(false);

        var rest = new RestAccess();
        pr.Show();
        //Download the data from the REST API
        listofarticles = await rest.ListArticlesAsync ("SomeSource");
        pr.Hide();
        Console.WriteLine (listofarticles.Capacity);

        listAdapter = new ArrayAdapter<Article>(globalContext, Android.Resource.Layout.SimpleListItem1, listofarticles);
    }

    public override View OnCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
    {
        var view = inflater.Inflate(Resource.Layout.Fragment_LatestNews, null);
        ListView listView = view.FindViewById<ListView>(Resource.Id.list_latestnews);
        listView.Adapter = listAdapter;
        listView.OnItemClickListener = this;
        return view;
    }
}

Upvotes: 1

Views: 5287

Answers (3)

Florian Haider
Florian Haider

Reputation: 1912

As explained in this answer https://stackoverflow.com/a/32656807/1230302 it is not a good idea to do anything view related in OnCreate() in fragments.

Also if you make OnCreate() an async method, the other fragment lifecycle events (see http://developer.android.com/guide/components/fragments.html) will be called immediately after the await line. Now the problem is you cannot rely on the order in which the code after await will run, if your REST API is slow OnCreateView() might be done already and you will be fine, but if the API is fast OnCreateView() might still run and your app will crash.

Why not use OnActivityCreated() to load your data? You can be sure everything is initialized in that one, something like:

private ListView listView;

public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
    var view = inflater.Inflate(Resource.Layout.Fragment_LatestNews, null);

    listView = view.FindViewById<ListView>(Resource.Id.list_latestnews);    
    listView.OnItemClickListener = this;

    return view;
}

public override async void OnActivityCreated(Bundle savedInstanceState)
{
    base.OnActivityCreated(savedInstanceState);

    //Download the data from the REST API
    var rest = new RestAccess();    
    var listofarticles = await rest.ListArticlesAsync("SomeSource");

    listView.Adapter = new ArrayAdapter<Article>(Context, Android.Resource.Layout.SimpleListItem1, listofarticles);
}

Upvotes: 7

Vahid Amiri
Vahid Amiri

Reputation: 11127

The solution to this problem was to create a field variable of type View to store the inflated view that fragment will return, and use that view to get the layout file in the OnCreate method. There and after the await we can fill the ListView with the data. So the final code would be:

public class LatestNewsFragment : Android.Support.V4.App.Fragment, ListView.IOnItemClickListener
{
    private Context globalContext = null;
    private List<Article> listofarticles;
    View view;

    public LatestNewsFragment()
    {
        this.RetainInstance = true;
    }

    public async override void OnCreate (Bundle savedInstanceState)
    {
        base.OnCreate (savedInstanceState);

        globalContext = this.Context;

        //Create a progress dialog for loading
        Android.App.ProgressDialog pr = new Android.App.ProgressDialog(globalContext);
        pr.SetMessage("Loading data");
        pr.SetCancelable(true);

        var rest = new RestAccess();
        pr.Show();
        listofarticles = await rest.ListArticlesAsync ("SomeSource");
        pr.Hide();

        ArrayAdapter<Article> listAdapter = new ArrayAdapter<Article>(globalContext, Android.Resource.Layout.SimpleListItem1, listofarticles);
        ListView listView = view.FindViewById<ListView>(Resource.Id.list_latestnews);
        listView.Adapter = listAdapter;
        listView.OnItemClickListener = this;
    }

    public override View OnCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
    {
        view = inflater.Inflate(Resource.Layout.Fragment_LatestNews, null);

        return view;
    }
}

Upvotes: 1

Sami Kuhmonen
Sami Kuhmonen

Reputation: 31203

async doesn't mean "wait and don't run any other code." It means "allow other code to run while waiting for this call in this context." Each async block is its own context and without await will run its own course. Therefore it is quite normal that the calls to other methods are run while waiting for that call to finish, or even start. You shouldn't rely on order that way but rather synchronize your code properly.

It would be better to get the data after all this view creation and then assign it to the adapter. This way everything will be predictable and you can even show a proper loading indicator, if necessary.

Upvotes: 1

Related Questions