简体   繁体   中英

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 listofarticles and 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:

  • OnCreate is called. Processed to the line where I call the awaitable method (that is supposed to fill listofarticles ) and simply skips to the OnCreateView . Therefore listofarticles is null.

  • OnCreateView is called and processed. At this point program just goes back to the activity it's called.

  • I can no longer debug it because Xamarin Studio thinks I'm finished but at the emulator it just starts to call the REST method after all this and because of that the data inside listofarticles is useless because I can't pass it to Adapter now.

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;
    }
}

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);
}

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.

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;
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM