In Android UI, updating occurs only after user interaction

An interesting (at least for me) error that I found there.

I am making a (prototype) application, it executes some web requests and returns simple data.

There is an ObservableCollection<DownloadbleEntity> , which is updated dynamically (because DownloadbleEntity contains the image that we get for other requests to display a list item with an image).

Here is part of the layout:

  <MvvmCross.Binding.Droid.Views.MvxListView android:id="@+id/searchlist" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" local:MvxBind="ItemsSource FoundItems; ItemClick OnItemClickCommand; OnScrollToBottom GetNewAsyncCommand" local:MvxItemTemplate="@layout/listitem" /> 

And this is the ViewModel code to show how the update happens:

  private ObservableCollection<DownloadableEntity> _foundItems; public ObservableCollection<DownloadableEntity> FoundItems { get { return _foundItems; } set { if (_currentPage > 0) { _foundItems = new ObservableCollection<DownloadableEntity>(_foundItems.Concat(value)); } else { _foundItems = value; } RaisePropertyChanged(() => FoundItems); } } private async Task PrepareDataForOutput(SearchResult searchResult) { _currentListLoaded = false; IMvxMainThreadDispatcher dispatcher = Mvx.Resolve<IMvxMainThreadDispatcher>(); List<Task<DownloadableEntity>> data = searchResult.Tracks.Items.ToList().Select(async (x) => await PrepareDataOutputAsync(x).ConfigureAwait(false)).ToList(); Android.Util.Log.Verbose("ACP", "PrepareDataForOutput"); try { var result = new ObservableCollection<DownloadableEntity>(); while (data.Count > 0) { var entityToAdd = await Task.Run(async () => { Task<DownloadableEntity> taskComplete = await Task.WhenAny(data).ConfigureAwait(false); data.Remove(taskComplete); DownloadableEntity taskCompleteData = await taskComplete.ConfigureAwait(false); await Task.Delay(500); return taskCompleteData; }).ConfigureAwait(false); result.Add(entityToAdd); // as it recommended by mvvmcross providers dispatcher.RequestMainThreadAction(async () => await Task.Run(() => { Android.Util.Log.Verbose("ACP", $"RequestMainThreadAction update {result.Last().Title}"); _toastService.ShowToastMessage($"Got {result.Last().Title}"); FoundItems = result; }).ConfigureAwait(false) ); } await Task.WhenAll(data).ContinueWith((x) => { Android.Util.Log.Verbose("ACP", "Output is Done"); _currentListLoaded = true; }); } catch (Exception e) { Android.Util.Log.Verbose("ACP", e.Message); } } private async Task<DownloadableEntity> PrepareDataOutputAsync(PurpleItem x) { return new DownloadableEntity { Title = x.Title, ArtistName = x.Artists.Select(y => y.Name).Aggregate((cur, next) => cur + ", " + next), Image = await Task.Run(() => _remoteMusicDataService.DownloadCoverByUri(x.Albums.FirstOrDefault().CoverUri)).ConfigureAwait(false), AlbumName = x.Albums.First().Title ?? "", AlbumId = x.Albums.First().Id, TrackId = x.Id }; } 

Well, the thing is that on devices, after the data is released, it gives out one element of the list and at the same time displays this:

Time Device name Type PID tag message 12-09 13: 45: 22.012 Wileyfox Swift 2 X Info 20385 mvx android.view.ViewRootImpl $ CalledFromWrongThreadException: Only the source thread that created the view hierarchy can touch its Views. in android.view.ViewRootImpl.checkThread (ViewRootImpl.java:6898) in android.view.ViewRootImpl.requestLayout (ViewRootImpl.java:1048) in android.view.View.requestLayout (View.java:19785) at android.view. View.requestLayout (View.java:19785) at android.view.View.requestLayout (View.java:19785) at android.view.View.requestLayout (View .java: 19785) at android.widget.AbsListView.requestLayout (AbsListView.java:1997) in android.widget.AdapterView $ AdapterDataSetObserver.onChanged (AdapterView.java:840) in android.widget.AbsListView $ AdapterDataSetObserver.AnsLonView java: 6380) in android.database.DataSetObservable.notifyChanged (DataSetObservable.java:37) in android.widget.BaseAdapter.notifyDataSetChanged (BaseAdapter.java:50)

But he continues to update my collection, so Android.Util.Log.Verbose("ACP", $"RequestMainThreadAction update {result.Last().Title}"); switches, and I can see it in the device log window.

But all this continues to be displayed on the screen of my device only after I do something - tap the screen or tap the SearchView input or rotate it.

This is strange, I wonder what causes it. Is it because I'm doing something wrong with respect to my collection update?

I recorded a video about what is happening, so here it is (sorry for my English and accent :()

UPD (relative to the last comment):

Is the collection really updated from the background thread if the update happens inside dispatcher.RequestMainThreadAction ?

UPD2

I added a definition for the number of threads, so it seems that the number is always the same

The code:

  // as it recommended dispatcher.RequestMainThreadAction(async () => await Task.Run(() => { var poolId = TaskScheduler.Current.Id; Android.Util.Log.Verbose("ACP THREAD INFO", $"RequestMainThreadAction update {result.Last().Title} THREAD NUMBER {poolId}"); _toastService.ShowToastMessage($"Got {result.Last().Title} by {result.Last().ArtistName}"); FoundItems = result; }).ConfigureAwait(false) ); 

Also in View I added:

 protected override void OnCreate(Bundle bundle) { base.OnCreate(bundle); _searchView = FindViewById<SearchView>(Resource.Id.search10); ViewModel.OnSearchStartEvent += ViewModel_OnSearchStartEvent; var poolId = TaskScheduler.Current.Id; Android.Util.Log.Verbose("ACP THREAD INFO", $"VIEW CREATED FROM THREAD NUMBER {poolId}"); } 

Exit:

enter image description here

In addition, I tried this approach, but failed:

 Mvx.Resolve<IMvxAndroidCurrentTopActivity>().Activity.RunOnUiThread(async () => await Task.Run(() => { var poolId = TaskScheduler.Current.Id; Android.Util.Log.Verbose("ACP THREAD INFO", $"RequestMainThreadAction update {result.Last().Title} THREAD NUMBER {poolId}"); _toastService.ShowToastMessage($"Got {result.Last().Title} by {result.Last().ArtistName}"); FoundItems = result; }).ConfigureAwait(false) ); 

UPD3

I found one solution (but still not a solution) - if I use MvxObservableCollection instead of ObservableCollection for FoundItems - everything works as it should!

If we look at this class ( MvxObservableCollection.cs ), we will see that it has those functions that trigger updates, it looks like it does the same:

  protected virtual void InvokeOnMainThread(Action action) { var dispatcher = MvxSingleton<IMvxMainThreadDispatcher>.Instance; dispatcher?.RequestMainThreadAction(action); } protected override void OnPropertyChanged(PropertyChangedEventArgs e) { InvokeOnMainThread(() => base.OnPropertyChanged(e)); } 

But I do not understand why in my case this does not work, as it is supposed, I mean only regular ObservableCollection ?

Is notyfier for an ObservableCollection change creating another thread or what?

+1
source share
1 answer

Not in order to steal a loan from @jazzmasterkc, but I thought that this should be published as an answer. Using MvxObservableCollection instead of ObservableCollection to bind list values ​​fixes the problem, since MvxObservableCollection raises all PropertyChanged and CollectionChanged events in the main thread.

0
source

Source: https://habr.com/ru/post/1499140/


All Articles