Android Tutorial [B4X] [XUI] CustomListView - lazy loading / virtualization

xCustomListView v1.50 adds an important new event named VisibleRangeChanged. This event is fired whenever the visible range of items changes.

We can use this event to defer the items creation. This can significantly improve the performance of lists with complex items.

As an example, if we try to create a list with 1000 cards with this code (based on https://www.b4x.com/android/forum/threads/cards-list-with-customlistview.87720/#content):
B4X:
Sub FillList
   Dim bitmaps As List = Array("pexels-photo-446811.jpeg", "pexels-photo-571195.jpeg", _
       "pexels-photo-736212.jpeg", "pexels-photo-592798.jpeg")
   Dim n As Long = DateTime.Now
   For i = 1 To 1000
       Dim content As String = $"Lorem ipsum dolor sit amet,
consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."$
       CLV1.Add(CreateItem(CLV1.AsView.Width, $"This is item #${i}"$, bitmaps.Get((i - 1) Mod bitmaps.Size), content), "")
   Next
   Log("Loading cards took: " & (DateTime.Now - n) & "ms")
End Sub

It takes almost 10 seconds. Not good enough...

So instead we create empty cells and only load the items when they become visible:

Global type:
B4X:
Type CardData (Title As String, Content As String, BitmapFile As String)

B4X:
Sub FillList2
   Dim bitmaps As List = Array("pexels-photo-446811.jpeg", "pexels-photo-571195.jpeg", _
       "pexels-photo-736212.jpeg", "pexels-photo-592798.jpeg")
   Dim n As Long = DateTime.Now
   For i = 1 To 1000
       Dim content As String = $"Lorem ipsum dolor sit amet,
consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."$
       Dim cd As CardData
       cd.Initialize
       cd.Title = $"This is item #${i}"$
       cd.Content = content
       cd.BitmapFile = bitmaps.Get((i - 1) Mod bitmaps.Size)
       Dim p As B4XView = xui.CreatePanel("")
       p.SetLayoutAnimated(0, 0, 0, CLV1.AsView.Width, 280dip)
       CLV1.Add(p, cd)
   Next
   Log("Loading cards took: " & (DateTime.Now - n) & "ms")
End Sub

Sub CLV1_VisibleRangeChanged (FirstIndex As Int, LastIndex As Int)
   Dim ExtraSize As Int = 20
   For i = Max(0, FirstIndex - ExtraSize) To Min(LastIndex + ExtraSize, CLV1.Size - 1)
       Dim p As B4XView = CLV1.GetPanel(i)
       If p.NumberOfViews = 0 Then
           Dim cd As CardData = CLV1.GetValue(i)
           '**************** this code is similar to the code in CreateItem from the original example
           p.LoadLayout("Card1")
           lblTitle.Text = cd.Title
           lblContent.Text = cd.Content
           SetColorStateList(lblAction1, xui.Color_LightGray, lblAction1.TextColor)
           SetColorStateList(lblAction2, xui.Color_LightGray, lblAction2.TextColor)
           ImageView1.SetBitmap(xui.LoadBitmapResize(File.DirAssets, cd.BitmapFile, ImageView1.Width, ImageView1.Height, True))
       End If
   Next
End Sub
This time it takes 250ms for the list to be created. Scrolling the list is very smooth.

SS-2018-01-04_12.31.51.png


We can go another step and remove the invisible items. This can be relevant if the items include large bitmaps or other "heavy" UI elements.

B4X:
Sub CLV1_VisibleRangeChanged (FirstIndex As Int, LastIndex As Int)
   Dim ExtraSize As Int = 20
   For i = 0 To CLV1.Size - 1
       Dim p As B4XView = CLV1.GetPanel(i)
       If i > FirstIndex - ExtraSize And i < LastIndex + ExtraSize Then
           'visible+
           If p.NumberOfViews = 0 Then
               Dim cd As CardData = CLV1.GetValue(i)
               p.LoadLayout("Card1")
               lblTitle.Text = cd.Title
               lblContent.Text = cd.Content
               SetColorStateList(lblAction1, xui.Color_LightGray, lblAction1.TextColor)
               SetColorStateList(lblAction2, xui.Color_LightGray, lblAction2.TextColor)
               ImageView1.SetBitmap(xui.LoadBitmapResize(File.DirAssets, cd.BitmapFile, ImageView1.Width, ImageView1.Height, True))
           End If
       Else
           'not visible
           If p.NumberOfViews > 0 Then
               p.RemoveAllViews '<--- remove the layout
           End If
       End If
   Next
End Sub

You need to remember that you can no longer access UI elements of random items. If there is any state that needs to be preserved then you should add it to the custom type that is used as the item's value.

As an example we will change the content color whenever an item is clicked:
B4X:
Type CardData (Title As String, Content As String, BitmapFile As String, Color As Int) 'new Color field

B4X:
Sub CLV1_ItemClick (Index As Int, Value As Object)
   UpdateItemColor(Index, Rnd(0xff000000, 0xffffffff))
End Sub

Sub UpdateItemColor (Index As Int, Color As Int)
   Dim cd As CardData = CLV1.GetValue(Index)
   cd.Color = Color
   Dim p As B4XView = CLV1.GetPanel(Index)
   If p.NumberOfViews > 0 Then
       'get the content label view (it is inside an additional panel)
       Dim ContentLabel As B4XView = p.GetView(0).GetView(1)
       ContentLabel.TextColor = Color
   End If
End Sub

When an item becomes visible we also call UpdateItemColor:
B4X:
Sub CLV1_VisibleRangeChanged (FirstIndex As Int, LastIndex As Int)
   Dim ExtraSize As Int = 20
   For i = 0 To CLV1.Size - 1
       Dim p As B4XView = CLV1.GetPanel(i)
       If i > FirstIndex - ExtraSize And i < LastIndex + ExtraSize Then
           'visible+
           If p.NumberOfViews = 0 Then
               Dim cd As CardData = CLV1.GetValue(i)
               p.LoadLayout("Card1")
               lblTitle.Text = cd.Title
               lblContent.Text = cd.Content
               SetColorStateList(lblAction1, xui.Color_LightGray, lblAction1.TextColor)
               SetColorStateList(lblAction2, xui.Color_LightGray, lblAction2.TextColor)
               ImageView1.SetBitmap(xui.LoadBitmapResize(File.DirAssets, cd.BitmapFile, ImageView1.Width, ImageView1.Height, True))
               UpdateItemColor(i, cd.Color) '<-------------
           End If
       Else
           'not visible
           If p.NumberOfViews > 0 Then
               p.RemoveAllViews
           End If
       End If
   Next
End Sub

SS-2018-01-04_12.58.56.png
 
Last edited:

MarcoRome

Expert
Licensed User
Longtime User
xCustomListView v1.50 adds an important new event named VisibleRangeChanged...

Hi Erel, where i found 1.50 i see HERE but i see only 1.20
 
Last edited:

Roberto P.

Well-Known Member
Licensed User
Longtime User
well, it would be handy to have the project updated. thank you ;)
 

Angel Garcia

Member
Licensed User
Hi All,
I have an issue with this example, it works great with several items, but after a second reload with just one item result, it shows nothing and i think its because the VisibleRangeChanged event doesn't fire because no range changed after first load of one item result.
Of course i call the CLV1.Clear before reloading items, i think i'm missing a line of ReDrawing CLV1 or something.
Can anybody can help me out?
Many thanks in advance!

EDIT:
Finally i solved calling after the loading:
CLV1_VisibleRangeChanged(0,0)

But i don't know if its the cleaner solution
 
Last edited:

b4xscripter

Member
Licensed User
Longtime User
Hi,

Many thanks for this thread that is very useful!

Where can I find the update project?

Thank you!
 

Martin Larsen

Active Member
Licensed User
Longtime User
This is a godsend for my project as I was facing out of memory issues with my xCLV.

However, there is a problem as calling CLV.clear makes the app crash with the following exception:

B4X:
customlistview_findindexfromoffset (B4A line: 370)
Dim CurrentItem As CLVItem = items.Get(Position)
java.lang.IndexOutOfBoundsException: Invalid index 0, size is 0
    at java.util.ArrayList.throwIndexOutOfBoundsException(ArrayList.java:255)
    at java.util.ArrayList.get(ArrayList.java:308)
    at anywheresoftware.b4a.objects.collections.List.Get(List.java:117)
    at b4a.example3.customlistview._findindexfromoffset(customlistview.java:475)
    at b4a.example3.customlistview._getfirstvisibleindex(customlistview.java:585)
    at b4a.example3.customlistview._updatevisiblerange(customlistview.java:1620)
    at b4a.example3.customlistview._scrollhandler(customlistview.java:1444)
    at b4a.example3.customlistview._sv_scrollchanged(customlistview.java:1596)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:515)
    at anywheresoftware.b4a.BA.raiseEvent2(BA.java:186)
    at anywheresoftware.b4a.BA$1.run(BA.java:325)
    at android.os.Handler.handleCallback(Handler.java:733)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:136)
    at android.app.ActivityThread.main(ActivityThread.java:5001)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:515)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:785)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:601)
    at dalvik.system.NativeStart.main(Native Method)

It has nothing to do with VisibleRangeChanged as this never called after clearing the CLV. The app crashes as soon as CLV.Clear is called.

Note: The problem only occurs if the CLV has been scrolled before called Clear.

I have attached an example based on the original demo code.
 

Attachments

  • xCustomListView_with_clear.zip
    17.8 KB · Views: 2,111

DonManfred

Expert
Licensed User
Longtime User
1, You should always create a new Thread for your question!
2.
owever, there is a problem as calling CLV.clear makes the app crash with the following exception:

No, the error happens in
B4X:
Dim CurrentItem As CLVItem = items.Get(Position)
java.lang.IndexOutOfBoundsException: Invalid index 0, size is 0
The list is empty but you are trying to get the first item from it....
 
Top