Skip to main content

Migration Strategies

Prerequisites​

Before reading this page, you may find it helpful to review the following sections of the Tutorial:

Adopting Litho in your app​

Using Litho in a new surface is fairly straightforward: you can put a LithoView at the root of your new Fragment or Activity and start writing your components. However, adopting Litho within an existing surface needs to be done more incrementally and can require a bit more thought.

Litho components can interoperate with Views in the same App or even on the same screen, so you can migrate View-based screen to Litho incrementally and maintain a hybrid Component-View UI.

There are two common strategies for incrementally migrating to Litho: Bottom Up and Top-down, as detailed in the following sub-sections.

Let's use the simple UI shown below as an example:

litho-compound-component

This is how the XML/View implementation of the above UI could look like:

XMLView
<!-- my_custom_layout.xml -->
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/ic_launcher"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="Hello there"/>
</LinearLayout>
class MyCustomLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {
init {
orientation = HORIZONTAL
layoutParams = LayoutParams(
MATCH_PARENT,
WRAP_CONTENT
)

addView(
ImageView(context).apply {
layoutParams = LayoutParams(
100.dpToPx,
100.dpToPx)
setImageResource(R.drawable.ic_launcher)
}
)

addView(
TextView(context).apply {
layoutParams = LayoutParams(
WRAP_CONTENT,
WRAP_CONTENT).apply {
gravity = CENTER_VERTICAL
}
text = "Hello there"
}
)
}
}

Bottom-up​

With the bottom-up approach, you break down the UI into smaller pieces that can be converted incrementally. The View or ViewGroup in the original implementation is replaced by a LithoView that you attach as child to the root ViewGroup of your UI.

In the example component above, you can identify two UI blocks, which can be converted independently into two Litho components: Image and Text

Litho provides a library of widget components, which you can immediately start using. If your app has a custom design system that implements custom views for primitives such as Button, Text or Image, you can start by creating Primitive Components) for them first; you can also reuse these components across the app to convert multiple screens to Litho.

Once you've completed the conversion of the Views, you can coalesce all the individual Primitive Components into a single KComponent and use one LithoView as the root of the UI.

Here is an example showing how the example component could be implemented with a bottom-up:

class MyCustomComponent : KComponent() {
override fun ComponentScope.render(): Component {
return Row {
child(
Image(
drawable = drawableRes(R.drawable.ic_launcher),
style = Style.width(100.dp).height(100.dp))
)
child(
Text(
text = "Hello there",
style = Style.alignSelf(YogaAlign.CENTER))
)
}
}
}

It's recommended to use the bottom-approach because this way you can use the full potential of Litho and its performance optimisations such as:

  • Incremental mount
  • View flattening
  • Moving work to background thread
  • View/Drawable pooling and preallocation
  • Granular UI updates

Top-down​

With the top-down approach, you replace the root ViewGroup of your UI with a LithoView and wrap the root View representing the UI into a Primitive Component(). As you convert smaller parts of the UI into Components, you extract them out of the Primitive Component and into individual LithoViews.

Here is how the example component could be implemented with the top-down approach:

class MyCustomComponent : PrimitiveComponent() {
override fun PrimitiveComponentScope.render(): LithoPrimitive {
return LithoPrimitive(
layoutBehavior = MyCustomLayoutBehavior,
mountBehavior = MountBehavior(ALLOCATOR) {},
style = null)
}
}

private val ALLOCATOR: ViewAllocator<LinearLayout> =
ViewAllocator { context ->
// Either instantiate the View
MyCustomLayout(context) as LinearLayout
// or inflate the XML
LayoutInflater.from(context)
.inflate(
R.layout.my_custom_layout,
null
) as LinearLayout
}

private object MyCustomLayoutBehavior : LayoutBehavior {
override fun LayoutScope.layout(
sizeConstraints: SizeConstraints
): PrimitiveLayoutResult {
val content = ALLOCATOR.createContent(androidContext)
content.measure(
sizeConstraints.toWidthSpec(),
sizeConstraints.toHeightSpec()
)

return PrimitiveLayoutResult(
width = max(sizeConstraints.minWidth, content.measuredWidth),
height = max(sizeConstraints.minHeight, content.measuredHeight))
}
}

This may be a good starting point, and in many cases it will be faster to implement than the bottom-up approach, especially when wrapping complex Views containing lots of internal logic. However, components implemented with the top-down approach won't be as performant as the ones implemented with the bottom-up approach, because Litho won't be able to perform all of the available optimizations on components implemented with the top-down approach. For example:

  • Less work can be moved to background thread - when the logic to initialise the View is encapsulated inside the View, then it is necessary to do it on the main thread.
  • Possible corretness issues - mixing composite Views and component wrappers can be prone to accidental mutations of the internal View state, causing the component and View state to diverge.

Litho will still apply the following optimizations making the wrapped Views more performant compared to not using Litho at all:

  • Incremental Mount
  • Pooling and preallocation

Some scenarios when the top-down approach is suitable include:

  • Using Litho for the architecture of your surface and for writing new features, but existing Views might not be immediately converted to Litho.
  • Converting a list surface to Sections. The root of the surface is a LithoView rendering a RecyclerCollectionComponent, while the individual list items can be either Views or Litho Components. You can leverage the Litho Lists API for features such as asynchronous data diffing or granular RecyclerView.Adapter updates before converting the entire UI to Litho.

More complex example​

Consider the following UI as an example:

post-breakdown

You can identify three UI blocks, which can be converted independently into three Litho Components:

  • Header
  • Media
  • Footer

You'll have three LithoViews in your UI to render the components.

These components will be composed of smaller widgets such as Text or Image, similar to how ViewGroups arrange smaller Views.

Just like in the simpler example above - when the incremental conversion is complete, the individual Primitive Components can be merged into a single KComponent.

With the top-down approach, you'd replace the root ViewGroup of your UI with a LithoView and wrap the root View representing the UI into a Primitive Component(). As you convert smaller parts of the UI into Components, you extract them out of the Primitive Component and into individual LithoViews.