Skip to main content

Lifecycle of a Primitive Component

A Primitive represents a reusable unit responsible for hosting the logic to create, measure, and mount the content that the Primitive Component will render.

As illustrated in the Creating a Primitive Component page, the render() method should return a Primitive implementation and any Style object to be applied to the component on the LithoPrimitive object.

This page provides an overview of a Primitive: a composable API that can be configured to provide a bespoke implementation.

A Primitive consists of:

  • LayoutBehavior - defines how a Primitive measures itself
  • MountConfiguration - defines how a Primitive mounts and configures a View or a Drawable associated with that Primitive

Lifecycle of a Primitive​

A Primitive has four important stages in its lifecycle, which occur in the following order:

  1. Creation
  2. Content size measurement
  3. Content creation
  4. Mounting and unmounting content properties

Each of these stages is detailed in the following sub-sections.

Creation of a Primitive​

In order to create a Primitive, create an instance of a Primitive class.

The following example provides an implementation of Primitive with an ImageView as content:

class SimpleImageViewPrimitiveComponent(private val style: Style? = null) : PrimitiveComponent() {

override fun PrimitiveComponentScope.render(): LithoPrimitive {
return LithoPrimitive(primitive = SimpleImageViewPrimitive, style = style)
}
}

internal val PrimitiveComponentScope.SimpleImageViewPrimitive
get() =
Primitive(
layoutBehavior = ImageLayoutBehavior,
mountBehavior =
MountBehavior(ViewAllocator { context -> ImageView(context) }) {
bind(R.drawable.ic_launcher) { imageView ->
imageView.setImageDrawable(drawableRes(R.drawable.ic_launcher))
onUnbind { imageView.setImageResource(0) }
}
})

Content size measurement​

note

This stage of the Primitive's lifecycle can occur on any thread.

Each Primitive should privide an implementation of LayoutBehavior interface to define how it measures itself given arbitrary width and height specs. The PrimitiveLayoutResult object it returns contains the width and height of the content, and optionally any layout data, as shown in the following example:

internal object ImageLayoutBehavior : LayoutBehavior {
private const val defaultSize: Int = 150

override fun LayoutScope.layout(sizeConstraints: SizeConstraints): PrimitiveLayoutResult {
return PrimitiveLayoutResult(
size =
if (!sizeConstraints.hasBoundedWidth && !sizeConstraints.hasBoundedHeight) {
Size(defaultSize, defaultSize)
} else {
Size.withEqualDimensions(sizeConstraints)
})
}
}

To learn about the different strategies to measure content, see the Measuring page.

Content creation​

note

This stage of the Primitive's lifecycle can only occur on the main thread.

Each Primitive needs to create the content it hosts (either a View or a Drawable) by prividing a ViewAllocator or a DrawableAllocator to the MountBehavior, as shown in the following example:

MountBehavior(DrawableAllocator(poolSize = 30) { MatrixDrawable<Drawable>() }) {
bindWithLayoutData<PrimitiveImageLayoutData>(drawable, scaleType) {
content,
layoutData ->
content.mount(drawable, layoutData.matrix)
content.bind(layoutData.width, layoutData.height)
onUnbind { content.unmount() }
}
}
note

The content should not be mutated based on props passed to the PrimitiveComponent.

In order to optimize the mount performance, the properties of the View/Drawable Allocator can also be customized to adjust the content pooling strategy.

Mounting and unmounting content properties​

note

This stage of the Primitive's lifecycle can only occur on the main thread.

Properties can be set and unset on the content using bindTo, bind, and bindWithLayoutData methods inside of MountBehavior scope.

The following code shows a component that appropriately sets and unsets the properties on the content:

MountBehavior(DrawableAllocator(poolSize = 30) { MatrixDrawable<Drawable>() }) {
bindWithLayoutData<PrimitiveImageLayoutData>(drawable, scaleType) {
content,
layoutData ->
content.mount(drawable, layoutData.matrix)
content.bind(layoutData.width, layoutData.height)
onUnbind { content.unmount() }
}
}

Methods like bind and bindWithLayoutData take dependencies as an argument. Any time dependencies changes between layouts, the onUnbind {} callback will be invoked, followed by bind or bindWithLayoutData. Dependencies should include all the props/state that are used to configure the contentinside bind/bindWithLayoutData/onUnbind calls.

note

Dependencies are checked for equivalence by calling equals.

Important

Once set, a property should be unset in the onUnbind {} callback to ensure correctness when the content is reused.