Migrating Sections
This page provides guidance for migrating an existing list rendered with Sections to Lazy Collections.
Advantages of migrating to Lazy Collectionsβ
The advantages of Lazy Collections over Sections include:
- No code generation.
- A cleaner API for building scrolling lists.
- Simple inline declaration (similar to a
Row
andColumn
). - List items have direct access to the containing components props and state.
- Automatic diffing of content updates.
Migration overviewβ
The migration to Lazy Collections involves the following three steps:
- Replacing the component that renders the section tree with a Lazy Collection component β see Replace the
RecyclerCollectionComponent
. - Adding each component from the section tree as a child of the Lazy Collection β see Migrate the
GroupSectionSpec
and Migrate theDataDiffSection
. - Migrating any event handlers or state β see Migrate Event Handlers and Migrate Section State.
Each of the above steps is detailed in the following sections.
Replace the RecyclerCollectionComponent
β
Sections are typically rendered by a RecyclerCollectionComponent
:
val component = RecyclerCollectionComponent.create(c)
.section(...)
.build()
This should be replaced with a Lazy Collection component, such as LazyList
. For information on migrating the RecyclerCollectionComponent
's configuration options, see the Layout and Styling page.
val component = LazyList {
// Content will be added inline
}
Lazy Collections can only be used inside KComponent
s. If migrating a Spec component then first convert it to a KComponent
or implement the Lazy Collection inside a KComponent
container.
Next, migrate the content from the Section into the Lazy Collection, as detailed below.
Migrate the GroupSectionSpec
β
A GroupSectionSpec
is used to structure other Sections into a hierarchy. It's key part is an onCreateChildren
method that creates and combines SingleComponentSection
s, DataDiffSection
s and other custom GroupSection
s into a list of children. If onCreateChildren
's body is relatively simple, you can inline it inside Lazy Collection in the parent component, like in the code snippet above. In case of a complex children composition logic that benefits from encapsulation, you can replace GroupSectionSpec
class with a custom KComponent
that wraps Lazy Collection together with its children, and use it instead of RecyclerCollectionComponent
in the parent component. For examples of the second approach check out the code snippets below.
Replace the SingleComponentSection
sβ
The easiest part of Sections children migration is dealing with SingleComponentSection
s. The following GroupSectionSpec
renders two components as SingleComponentSection
s:
@GroupSectionSpec
object SimpleGroupSectionSpec {
@OnCreateChildren
fun onCreateChildren(c: SectionContext, @Prop title: String): Children =
Children.create()
.child(SingleComponentSection.create(c).component(Text.create(c).text(title).build()))
.child(SingleComponentSection.create(c).component(MyComponent()))
.build()
}
The component from each SingleComponentSection
should be added as a child to the Lazy Collection. Lazy Collections support both Kotlin API components and Spec children. SingleComponentSection
config options are available as parameters on child
method, such as isSticky
.
class SimpleGroupMigration(private val title: String) : KComponent() {
override fun ComponentScope.render(): Component {
return LazyList {
// Add SingleComponentSection components as children:
child(Text(title))
child(MyComponent())
}
}
}
Next, consider a list rendered with a DataDiffSection
.
Replace the DataDiffSection
sβ
A DataDiffSection
is used to render a collection of homogeneous data. This type of Section can be defined as a child in a GroupSectionSpec
, or directly passed to a RecyclerCollectionComponent
. It requires the following event handlers:
renderEventHandler
- determines how to render a list item.onCheckIsSameItemEventHandler
- determines if items have the same identity.onCheckIsSameContentEventHandler
- determines if items have updated content.
Consider the following GroupSectionSpec
that contains a DataDiffSection
to render a list of the following type:
class Model(val id: String, val field1: String, val field2: String)
@GroupSectionSpec
object ListSectionSpec {
@OnCreateChildren
fun onCreateChildren(c: SectionContext, @Prop models: List<Model>): Children =
Children.create()
.child(
DataDiffSection.create<Model>(c)
.data(models)
.renderEventHandler(ListSection.onRenderItem(c))
.onCheckIsSameItemEventHandler(ListSection.onCheckIsSameItem(c))
.onCheckIsSameContentEventHandler(ListSection.onCheckIsSameContent(c)))
.build()
@OnEvent(RenderEvent::class)
fun onRenderItem(c: SectionContext, @FromEvent model: Model): RenderInfo =
ComponentRenderInfo.create()
.component(Text.create(c).text("${model.field1} ${model.field2}").build())
.build()
@OnEvent(OnCheckIsSameItemEvent::class)
fun onCheckIsSameItem(
c: SectionContext,
@FromEvent previousItem: Model,
@FromEvent nextItem: Model
): Boolean =
previousItem.id == nextItem.id
@OnEvent(OnCheckIsSameContentEvent::class)
fun onCheckIsSameContent(
sectionContext: SectionContext,
@FromEvent previousItem: Model,
@FromEvent nextItem: Model
): Boolean = previousItem.field1 == nextItem.field1 && previousItem.field2 == nextItem.field2
}
A component for each list item should be added to the Lazy Collection as a child. It's the same component that is passed to the component
parameter during ComponentRenderInfo
creation in RenderEvent
handler method.
The child id
parameter should be unique and consistent across re-renders and will likely be the value that is compared in onCheckIsSameItem
. See the guidelines for child ids.
It is not necessary to migrate the checks from onCheckIsSameContent
as the Lazy Collection will automatically compare the child component props. However, additional steps may be required to eliminate unnecessary layouts if child takes props that do not provide an equals()
. For more information, see the Avoiding Unnecessary Layouts page.
class ListMigration(private val models: List<Model>) : KComponent() {
override fun ComponentScope.render(): Component {
return LazyList {
// Add DataDiffSection contents as children with ids
children(items = models, id = { it.id }) { model ->
Text("${model.field1} ${model.field2}")
}
}
}
}
Migrate Event Handlersβ
Event handlers that are defined as @OnEvent
functions (such as visibility or click handling) can be passed as lambdas to the appropriate Style
functions. Note that using lambdas as props could cause unnecessary updates, so they should be wrapped in a useCallback
hook.
@GroupSectionSpec
object EventHandlerSectionSpec {
@OnCreateChildren
fun onCreateChildren(c: SectionContext): Children =
Children.create()
.child(
SingleComponentSection.create(c)
.component(
Text.create(c)
.text("Say Hello")
.clickHandler(EventHandlerSection.onClick(c))
.build()))
.build()
@OnEvent(ClickEvent::class)
fun onClick(c: SectionContext) {
println("Hello World!")
}
}
Migrating the above Section to a Lazy Collection's yields:
class EventHandlerMigration : KComponent() {
override fun ComponentScope.render(): Component {
val onClick = useCallback { _: ClickEvent -> println("Hello World!") }
return LazyList {
// Using Style.onClick(..)
child(Text("Say Hello", style = Style.onClick(action = onClick)))
// Or using the Spec api with eventHandler(..)
child(Text.create(context).text("Say Hello").clickHandler(eventHandler(onClick)).build())
}
}
}
Migrate Section Stateβ
State managed by a Section
should be moved into the component that manages the Lazy Collection. It may be beneficial to consider defining the Lazy Collection inside a dedicated KComponent
so that it can maintain its own state.
@GroupSectionSpec
object StateSectionSpec {
@OnCreateInitialState
fun createInitialState(c: SectionContext, counter: StateValue<Int>) {
counter.set(0)
}
@OnCreateChildren
fun onCreateChildren(c: SectionContext, @State counter: Int): Children =
Children.create()
.child(
SingleComponentSection.create(c)
.component(
Text.create(c)
.text("Increment: $counter")
.clickHandler(StateSection.onClick(c))
.build()))
.build()
@OnEvent(ClickEvent::class)
fun onClick(c: SectionContext) {
StateSection.incrementCounter(c)
}
@OnUpdateState
fun incrementCounter(counter: StateValue<Int>) {
counter.set((counter.get() ?: 0) + 1)
}
}
Migrating the above Section to a Lazy Collection yields the following:
class StateMigration : KComponent() {
override fun ComponentScope.render(): Component {
val counter = useState { 0 }
val onClick = useCallback { _: ClickEvent -> counter.update { it + 1 } }
return LazyList {
child(Text("Increment ${counter.value}", style = Style.onClick(action = onClick)))
}
}
}