Skip to content
This repository has been archived by the owner on May 1, 2024. It is now read-only.

[Bug] [Android] CollectionView jittering on Android with GridItemsLayout #8718

Open
yurkinh opened this issue Nov 29, 2019 · 44 comments
Open

Comments

@yurkinh
Copy link
Contributor

yurkinh commented Nov 29, 2019

Description

I tried to reproduce UI of one popular music app (DI.FM).
During implementation of page with styles I have encountered issue with collectionview and images. Fast scroll on CollectionView with lots of images produces jittering on Android.
I have tried Image, ffImageLoading:CachedImage (a bit better), Image with GlideX(a bit better).
While it works perfectly smooth on ios even with ordinary Image control. Also I have tried different sizing strategies.

Steps to Reproduce

  1. Start reproduction project on Android and open RadioChannels "Popular" tab or just "Popular" page from side menu. (Same on ios for comparison)

Expected Behavior

No jittering, smooth scrolling

Actual Behavior

Jittering

Basic Information

  • Version with issue:
  • Nuget Packages: Xamarin.Forms 4.3.0.991211
  • Affected Devices: Android devices and emulators

Reproduction Link

DIFM.zip

@yurkinh yurkinh added s/unverified New report that has yet to be verified t/bug 🐛 labels Nov 29, 2019
@yurkinh yurkinh changed the title [Bug] [Android] CollectionView jittering on Android with with GridItemsLayout [Bug] [Android] CollectionView jittering on Android with GridItemsLayout Nov 29, 2019
@jfversluis jfversluis added a/collectionview p/Android and removed s/unverified New report that has yet to be verified labels Nov 29, 2019
@jfversluis
Copy link
Member

I don't see anything weird with how the images are loaded. Images are not too big. Yet the scrolling is not very smooth.

@yurkinh
Copy link
Contributor Author

yurkinh commented Nov 29, 2019

Yes. But on ios scrolling is very smooth.
Also, you can download the original app from PlayMarket to check the same page. Scrolling is very smooth on Android in the original app with the approximately same amount of images. An original app was written with Xamarin.Native

@Alex-111
Copy link

Same problem here...
Scrolling jitters a lot on Android...
Any news for this issue?

@yurkinh
Copy link
Contributor Author

yurkinh commented Mar 20, 2020

@Alex-111 Tried latest stable version and still the same. But a preview of 4.6.0 looks a bit better

@Alex-111
Copy link

Alex-111 commented Mar 31, 2020

Also just tried the preview of 4.6. Unfortunately not much change here….
Also wanted to point out, that it has not only to do with grid layout. Also other layouts are affected. Seems it has something to do how collectionview renders the elements...

@rudyspano
Copy link

rudyspano commented Apr 2, 2020

Hello. Same problem on my side... Even with LinearItemsLayout
I've tried: FFIL (better) and GlideX (really better).
In the output console, we can see many garbage collections even on a simple project.

After analysis with Xamarin Profiler, it seems there's a lot of BindableProperty and linked mechanisms created in memory when scrolling (BindablePropertyContext, ...). I guess, that's a consequence of recycling...

When i don't use DataBinding, things are better but not perfects... Not relying on DataBinding specially with images is advised by the FFIL author for example....
You should try even if it's not cool...

I've also seen the same kind of problems with ListView...

If someone has some best practices/solutions on this topic, it will be really appreciated :)

@Alex-111
Copy link

Alex-111 commented Apr 3, 2020

I can confirm this. Lots of garbage collections in the debug output… But not sure if this alone is the reason for the heavy jitter..
Unfortunately, it is event not smooth on high end smartphones like Samsung S20, which is not accepted by customers...

@JMNewsome
Copy link

Any progress on this issue? This is causing serious issues within our project and we can't release due to this issue

@yurkinh
Copy link
Contributor Author

yurkinh commented Jun 4, 2020

Latest version in combination with GlideX.Forms looks pretty good but still not perfect as iOS version of CollectionView

@Happypig375
Copy link
Contributor

Happypig375 commented Jun 4, 2020

@JMNewsome Don't expect the Xamarin.Forms team to be quick on resolving issues...

@MSiccDev
Copy link

Any news on this? This one makes developing on Android really painful....

@brminnick
Copy link
Contributor

I'm seeing the same behavior in my GitTrends app.

Here's a video one of the users posted on Twitter running GitTrends v1.2.0:
https://twitter.com/anaseeen/status/1281610967336595458?s=20

  • 4GB RAM
  • Media Tek Helio octa core P23
  • Android 9

GitTrends v1.2.0 Source Code:
https://github.com/brminnick/GitTrends/releases/tag/v1.2.0

@JMNewsome
Copy link

Yeah, this is a very big performance problem, can we get some kind of higher priority on this item?

@brminnick
Copy link
Contributor

brminnick commented Jul 12, 2020

Workaround

I created a blog post regarding this:
https://codetraveler.io/2020/07/12/improving-collectionview-scrolling/

I found that two things were causing the UI to jitter when the user scrolls a CollectionView:

  • Bindings
  • Garbage Collection

I was able to remove the jitter by removing bindings from my DataTemplate and increasing the size of the Android Nursery to decrease the number of garbage collections.

If you'd like to see exactly how I solved it, here is the Pull Request that fixed it in my GitTrends app: brminnick/GitTrends#143

Before (Substantial Scrolling Jitter) After (No Scrolling Jitter)
BeforeFix AfterFix

Removing DataTemplate Bindings

To remove bindings, we can use a DataTemplateSelector to pass the BindingContext directly into our DataTemplate and assign the values on initialization.

Before Removing DataTemplate Bindings

using Xamarin.Forms.Markup;

class CollectionViewPage : ContentPage
{
    Content = new CollectionView
    {
        ItemTemplate = new ImageDataTemplate()
    }.Bind(CollectionView.ItemsSourceProperty, nameof(CollectionViewModel.ImageList));
}

class ImageModel
{
    public string ImageTitle { get; set; }
    public string ImageUrl { get; set; }
}

class ImageDataTemplate : DataTemplate
{
    public MyDataTemplate() : base(() => CreateDataTemplate())
    {

    }

    static View CreateDataTemplate() => new StackLayout
    {
        Children = 
        {
            new Image().Bind(Image.SourceProperty, nameof(ImageModel.ImageUrl))
            new Label().Bind(Label.TextProperty, nameof(ImageModel.ImageTitle))
        }
   }
}

After Removing DataTemplate Bindings

using Xamarin.Forms.Markup;

class CollectionViewPage : ContentPage
{
    Content = new CollectionView
    {
        ItemTemplate = new ImageDataTemplateSelector()
    }.Bind(CollectionView.ItemsSourceProperty, nameof(CollectionViewModel.ImageList));
}

class ImageModel
{
    public string ImageTitle { get; set; }
    public string ImageUrl { get; set; }
}

class ImageDataTemplateSelector : DataTemplateSelector
{
    protected override DataTemplate OnSelectTemplate(object item, BindableObject container) => new  ImageDataTemplate((ImageModel)item);

    class ImageDataTemplate : DataTemplate
    {
        public MyDataTemplate(ImageModel imageModel) : base(() => CreateDataTemplate(imageModel))
        {

        }

        static View CreateDataTemplate(ImageModel imageModel) => new StackLayout
        {
            Children = 
            {
                new Image { Source = imageModel.ImageUrl },
                new Label { Text = imageModel.ImageTitle }
            }
       }
    }
}

Increate Nursery Size to Decrease Garbage Collection

We can set the size of the Android Nursery by setting it in the MONO_GC_PARAMS:

  1. In the Xamarin.Android project, create a new file called GarbageCollector.config
  2. In GarbageCollector.config, add the following line of code:
MONO_GC_PARAMS=nursery-size=64m

Note: nursery-size must be a power of 2, e.g. 2, 4, 8, 16, 32, 64, 128 etc.
Note: I recommend trying different values for your nursery-size because each app is different and will have different memory requirements

  1. In Visual Studio, in the Solution Explorer, right-click on GarbageCollector.config
  2. In the right-click menu, select BuildAction > AndroidEnvironment

Screen Shot 2020-07-12 at 8 53 36 AM

@rudyspano
Copy link

rudyspano commented Jul 15, 2020

Thank you @brminnick . I will try this.
Concerning the code refactoring Binding => Assignment, create a DataTemplate in code-behind seems to me a little bit unusual (especially with more complex cells). But I guess, same result could be obtained with Xaml and OnBindingContextChanged overriding. DataBinding is however powerfull and that's too bad that we cannot use it properly here :(. Thanks again

@Alex-111
Copy link

@brminnick : Thanks for suggesting some workarounds!

  • regarding the nursery-size: This alone had no effect in my case - unfortunately….

  • regarding the binding: In your case you do not update the view on data change. This is needed in my case. So unfortunately we cannot use your workaround...

any further suggestions are appreciated...

@JMNewsome
Copy link

I think this garbage collection issue is the number one factor in Xamarin Forms for Android running terribly and should be something that is prioritized.

@samhouts samhouts added this to the 5.0.0 milestone Aug 13, 2020
@libin85
Copy link

libin85 commented Sep 10, 2020

It's pretty sad to see Xamarin go down this path. The listview is not recommended any more and collection view is pretty much not usable. As a developer I dont know how am I gonna use xamarin to build apps, or even support the apps that we have build over the years.

@JMNewsome
Copy link

I agree, what I love about Xamarin is the ability to use C# and shared between the platform, code execution speed for me on a compiled app is NOT the problem. The problem is the UI thread, which of course will cause everything else to appear to run bad. If the UI thread didn't feel like it was held together with duct tape and staples, It would be so much better

@Deepfreezed
Copy link

@brminnick, is this an issue when creating UI controls from code behind? I tested this out with one of my CollectionView templates. Creating UI control from code behind causes the Nursery to become full frequently, in turn calling the GC constantly. Using XAML, Nursery full warning is less frequent.

@brminnick
Copy link
Contributor

brminnick commented Sep 14, 2020

@Deepfreezed Can you share your reproduction code and its associated performance testing results?

As far as I'm aware, this is not a XAML-specific nor a C#-specific problem. Rather, it is a problem related to bindings and garbage collection.

@kerberosargos
Copy link

Same issue is here for me. Android performance is very bad.

@ilsur-dev111
Copy link

Will the issue be resolved in the next release of Xamarin Forms? Due to this issue, we are unable to distribute the application.

@andersonvieiragomeslopes

This problem is really very serious, I changed the listview for collectionview by official recommendation, now a problem like this arises. It is very sad to see that it has been over a year and the problem has not been solved, the solution @brminnick does not sound very good to me due to all the efforts made, but it solved this problem in a demo I made, we have an application with 5 million downloads made in xamarin and we often have a crash and only now I discovered that this was the real problem, what a shame.
That is unfortunate 👎

@samhouts samhouts removed this from the 5.0.0 milestone Nov 2, 2020
@niccolocapitelli
Copy link

I had this problem on a production app. I need this fix in the 5.0 milestone.

@AlleSchonWeg
Copy link
Contributor

I replaced the CollectionView with a StackLayout and Datatemplate. This works for me.

@JMNewsome
Copy link

@AlleSchonWeg Replacing a CollectionView with a StackLayout and Datatemplate is not a proper solution as it does not support virtualization, so you will see an impact the more items you have

@rudyspano
Copy link

rudyspano commented Jan 26, 2021

Hello. We achieved step by step to deliver a good experience to our customers in terms of performances with our CollectionView containing lot of content (Images, Buttons, Texts, ...). I have one advice and one trick that could perhaps help some of you...

  1. Most of the time, it's not only the fault of the CollectionView, I advice you to create a Debug page to see how much time is needed for the view you define as DataTemplate are rendered if you add 10 => 100 instances in a StackLayout (for example). By separating each part into different files, we could see which element are slow to render (StopWatch before and after InitializeComponent). Off course, it depends on the targeted device and configuration (debug vs release, AOT or not, use of HotReload, ...) but we could optimize our cells rendering time by ~3-4. Some controls are surprisingly slow or unstable in terms of rendering (without binding), we replaced several BorderView by a complte Image to limit the number of controls for example... Moreover, do not forget that controls not visible have also a cost... (see https://xamgirl.com/stop-doing-isvisibletrue-false-to-show-hide-views-in-runtime-in-xamarin-forms/ for solutions)

  2. We have several types of cells => several DataTemplate and a DataTemplateSelector. In my opitnion, a big problem with the CollectionView implementation on Android (and also on iOS by the way) is that the Adapter has use a few types of ViewHolders (see https://github.com/xamarin/Xamarin.Forms/blob/d99264507dc1623aecb83daf81aadd2effae62ae/Xamarin.Forms.Platform.Android/CollectionView/ItemViewType.cs ). The consequence is that ItemViewType.TemplatedItem was used for all cells and when a ViewHolder changed from a type of view to another (different Datatemplate), a new view was created... On a long list, with long scroll, views were created indefinitively (bad recycling behavior). In short, I suggest you to override the CollectionViewRenderer and the associated Adapter with a logic that creates a ViewHolder for each type of item. For example:

private class GroupableItemsViewAdapterAdvanced<TItemsView, TItemsViewSource> : GroupableItemsViewAdapter<TItemsView, TItemsViewSource>
            where TItemsView : GroupableItemsView
            where TItemsViewSource : IGroupableItemsViewSource
        {
            private Dictionary<object, int> _viewHoldersViewTypesAssociations = new Dictionary<object, int>();
            private CollectionViewAdvancedRenderer _collectionViewAdvancedRenderer;
            public GroupableItemsViewAdapterAdvanced(TItemsView groupableItemsView, CollectionViewAdvancedRenderer collectionViewAdvancedRenderer, Func<View, Context, ItemContentView> createView = null)
                : base(groupableItemsView, createView)
            {
                _collectionViewAdvancedRenderer = collectionViewAdvancedRenderer;
            }

            public override int GetItemViewType(int position)
            {
                var itemViewType = base.GetItemViewType(position);

                //ensure all item types have its own placeholder and not a generic one TemplatedItem
                if (itemViewType == ItemViewType.TemplatedItem)
                {
                    var itemType = ItemsSource.GetItem(position).GetType();

                    if (!_viewHoldersViewTypesAssociations.ContainsKey(itemType))
                    {
                        int viewTypeId;

                        if (!_viewHoldersViewTypesAssociations.Any())
                        {
                            //Framework TemplateItem View type is 42, ours are 421,422, ... 
                            viewTypeId = ItemViewType.TemplatedItem * 10 + 1;
                        }
                        else
                        {
                            viewTypeId = _viewHoldersViewTypesAssociations.Max(assocation => assocation.Value) + 1;
                        }

                        //By default, Cells are not always recycled depending on state. Force to recycle the most mossible
                        //https://developer.android.com/reference/android/support/v7/widget/RecyclerView.RecycledViewPool.html#setMaxRecycledViews(int,%20int)
                        _collectionViewAdvancedRenderer.GetRecycledViewPool().SetMaxRecycledViews(viewTypeId, 200);

                        _viewHoldersViewTypesAssociations.Add(itemType, viewTypeId);
                    }

                    itemViewType = _viewHoldersViewTypesAssociations[itemType];
                }

                return itemViewType;
            }
        }

@roubachof
Copy link

I also noticed that the GetItemViewType was not correctly implemented on Android.
You should have one ItemViewType by DataTemplate.
It means that if you have a DataTemplateSelector with 4 differents DataTemplate, you should have one item type for each of those DataTemplate.
See my implementation of the GetItemViewType in the sharpnado's HorizontalListView:

            public override int GetItemViewType(int position)
            {
                if (_isDisposed)
                {
                    return -1;
                }

                if (_element.ItemTemplate is DataTemplateSelector dataTemplateSelector)
                {
                    var dataTemplate = dataTemplateSelector.SelectTemplate(_dataSource[position], _element);

                    int itemViewType = _dataTemplates.IndexOf(dataTemplate);
                    if (itemViewType == -1)
                    {
                        itemViewType = _dataTemplates.Count;
                        _dataTemplates.Add(dataTemplate);
                    }

                    return itemViewType;
                }

                return base.GetItemViewType(position);
            }

@angelru
Copy link

angelru commented Feb 1, 2021

The same issue here...

@azrinsani
Copy link

azrinsani commented Apr 18, 2021

Thank you Brandon for saving my life!!
This is his post

I found that correcting the GC solved my collection view scroll jittery problem. Android default is 512KB, which is very small. I changed it to 32MB and its silky smooth now.

Side Note:

  • I use XAML, not code behind.
  • I have hella lot of XAML bindings and mostly two way bindings (like about 15,000 bindings). Also have several layers of 'BindableLayout.ItemsSource' in XAML (not code behind). All these does not seem to affect performance.
  • I use datatemplates, NOT datatemplateselector as advised by Brandon. My item source is huge (like 3000+), but it's silky smooth scroll.
  • I use a lot of custom renderer (which I conclude has no affect on performance).
  • I have images, which I bind to ImageSource Directly. I use the default Xamarin Library, no FFLoading or Syncfusion stuff.
  • I use a lot of grid layouts, with specified row height. I have only one stack layout in my data template as it is binded to an Observable collection which I can't be sure of the height.
  • I use a lof of Multi Triggers. This does not seem to affect performance as one may think.

@azrinsani
Copy link

azrinsani commented May 3, 2021

Today I spent a few hours trying to further improve the scrolling, which Mr Jitter came back again! I'd like to share what I did, what worked and what did not work to improve the jittery scrolling

WHAT DID NOT WORK

  • I tried swapping my 'Editor's with 'Label's, as according to your analysis, labels are faster. Unfortunately this had no difference on scroll choppiness. It was still very choppy.
  • I tried reducing the CollectionView itemsource size (I had 3000 items, i reduced it to 10), though this had no effect on performance at all. Maybe you want to update your info to mention collection size does not effect
  • I tried changing FlexLayout to StackLayout this too had no impact on performance
  • I tried Removing All icon images, this too had not impact on performance
  • I tried Changing my 'BindableLayout.ItemTemplateSelector' to only 'BindableLayout.ItemTemplate'. This does not seem to improve performance as how some have alluded too.
  • Some of my controls had DataTrigger. Removing Data Triggers did not have any impact on performance
  • I had a verbose alternative declaration of a button using labels and frames. Changing this to a button did not seem to have any impact on scroll performance.

WHAT WORKED

  • Removing Animations. I had a popup message which would appear by means of opacity animation. I noticed by removing this improved performance.
  • I had a Viewmodel that when 'Get' was called, it perform a few calculations. Removing this improved performance very slightly, though not that significant.
  • OK HERE'S THE BIG ONE => I tried removing BindableLayout.ItemsSource on a flex layout which I was using, and manually added items one by one into the flex layout. Surprise! This had significant jitter improvement. As of now, I know now that this is the main culprit. Sadly, this means I cannot use my view models anymore. On this note, I tried to wrap the FlexLayout inside a , as some suggested though this did not work as well. I also changed the binding of itemsource to ONETIME binding, also did not work!!!, this got me thinking that BindableLayout is just bad.

VERDICT => BINDABLE LAYOUT PERFORMANCE IS BAD.... DONT USE IT . On further reasearch I realized that this has been mentioned on Microsoft Website:-

Bindable layouts should only be used when the collection of items to be displayed is small, and scrolling and selection isn't required. While scrolling can be provided by wrapping a bindable layout in a ScrollView, this is not recommended as bindable layouts lack UI virtualization. When scrolling is required, a scrollable view that includes UI virtualization, such as ListView or CollectionView, should be used. Failure to observe this recommendation can lead to performance issues.

Source => https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/layouts/bindable-layouts

@BlueRaja
Copy link

This is still an issue, and none of the workarounds seem to work. In my scenario, each item is a single label with a single one-time binding on Text (or 0 bindings, using @brminnick's hack). There's 1000's of them, grouped into groups of 10 using IsGrouped="True".

The first 500 or so items scroll fine, but after that it gets slower and slower until around the 1000th item it's literally unusable. Somewhere around 1500 items the app crashes.

1000 items should be a fairly common scenario, so it's absurd that there's seemingly no way to get this to work.

@AlleSchonWeg
Copy link
Contributor

You could try Sharpnado ListView: https://github.com/roubachof/Sharpnado.HorizontalListView. It supports grid, horizontal and vertical layout.

@BlueRaja
Copy link

@AlleSchonWeg Yes I'm aware of that one, along with several other open-source options but none of them support groupings.

@Phenek
Copy link

Phenek commented Sep 20, 2021

It's really hard to have an Android app that is fluid with Xamarin.
It seems impossible to me

@othmanTeffahi99
Copy link

I tested the CollectionView on MAUI in Android, it's working great, you can migrate to Maui, but it is still in the preview Version .

@dimonovdd
Copy link
Contributor

MONO_GC_PARAMS=nursery-size=64m

@brminnick Hi. Could it create serious problems or memory leak?

@haavamoa
Copy link

haavamoa commented Jan 8, 2022

This seems interesting:
levitali/CompiledBindings#4

Sees significant performance improvments when swapping XF bindings with his own implementation. 🥰

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests