- Getting started
- The MVU architecture
Views and Performance
(topic last updated: pending)
The performance of your app may in some cases be dominated by your view function.
This is particularly the case if many message updates are being generated and processed, though not if other operations dominate such as network latency. Improving the performance of your view function should be done with respect to your overall performance targets and needs.
On each update to the model, the view function is executed. The resulting view is then compared item by item with the previous view and updates are made to the underlying controls.
As a result, views functions that are frequently executed (because of many message updates) are generally only
efficient for large UIs if the unchanging parts of a UI are “memoized”, returning identical
objects on each invocation of the
This must be done explicitly. One way of doing this is to use
Here is an example for a 6x6 Grid that depends on nothing, i.e. never changes:
let view model dispatch = ... dependsOn () (fun model () -> View.StackLayout( children= [ View.Label(text=sprintf "Grid (6x6, auto):") View.Grid(rowdefs= [for i in 1 .. 6 -> box "auto"], coldefs=[for i in 1 .. 6 -> box "auto"], children = [ for i in 1 .. 6 do for j in 1 .. 6 -> View.BoxView(Color((1.0/float i), (1.0/float j), (1.0/float (i+j)), 1.0) ) .GridRow(i-1).GridColumn(j-1) ] ) ])
Inside the function - the one passed to
dependsOn - the
model is rebound to be inaccessbile with a
DoNotUseMe type so you can’t use it. Here is an example where some of the model is extracted:
let view model dispatch = ... dependsOn (model.CountForSlider, model.StepForSlider) (fun model (count, step) -> View.Slider(minimum=0.0, maximum=10.0, value= double step, valueChanged=(fun args -> dispatch (SliderValueChanged (int (args.NewValue + 0.5)))))) ...
In the example, we extract properties
StepForSlider from the model, and bind them to
step. If either of these change, the section of the view will be recomputed and no adjustments will be made to the UI.
If not, this section of the view will be reused. This helps ensure that this part of the view description only depends on the parts of the model extracted.
You can also use
fixfunction for portions of a view that have no dependencies at all (besides the “dispatch” function)
fixffunction for command callbacks that have no dependencies at all (besides the “dispatch” function)
Optimizing view performance in advanced scenarios: the
Each time the
view function is called, Fabulous will try to update the UI the most efficiently possible by reusing existing controls as much as possible (for exact details, see Views: Differential Update of Lists of Things).
This is fine in the majority of scenarios, but some times Fabulous might reuse controls that don’t really match the expectations we can have from the code.
This is especially true if the ordering of the elements are changed, or an element has been added/removed before other elements.
This can result in unnecessary creation of controls (lower performance) or losing state of a control (like a video playing).
This is because Fabulous doesn’t know about the intent of your code, and will try to reuse controls from first to last in the list.
Say we have the following code:
View.StackLayout([ if model.ShowFirstVideo then yield View.MediaElement(source = MediaPath "path/to/video.mp4") yield View.MediaElement(source = MediaPath "path/to/other-video.mp4") yield View.Button(text = "Toggle first video", command = (fun () -> dispatch ToggleFirstVideo)) ])
In this case, when
true, the StackLayout will have 3 children, the 1st and 2nd video players + the button.
false, there will be only 2 child left, the 2nd video player and the button.
Due to how Fabulous reuses controls between updates, when switching
true, Fabulous will remove the 2nd video player and reuse/update the 1st video player, which will lose the state of the 2nd video if it was playing.
That’s because Fabulous has no way of knowing the real intent behind your code. For it, all MediaElements are interchangeable.
To prevent that, the first thing you can do is to change ordering as little as possible.
If you really must change ordering, you can help Fabulous by providing a
key value to let Fabulous know that a specific element should reuse the same control as previously.
In our example, this would be:
View.StackLayout([ if model.ShowFirstVideo then yield View.MediaElement(source = MediaPath "path/to/video.mp4") yield View.MediaElement(key = "second-player", source = MediaPath "path/to/other-video.mp4") yield View.Button(text = "Toggle first video", command = (fun () -> dispatch ToggleFirstVideo)) ])
Now, Fabulous will be aware that
second-player should remain the same between updates and that the first MediaElement should be added/removed given the value of
key must be unique among its sibling inside a collection (e.g.
Using the same key at different places is ok.
Views: Differential Update of Lists of Things
There are a few different kinds of list in view descriptions:
- lists of raw data (e.g. data for a chart control, though there are no samples like that yet in this library)
- long lists of UI elements that are used to produce cells (e.g.
ListView, see above)
- short lists of UI elements (e.g. StackLayout
- short lists of pages (e.g. NavigationPages
The perf of incremental update to these is progressively less important as you go down that list above.
For all of the above, the typical, naive implementation of the
view function returns a new list
instance on each invocation. The incremental update of dynamic views maintains a corresponding mutable target
Children property of a
Xamarin.Forms.StackLayout, or an
ObservableCollection to use as an
ItemsSource to a
ListView) based on the previous (PREV) list and the new (NEW) list.
Fabulous prioritizes reuse in the following order:
- Same ViewElement instance (when using dependsOn)
View.Grid([ dependsOn () (fun _ _ -> View.Label(text = "Hello, World!")) ])
- Same key and control type (aka.
// Previous View View.Grid([ View.Label(key = "header", text = "Previous Header") View.Label(key = "body", text = "Previous body") ]) // New View View.Grid([ View.Label(key = "header", text = "New Header") // Will reuse previous header View.Button(key = "body", text = "New body") // Won't be able to reuse previous body since Label != Button ])
If none of the above, Fabulous will select the first element that returns
canReuseView = trueamong the eligible remaining previous elements.
If no previous element can be reused, a new one is created
Note that old keyed elements that didn’t had a matching key in the new list will be destroyed instead of being reused by new unkeyed elements to help developers avoid undesired animations, such as fade-in/fade-out on Button Text changes on iOS (#308) or ripple effects on Android Button.
In the end, controls that weren’t reused are destroyed.
- Incremental update costs minimally one transition of the whole list.
- Incremental update recycles controls as much as possible if you use the same instance or
Otherwise, there is no guarantee that you get same control next time.
NOTE: The list diffing will limit mutations to only Move, Remove, and Insert, even when more straightforward operations could be done.
This is to support the limitations imposed by how Xamarin.Forms reacts to changes in
The above is sufficient for many purposes, but care must always be taken with large lists and data sources, see
ListView above for example. Care must also be taken whenever data updates very rapidly.