2019-09-16 15:00:00
Xamarin.Forms: StackLayout vs Grid
When using Xamarin.Forms, it's pretty easy to get
addicted to StackLayout
. And why not? It's very convenient for the developer.
Just toss in some child views, and they will get positioned
nicely in a line.
<StackLayout Orientation="Vertical"> <Label Text="One"/> <Label Text="Two"/> <Label Text="Three"/> <Label Text="Four"/> <Label Text="Five"/> </StackLayout>
In contrast, the Grid
layout is relatively tedious, since we have to
specify row and column definitions and then set the Row
and Column
attributes on each child view.
<Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Label Grid.Row="0" Grid.Column="0" Text="One" /> <Label Grid.Row="1" Grid.Column="0" Text="Two" /> <Label Grid.Row="2" Grid.Column="0" Text="Three" /> <Label Grid.Row="3" Grid.Column="0" Text="Four" /> <Label Grid.Row="4" Grid.Column="0" Text="Five" /> </Grid>
However, the developer convenience of StackLayout
comes with a
tradeoff.
This is a case where knowing how Xamarin.Forms works
"under the hood" can help us write better apps.
Measurement
A Xamarin.Forms Layout
is responsible for setting
the size and position of all its child views. The issue
in play for this blog entry is the question of whether
the child view is participating in the decision.
In every case, the decision ends with the Layout
telling the child,
"Here is the box you have to live in. Deal with it."
In some cases, that edict is all that happens.
But in other cases, the Layout
first asks the child,
"What size do you want to be?", and then it uses that information
to make a final decision.
In terms of performance, this "measurement" step can be expensive. It is worth avoiding when it is not needed.
So how do we do that? In other words, in the text above, when I said "in some cases" and "in other cases", exactly which cases are which?
Grid, Auto, StackLayout
Here's a mostly-correct answer:
With a
Grid
, measurement only happens whenGridLength
usesGridUnitType.Auto
.With a
StackLayout
, measurement always happens.
And this is why my fondness for StackLayout
is fading fast.
It's a convenient abstraction, but it always
measures its child views, whether that measurement is necessary or not.
On the other hand, if I do the extra (read: "tedious") work involved in using a Grid
, and
if I take the time to define the rows and
columns such that Auto
is not needed, I can eliminate the
measurement and get a better performing UI.
Example
Suppose I want a horizontal
layout which has a label filling the space between two buttons.
With a StackLayout
, the corresponding XAML might look like this:
<StackLayout Orientation="Horizontal"> <Button Text="One" /> <Label Text="Hello" HorizontalOptions="FillAndExpand" HorizontalTextAlignment="Center" /> <Button Text="Two" /> </StackLayout>
The equivalent Grid
is:
<Grid> <Grid.RowDefinitions> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <Button Grid.Row="0" Grid.Column="0" Text="One" /> <Label Grid.Row="0" Grid.Column="1" Text="Hello" HorizontalTextAlignment="Center" /> <Button Grid.Row="0" Grid.Column="2" Text="Two" /> </Grid>
The Grid
certainly does involve more clutter. However, now I can tweak the column definitions to get rid of Auto
. For example, if I give the buttons absolute widths, they don't need to be measured during layout anymore:
<Grid.ColumnDefinitions> <ColumnDefinition Width="80"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="80"/> </Grid.ColumnDefinitions>
As I said, StackLayout
is more convenient. There may be cases where it is a bit tricky to express a desired layout with row and column definitions. Don't give up too easily. The layout capabilities of Grid
are very powerful.
More correct
Remember earlier when I tagged an answer as "mostly-correct"? Well, it would have been a bit more correct if I had taken the time to explain the following exception to the rule:
Measurement can still happen without GridUnitType.Auto
if you use
HorizontalOptions
or VerticalOptions
with a LayoutOptions
that has alignment set to anything but Fill
.
(The previous paragraph was a mouthful, which is why I didn't want to inline that explanation into the section above.)
It remains true that as long as GridUnitType.Auto
is nowhere in sight, measurement is not
used to determine the row and column sizes for the grid.
The grid calculates the proper sizes for all its rows and
columns without asking for advice from the child views.
And then, as always, it tells each child view, "Here is the box you have to live in. Deal with it."
But then, when it comes time to "Deal with it", depending on the LayoutOptions
,
measurement may be needed to determine
how the child view is placed into that box:
If the alignment is
Fill
, the child view is given the same size as the grid cell into which it is being placed, so we don't need measurement. (Yay!)But if the alignment is
Start
,Center
, orEnd
, then that child view will instead be allowed to determine its own size and position within the box, which means we are back to needing measurement.
For example, suppose a grid cell is determined to have a width of 100, and a label therein would say (if asked) that it wants to have a width of 80.
If that label has a horizontal alignment of
Fill
, it will not be measured, and it will get a width of 100.But if the alignment is
Start
, then measurement will happen, and it will get its requested width of 80, and it will be placed at the left (Start
) edge of the grid cell, and there will be 20 units of unused space to its right.Alignments of
Center
andEnd
are handled similarly.
To be fair, in terms of performance, this particular measurement scenario is not as bad as the cases mentioned above.
When measurement happens because of
GridUnitType.Auto
, it can affect the height or width of all the child views in that row or column, and the grid layout code has to sort all that out.Even worse, if measurement happens in a child view and the
Layout
itself is also being measured (because it is nested), then the effects can bubble all the way to to the top of the layout hierarchy.But when measurement happens because of
LayoutOptions
alignment for a child of aGrid
, it only matters to that one child view, so its effects are more isolated.
Still a little bit incorrect
A thorough discussion of Xamarin.Forms layout would be lengthy, and I'm trying to keep this blog post fairly focused. I have intentionally omitted some things.
There are places in my explanations above where I speak of a grid cell in the singular when in fact it could be a span.
Also, I didn't bother discussing the case where the measured size of a child view is larger than the size of grid cell provided by the grid.
And I didn't talk about how WidthRequest
and HeightRequest
are
used with measurement.
Finally, I completely ignored the issue of margins and how they affect the way the child view is placed during "Deal with it".
Summarizing
If you want to avoid measurement in your UI:
Use
Grid
instead ofStackLayout
Don't use
GridUnitType
.Auto
Don't use
HorizontalOptions
orVerticalOptions
Problems
When you ask a child view to measure itself, the work to be done depends on what kind of child view it is.
If it's a label, then something has to figure out the width and height of its text. That involves font metrics, which probably has to happen in platform-specific code.
If that child view is an image, then something has to figure out its width and height. That involves reading (at least part of) the image file and decoding it. And if the image file is actually coming from, say, an HTTP access, now we have a network delay. And as always, make sure that website is using TLS. So, now your simple need for a width involves file I/O, network latency, data compression, and cryptography.
And what if the child view is itself a Layout
? Well, that
Layout
probably can't measure itself without asking its child views
for measurements, after which it'll have some arithmetic to do.
Now we've got recursion.
So if you're trying to get the slowest possible UI with Xamarin.Forms, follow these guidelines:
Nest a
StackLayout
inside anotherStackLayout
, at least three or four deep.Throughout that layout hierarchy, put in some labels. And use text wrapping, to make the measurement harder.
And add some images. Even better if they have to be loaded from a website. On a different continent.
Set up some background timers to change the text on those labels every few seconds, so the layout cycle will need to be rerun regularly.
That should be fairly effective way to drain a mobile phone battery for no actual benefit.
Caveats
It is worth mentioning that having a child view provide information about its desired size is a feature that has legitimate use cases.
First of all, I acknowledge that there could be situations where the "performance vs developer convenience" tradeoff is worth it.
A better example is the issue of localization. The length of text (on, say, a label) can change a great deal depending on which natural language is being used. It is very common to create a piece of UI sized for a string in English and then discover that the text no longer fits after translation to another language. Allowing the label to request a size is a natural way to deal with that problem.
Bottom line
I am not saying you should never use Xamarin.Forms in ways
that need child view measurement.
If you actually need StackLayout
or GridUnitType.Auto
, go ahead
and use them.
Rather, I am recommending awareness of the tradeoffs:
Consider the performance benefits of avoiding child view measurement wherever possible.
Be aware that using
StackLayout
, although convenient, means you are using child view measurement all the time.
For further reading on Xamarin.Forms performance, I suggest:
https://docs.microsoft.com/en-us/xamarin/xamarin-forms/deploy-test/performance