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:
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β
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β
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 providing a ViewAllocator or a DrawableAllocator to the MountBehavior
, as shown in the following example:
MountBehavior(
DrawableAllocator(poolSize = 30, canPreallocate = true) {
MatrixDrawable<Drawable>()
}) {
bindWithLayoutData<PrimitiveImageLayoutData>(drawable, scaleType) {
content,
layoutData ->
content.mount(drawable, layoutData.matrix)
content.bind(layoutData.width, layoutData.height)
onUnbind { content.unmount() }
}
}
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β
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, canPreallocate = true) {
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 content inside bind/bindWithLayoutData/onUnbind calls.
Dependencies are checked for equivalence by calling equals.
Once set, a property should be unset in the onUnbind {}
callback to ensure correctness when the content is reused.