On Android device UI update happens only after user interaction
Asked Answered
S

1

1

Interesting (at least for me) bug I found there.

I am making an (prototype) app, it does some web requests and returns simple data.

There is ObservableCollection<DownloadbleEntity> which is updated dynamicly (because DownloadbleEntity contains the image which we get by other requests, to output list element with an image).

Here is layout part:

   <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 an idea of how update is going:

        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 - on devices, after data output starts it outputs one list element and at the same time outputs 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 original thread that created a view hierarchy can touch its views. at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6898) at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1048) 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.view.View.requestLayout(View.java:19785) at android.view.View.requestLayout(View.java:19785) at android.widget.AbsListView.requestLayout(AbsListView.java:1997) at android.widget.AdapterView$AdapterDataSetObserver.onChanged(AdapterView.java:840) at android.widget.AbsListView$AdapterDataSetObserver.onChanged(AbsListView.java:6380) at android.database.DataSetObservable.notifyChanged(DataSetObservable.java:37) at android.widget.BaseAdapter.notifyDataSetChanged(BaseAdapter.java:50)

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

But it all continues to render on my device screen only after I do something - touch screen or touch my SearchView input or rotate it.

It's kinda strange I wonder what causes it. Is it because of I do something wrong regarding my collection update?

I recorded the video of whats happening, so here it is (sorry for my english and accent :( )

UPD (regarding to last comment):

Is the collection really being update from the background thread if update is happening inside dispatcher.RequestMainThreadAction?

UPD2

I added thread number detection, so, looks like 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 the 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}");
        }

The output:

enter image description here

Also, tried this approach but no success:

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 workaround (but still not solution) - if I use MvxObservableCollection instead of just ObservableCollection for FoundItems - everything working as it suppose to!

If we look at this class (MvxObservableCollection.cs ) we will see that it has those functions which are triggering on updates, looks like it does the same thing there:

        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 don't get it why in my case it's not working as it suppose to, I mean with just regular ObservableCollection?

Is notyfier for ObservableCollection change creates another thread or what?

Screak answered 9/12, 2017 at 11:53 Comment(7)
You have a threading problem. Probably caused by updating the Observable from a background thread. Read here, here and hereGove
@Gove I saw it, but I tought like call function in dispatcher.RequestMainThreadAction is enough, or it isn't?Screak
It is enough, to use the dispatcher. However, your UI updates are in Task.Run a lot of places.Breaux
@Breaux wellp even when I not using Task.Run wrapper, I got same picture :(Screak
@Breaux but I was just about updating the question, so, I found one thing thereScreak
Spent a full day trying to figure out this same exact issue and prefixing Mvx to ObservableCollection fixed it. Unbelievably stupid - welcome to the Xamarin experience. Little to no documentation, truck loads of bugs and issues. Thank you sir I would up vote you 100,000 times if I could. MvxObservableCollection is the answer.Gerbil
@Gerbil oh, thank you! Glad that this post was helpful! :DScreak
E
0

Not to steal the credit from @jazzmasterkc , but thought this needed to be posted as an answer. Using MvxObservableCollection instead of ObservableCollection for list value binding fixes the issue as MvxObservableCollection invokes all PropertyChanged and CollectionChanged events on main thread.

Ethylene answered 25/7, 2019 at 14:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.