Reputation: 1489
I have a Recycler View that displays information, downloaded from a server. I want it to be responsive while doing all this, so I wrote it using async/await operations and Tasks. The problem is that when it awaits for the information to be downloaded, the UI still continues to work and it either crashes because nothing is downloaded yet, or it doesn't show all of the information.
If GetDevicesInfo()
is async, the app crashes because there is no information to display yet. For some reason If I put the code in the method in the place where I call the method (and then comment the call), it works just fine.
The case is different with DisplaySensorStates()
.
This is how everything looks like when the method is not async:
...and when it's async:
Appereantely, when it downloads the information for the temperature, the view already has displayed the existing items, which is only the first one.
Code:
namespace *********.Fragments {
public class Dashboard : GridLayoutBase {
JsonFetcher jsonFetcher;
private ISharedPreferences pref;
private SessionManager session;
private string cookie;
private DeviceModel deviceModel;
private RecyclerView recyclerView;
private RecyclerView.Adapter adapter;
private RecyclerView.LayoutManager layoutManager;
private List<ItemData> itemData;
public static Activity activity;
private SwipeRefreshLayout swipeRefreshLayout;
private const string URL_DASHBOARD = "http://10.1.1.20/appapi/getdashboard";
private const string URL_DATA = "http://10.1.1.20/appapi/getdata";
public async override void OnStart() {
base.OnStart();
activity = Activity;
session = new SessionManager();
pref = Activity.GetSharedPreferences("UserSession", FileCreationMode.Private);
cookie = pref.GetString("PHPSESSID", string.Empty);
GetDevicesInfo();
DisplaySensorStates();
DisplayLastPhoto();
adapter = new ViewAdapter(itemData);
new System.Threading.Thread(new System.Threading.ThreadStart(() => {
Activity.RunOnUiThread(() => {
recyclerView.SetAdapter(adapter);
});
})).Start();
}
public async void GetDevicesInfo() {
var jsonFetcher = new JsonFetcher();
JsonValue jsonDashboard = await jsonFetcher.FetchDataWithCookieAsync(URL_DASHBOARD, cookie);
deviceModel = new DeviceModel();
deviceModel = JsonConvert.DeserializeObject<DeviceModel>(jsonDashboard);
}
// Shows sensor states
public async void DisplaySensorStates() {
itemData = new List<ItemData>();
foreach (var sensor in this.deviceModel.Sensors) {
string lastValue = String.Empty;
if (sensor.Type == "2") { // Temperature
var jsonFetcher = new JsonFetcher();
JsonValue jsonData = await jsonFetcher.FetchSensorDataAsync(URL_DATA, sensor.Id, "ASC", cookie);
var deviceModel = new DeviceModel();
deviceModel = JsonConvert.DeserializeObject<DeviceModel>(jsonData);
lastValue = deviceModel.SensorData.Last().Value;
}
itemData.Add(new ItemData() {
id = sensor.Id,
value = lastValue,
type = sensor.Type,
image = Resource.Drawable.smoke_red,
title = sensor.Name.First().ToString().ToUpper() + sensor.Name.Substring(1).ToLower(),
});
}
}
// Shows the last camera photo
public async void DisplayLastPhoto() {
// if (deviceModel.Error == "true" && deviceModel.ErrorType == "noPhoto") {
// //TODO: Show a "No photo" picture
// } else {
// string url = deviceModel.LastPhotoLink;
// Bitmap imageBitmap = await new ImageDownloader().GetImageBitmapFromUrlAsync(url, Activity, lastPhoto.Width, lastPhoto.Height);
// lastPhoto.SetImageBitmap(imageBitmap);
// imageBitmap.Dispose();
// }
}
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.Inflate(Resource.Layout.Dashboard, container, false);
SwipeRefreshLayout swipeRefreshLayout = view.FindViewById<SwipeRefreshLayout>(Resource.Id.swipe_container);
// swipeRefreshLayout.SetColorSchemeResources(Color.LightBlue, Color.LightGreen, Color.Orange, Color.Red);
// On refresh button press/swipe, refreshes the recycler view
swipeRefreshLayout.Refresh += async (sender, e) => {
this.GetDevicesInfo();
new System.Threading.Thread(new System.Threading.ThreadStart(() => {
Activity.RunOnUiThread(() => {
this.DisplaySensorStates();
adapter = new ViewAdapter(itemData);
// itemData[0].title = "Dooooooors";
// Console.WriteLine(itemData[0].title);
recyclerView.SetAdapter(adapter);
// Console.WriteLine ("yes");
adapter.NotifyDataSetChanged();
// recyclerView.Invalidate();
swipeRefreshLayout.Refreshing = false;
});
})).Start();
};
recyclerView = view.FindViewById<RecyclerView>(Resource.Id.dashboard_recycler_view);
layoutManager = new GridLayoutManager(Activity, 3);
recyclerView.HasFixedSize = true;
recyclerView.SetLayoutManager(layoutManager);
recyclerView.SetItemAnimator(new DefaultItemAnimator());
recyclerView.AddItemDecoration(new SpaceItemDecoration(8));
return view;
}
public class ViewAdapter : RecyclerView.Adapter {
private List<ItemData> itemData;
public string sensorId;
public string sensorType;
private ImageView imageId;
private TextView sensorValue;
private TextView sensorStatus;
public ViewAdapter(List<ItemData> itemData) {
this.itemData = itemData;
}
public class ItemView : RecyclerView.ViewHolder {
public View mainView { get; set; }
public string id { get; set; }
public string type { get; set; }
public ImageView image { get; set; }
public TextView value { get; set; }
public TextView status { get; set; }
public ItemView(View view) : base(view) {
mainView = view;
}
}
public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType) {
View itemLayoutView = LayoutInflater.From(parent.Context).Inflate(Resource.Layout.DashboardItems, null);
CardView cardView = itemLayoutView.FindViewById<CardView>(Resource.Id.dashboard_card_view);
imageId = itemLayoutView.FindViewById<ImageView>(Resource.Id.sensor_image);
sensorValue = itemLayoutView.FindViewById<TextView>(Resource.Id.sensor_value);
sensorStatus = itemLayoutView.FindViewById<TextView>(Resource.Id.sensor_status);
var viewHolder = new ItemView(itemLayoutView) {
id = sensorId,
type = sensorType,
image = imageId,
value = sensorValue,
status = sensorStatus
};
return viewHolder;
}
public override void OnBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
ItemView itemHolder = viewHolder as ItemView;
if (itemData[position].type == "2") { // Temperature
// itemHolder.image.Visibility = ViewStates.Invisible;
itemHolder.value.Text = itemData[position].value;
}
itemHolder.image.SetImageResource(itemData[position].image);
itemHolder.status.Text = itemData[position].title;
EventHandler clickUpdateViewEvent = ((sender, e) => {
var bundle = new Bundle();
var dualColumnList = new DualColumnList();
bundle.PutString("id", itemData[position].id);
dualColumnList.Arguments = bundle;
((FragmentActivity)activity).ShowFragment(dualColumnList, itemData[position].title, itemData[position].type);
});
itemHolder.image.Click += clickUpdateViewEvent;
itemHolder.value.Click += clickUpdateViewEvent;
itemHolder.status.Click += clickUpdateViewEvent;
}
public override int ItemCount {
get { return itemData.Count; }
}
}
public class ItemData {
public string id { get; set; }
public string type { get; set; }
public int image { get; set; }
public string value { get; set; }
public string title { get; set; }
}
}
}
How can I fix this?
Upvotes: 3
Views: 566
Reputation: 457362
You have to design your UI to be asynchronous.
That is, when your UI loads, it should (synchronously) load into a valid, expected state of having no data. It also starts the download(s) at that time. When the download(s) complete, you have to update the UI to show the new data. I have a series of MSDN articles on async MVVM applications that you may find helpful.
Also, avoid async void
. I have another MSDN article on async best practices that goes into more detail.
Upvotes: 1