Closed TimLariviere closed 2 years ago
@twop This is finally ready for review 😃
Just finished reading the description. And I have to say I'm in awe of your work and ability to describe such a monumental effort.
F^^&**g incredible job! <3
Looks fantastic! Again, awesome job!
My only concern is GetViewType
for Memo
, I don't have a concrete proposal (yet?), but I'm wondering about your thoughts there.
Yes, I agree Memo
in a virtualized list is something we shouldn't do.
Having a large number of memoized widgets can be an issue.
I will remove the support for it.
I changed View.memo: key -> fn -> WidgetBuilder
to fn -> key -> WidgetBuilder
, that way we're following the same signature than Elm's Html.lazy
. This allows us to simply prefix with View.memo
the view we want to memoize.
MainPage.view data
// becomes
View.memo MainPage.view data
Which is nicer for partial applications
let detailPage =
model.DetailPageModel
|> Option.map (View.memo DetailPage.view)
On memo
vs lazy
, I renamed it to lazy'
(with quote at the end).
Nicer for a lazyMap
to combine Memo and MapMsg
View.lazy' MyPage.view myPageModel
View.lazyMap MyPageMsg MyPage.view myPageModel
On
memo
vslazy
, I renamed it tolazy'
(with quote at the end). Nicer for alazyMap
to combine Memo and MapMsgView.lazy' MyPage.view myPageModel View.lazyMap MyPageMsg MyPage.view myPageModel
I like that!
View.lazy'
looks a bit weird (for me), but I think it is even better because you DO want usages of lazy
to be explicit and deliberate.
Closes #9
Intro
Virtualized collection enables displaying a scrolling list of data while only instantiating the visible rows. When scrolling, the no longer visible rows are reused and updated with the new data.
In Xamarin.Forms, this is done via 2 controls: ListView and CollectionView. Also those controls support grouping data and displaying a group header/footer.
My main idea was to let the user pass in its data source and declare a template function (as well as 2 others for header/footer). Fabulous would just remap the data source (
Seq.map templateFn items
) and pass it to Xamarin.Forms, avoiding enumeration.Usages
How ListView and CollectionView work in Xamarin.Forms
Virtualization in ListView/CollectionView works by combining 2 properties:
ItemsSource
: a list of raw dataDataTemplate
: a template object used to describe what the row should look likeGiven the MVVM nature of XF, row reuse is mostly driven by bindings. DataTemplate doesn't know about the raw data it will receive and only setups binding.
Xamarin.Forms creates a row using the
DataTemplate
and sets itsBindingContext
with the raw item. This triggers refresh of the row UI. Same on row reuse, XF sets the new raw item intoBindingContext
and refreshes the bindings.This model doesn't work with Fabulous at all, due to the lack of Binding. Fortunately, we can work with
DataTemplateSelector
to access the current item before returning the appropriateDataTemplate
. This allows Fabulous to support virtualization.How it works in Fabulous
Semantically speaking, our
Widget
is very close toDataTemplate
. They both describe what a specific piece of UI should look like. The main difference is thatDataTemplate
doesn't know of the data it will work with in advance, whereasWidget
has been built with that data.So the main challenge was to make the 2 concepts compatible.
Simple collections (aka not grouped)
Storing data and the template function
In Fabulous v1, we were creating all
ViewElement
items (Widget
in v2) on each view update. This can be problematic in case you have a large number of items.Actually thanks to how virtualization works, we only need to "process" a couple of items for the visible rows. No need to go create all widgets on each view update.
To support that, the following type has been created:
This
WidgetItems
is created by the functionViewHelpers.buildItems
and is stored directly in theItemsSource
scalar attribute.Note the use of
'itemMarker
to enforce the type of widget itemsKeeping the original items allows us to directly compare them on each update to determine if we need to update the UI.
Currently, scalar attributes in Fabulous have 2 generic parameters: the
'inputType
and'modelType
.The
'inputType
is what we expect the users to provide us. The'modelType
is an optimized representation of the same data (eg.'inputType
isint list
,'modelType
will beint[]
).Before storing the attribute value, we convert
'inputType
to'modelType
through theConvert
function declared in theScalarAttributeDefinition
;'inputType
is never stored.But the comparison function shown just above is only done between 2
'modelType
s, but ListView/CollectionView expect anIEnumerable
and not aWidgetItems
.So to support this, we need another generic parameter
'valueType
and the correspondingConvertValue: 'modelType -> 'valueType
function.With this, we can still store and compare
WidgetItems
, but when we actually need to apply the value to the XF property, we callConvertValue
to transform it. Attributes that don't need conversion can use theid
function to make it transparent.For
WidgetItems
, we build an IEnumerable on the fly using the original items and the template function before assigning it to the XF propertyItemsSource
.Now Xamarin.Forms has a list of Widgets, and thanks to
seq
, it will only enumerate the items it needs to display.Loading Widgets into rows
Unlike DataTemplate in MVVM apps, instead of setting bindings to capture the raw data inside
BindingContext
, we now have aWidget
.To make it work, we need 2 things:
BindingContextChanged
and run the Reconciler when a widget is attached to the rowNote that like said earlier, DataTemplate are created before knowing which value they will host. This means we can only create an empty row for now. So to enable row reuse later, we extract the root target type of a widget and create an empty row with it.
eg.
will have a target type of
ViewCell
and we create an emptyViewCell
.Side note: Given the potential high cost of instantiate a lot of
View.lazy'
, its use is not allowed in virtualized collections.This DataTemplateSelector instantiates a
WidgetDataTemplate
that will create the appropriate XF control and listen toBindingContextChanged
.For fresh rows, Xamarin.Forms will execute that function and then set the
BindingContext
with the widget from our list. When XF reuses a row, it will set the new widget in theBindingContext
as well. Each time, we call the Reconciler to update the row.Final step is to pass this
WidgetDataTemplateSelector
in the XF propertyItemTemplate
. Since it will never change, we assign it when creating the controls.A
registerWithAdditionalSetup
was needed becauseDataTemplateSelector
requires access to theViewNode
of the ListView/CollectionView, and it was not possible to do it in the constructor of the controls.Grouped collections
Grouped ListView/CollectionView is slightly different. Instead of having a 1-dimensional enumerable of raw data, we have a 2-dimension one (eg.
IEnumerable<IEnumerable<T>>
)To support this with everything we saw just before,
GroupItem
has been created.Instead of applying a list of
IEnumerable<Widget>
to XF propertyItemsSource
, we apply a list ofIEnumerable<GroupItem>
.The builder function takes 3 template functions instead of 1: the group header template, the item template and the group footer template. All of which are called on
ConvertValue
to create the list of 'GroupItem'.Since the data source is different, we need a different
DataTemplate
for XF propertiesGroupHeaderTemplate
andGroupFooterTemplate
. Even with grouping enabled,ItemTemplate
remains a simpleWidget
-> row.GroupedWidgetDataTemplateSelector
will either use theHeader
orFooter
widgets to create the corresponding rows.And when registering the ListView/CollectionView types, we enable the
IsGrouped
flag.Acceptance criteria
ItemsSource
andItemTemplate
TextCell
->TextCell
)