Skip to main content

Layout System with Flexbox

Litho uses the Yoga library, which is an implementation of Flexbox, to measure and layout on-screen components. If you have used Flexbox on the web before then this should be very familiar. If you are more familiar with how Android normally performs Layout then Flexbox will remind you of LinearLayout.

Flexbox attributes

This section shows how various attributes of Flexbox are used for on-screen layout.

Layout direction

Flexbox is a one dimensional layout model, which means all elements are placed along one line. This leads to the question, "Should your elements be laid out horizontally, or vertically?".

The primary axis of layout is called the Main Axis, and the direction perpendicular to it is called the Cross Axis. For example, if you wanted your children to be laid out left to right, then the Main Axis would be the horizontal axis, and the Cross Axis would be the vertical axis.

Litho uses special container components to define direction of the layout, as shown in the following table.

ComponentMain AxisChild Elements LayoutIf Wrapping is Enabled
Rowhorizontalleft to rightNext line will start under the first item on the left of the container.
Row (reverse)horizontalright to leftNext line will start under the first item on the right of the container.
Columnverticaltop to bottomNext line will start to the right of the first item on the top of the container.
Column (reverse)verticalbottom to topNext line will start to the right of the first item on the bottom of the container.

The above container components are illustrated in the following diagram.

flex-direction
note

The (reverse) option is available through the .reverse(boolean) method on Row/Column objects.

Distribution along the main axis

If your container has more space than is necessary along the Main Axis then the way children are positioned across that axis needs to be considered.

The justifyContent method specifies how the child elements are distributed across the Main Axis. It takes a YogaJustify enum input, which has the following possible values:

  • FLEX_START (default) - position children at the start of the container, along the main axis.
  • FLEX_END - position children at the end of the container, along the main axis.
  • CENTER - position children in the centre of the container, along the main axis.
  • SPACE_BETWEEN - distribute extra space evenly between children.
  • SPACE_AROUND - distribute space evenly around each child. Note that adjacent children will have '2x' space between them (because each child has its own 'padding').
  • SPACE_EVENLY - distribute space evenly around all children.

The YogaJustify values are illustrated below.

justify-content

Distribution along the cross axis

If the elements in your line have different heights, then the way they are positioned along the line needs to be considered.

The alignItems method specifies how the container's children are distributed across the Cross Axis. It takes a YogaAlign enum input, which has the following possible values:

  • STRETCH (default) - stretch the size of all elements to completely fill the line.
  • FLEX_START - align elements with the start of the cross axis.
  • FLEX_END - align elements with the end of the cross axis.
  • CENTER - align elements with the centre of the line.
  • BASELINE - align elements with respect to their baselines.

The YogaAlign values are illustrated below.

align-items

Wrapping to multiple lines

In addition to the one-dimensional Flexbox laying out its children in one line, Flexboxes can also wrap their children onto multiple lines, much like text wraps. You specify wrapping behavior with the Wrap method, which takes a YogaWrap enum value that has the following possible values:

  • NO_WRAP (default) - there is no wrapping. Children are forced into a single line; if they cannot fit, they will overflow.
  • WRAP - elements that overflow a single line will be moved to the next line. If the main axis is horizontal, then the next line will be below the previous line.
  • WRAP_REVERSE - similar to WRAP except the order of lines is reversed. If the main axis is horizontal, then the next line will be above the previous line.

The YogaWrap values are illustrated below.

wrap

Line distribution

If your container has extra space in the Cross Axis direction, then the way lines are distributed in that space needs to be considered.

The alignContent method specifies how lines are distributed along the Cross Axis. It takes a YogaAlign enum value, which has the following possible values:

  • STRETCH (default) - lines are stretched evenly to fill the available space in the cross axis.
  • FLEX_START - lines are placed at the start of the cross axis.
  • FLEX_END - lines are placed at the end of the cross axis.
  • CENTER - lines are placed in the centre of the cross axis.
  • SPACE_BETWEEN - evenly distributes extra space between all lines.
  • SPACE_AROUND - pads lines above and below with extra space.

The YogaAlign values, for Lines, are illustrated below.

align-content

For more information on specific Flexbox properties, refer to the Yoga documentation or explore any web resource on how Flexbox functions.

Yoga playground

You can also use the Yoga Playground to try different layout configurations and generate corresponding Litho code, as shown in the following screenshot.

Yoga Playground

Android Views vs. Litho with Yoga

This section lists typical layout configurations in Android and how they translate to Litho with Yoga.

Vertically stacked items

The following table shows vertically stacked items for Android Views and Litho with Yoga.

Android ViewsLitho with Yoga
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Text 1" />
<TextView
android:id="@+id/text2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Text 2" />
</LinearLayout>
Column {
child(Text(text = "Text 1"))
child(Text(text = "Text 2"))
}

Two items equally stretched horizontally

To achieve an effect similar to a LinearLayout with weights, Flexbox provides a concept called flexGrow(<weight>), as featured in the following table.

Android ViewsLitho with Yoga
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:backgroundColor="@color/red"
android:layout_weight="1"/>
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:backgroundColor="@color/blue"
android:layout_weight="1"/>
</LinearLayout>
Row {
child(Image(style = Style.flex(grow = 1f), drawable = ColorDrawable(Color.RED)))
child(Image(style = Style.flex(grow = 1f), drawable = ColorDrawable(Color.BLUE)))
}

Absolutely positioned items

If you'd like to overlay one view on top of the other, similar to a FrameLayout, Flexbox can achieve that with positionType(ABSOLUTE), as featured in the following table.

Android ViewsLitho with Yoga
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/some_big_image"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Overlaid text"/>
</FrameLayout>
Column {
child(Image(
style = Style.width(100.dp).height(100.dp),
drawable = drawableRes(R.drawable.some_big_image)))
child(Text(
style = Style.positionType(YogaPositionType.ABSOLUTE),
text = "Overlaid text"))
}

Troubleshooting

There are few issues you may face while using flexbox. This section discusses the more common of those issues; which may prove useful when debugging and finding issues in layout. For debugging you can also refer to Flipper layout inspector plugin to see the component hierarchy and you can update the flex properties in the plugin itself to understand how it affects layout. Other tool that can be used for understanding and playing around with flexbox properties is Yoga Playground.

Why is my text cut off?

Common problem is that there is not enough space for content to be visible on the screen and therefore text gets truncated. The default value of flexShrink is 1.0f, so if there is not enough space available for the text to render completely then text will shrink and you will see truncated text on the screen. Changing the flexShrink value to zero, will make sure that text component is not shrinked.

Let's understand this using an example. Here we have two text components in Row. If no flex properties are defined on either of the text component then they both will take equal space and text will be truncated if text is too long to fit into available space. Now, if we add flexShrink = 0f to first text then it will not shrink and take up the whole space to render itself completely and second text will take up the remaining space.

override fun ComponentScope.render(): Component {
return Row(style = Style.padding(16.dp)) {
child(
Text(
style =
Style.flex(shrink = 0f), // If flexShrink is 1f then this text will be truncated
text = "This is a really long text.",
textSize = 20.sp,
maxLines = 1))
child(
Text(
style = Style.margin(start = 8.dp),
text = "Another long text",
textSize = 20.sp,
maxLines = 1))
}
}

But second text is still truncated, based on your requirements - either you can make it multiline text or display these two texts inside a Column.

Another scenario where text can get truncated is if you have exact width and height defined on sibling component which is taking up all the available space and not allowing other children to grow. In this case, you can either remove the exact width / height defined so that both siblings can grow and take up the available space or set flexShrink to zero on sibling component.

override fun ComponentScope.render(): Component {
return Column(style = Style.height(135.dp).padding(16.dp)) {
child(
Column(style = Style.heightPercent(100f).widthPercent(100f)) {
// either remove widthPercent / heightPercent here so that both siblings can grow
// equally
child(Text(text = "This is a really long text.", textSize = 20.sp, maxLines = 1))
child(
Text(
style = Style.margin(top = 8.dp),
text = "Subtitle text",
textSize = 20.sp,
maxLines = 1))
})
child(
Text(
// or add flex(shrink = 0f) so that this component does not shrink
style = Style.margin(top = 16.dp),
text = "Small footer text",
textSize = 20.sp,
maxLines = 1))
}
}

How to build overlapping components?

You can achieve this by using position type as YogaPositionType.ABSOLUTE. You can also set exact position left / right coordinates for starting position of the absolute child. Refer to Absolutely positioned items section to make components overlap each other.

How to place my components at centre?

Use alignItems CENTER, if you want to centre your component along cross axis. For example, if you want to centre component inside Row along the vertical axis.
Use justifyContent CENTER, if you want to centre your component along the main axis. For example, if you want to centre component inside Row along the horizontal axis.

If it is specific to Text then consider using verticalGravity / horizontalGravity on the Text component to keep it centre aligned.

  Text(
text = "Align this text at centre vertically using vertical gravity",
verticalGravity = VerticalGravity.CENTER,
textSize = 20.sp))

I have defined flexGrow / flexShrink but it does not do anything?

If parent has fixed width / height then flexGrow / flexshrink is not expected to work correctly since there is not enough space to work with. Check in your component hierarchy if the component which is getting clipped getting enough width / height needed to render it completely or not.

override fun ComponentScope.render(): Component {
return Column(style = Style.height(50.dp).padding(16.dp)) { // exact height defined here
child(
Text(
style =
Style.flex(
shrink =
0f), // Even with flexShrink zero, text will be cut as there is not enough
// space
text = "This is a really long text.",
textSize = 20.sp,
maxLines = 1))
child(
Text(
style = Style.margin(start = 8.dp),
text = "Another long text",
textSize = 20.sp,
maxLines = 1))
}
}

When keyboard opens then content is cut off?

When you have a screen where in different scenarios the keyboard is open as layout contains a TextInput. The same layout now need to fit inside lesser space (height in this case). There are multiple ways to solve this.
First check if your text is not cut off due to flex shrink issues or may be you are setting exact width / height on parent components in the hierarchy.
Second, you can make your content scrollable by placing your content VerticalScroll.

override fun ComponentScope.render(): Component = VerticalScroll {
Column {
for (i in 0..20) {
child(
Text(
style = Style.padding(16.dp).margin(top = 8.dp),
text = "Text counter = " + i,
textSize = 20.sp,
maxLines = 1))
}
}
}

Third, you can keep a state value which lets you know if keyboard is open or not and change your layout depending on that.