Xamarin.Forms: StackLayout vs Grid
When using Xamarin.Forms, it's pretty easy to get
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
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
This is a case where knowing how Xamarin.Forms works
"under the hood" can help us write better apps.
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:
Grid, measurement only happens when
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
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.
Suppose I want a horizontal
layout which has a label filling the space between two buttons.
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>
<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>
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.
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
VerticalOptions with a
that has alignment set to anything but
(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
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
End, 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.
Endare 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
Layoutitself 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
LayoutOptionsalignment for a child of a
Grid, 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
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".
If you want to avoid measurement in your UI:
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:
StackLayout, 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.
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.
I am not saying you should never use Xamarin.Forms in ways
that need child view measurement.
If you actually need
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: