Skip to main content

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 and Column).
  • 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:

  1. Replacing the component that renders the section tree with a Lazy Collection component β€” see Replace the RecyclerCollectionComponent.
  2. Adding each component from the section tree as a child of the Lazy Collection β€” see Migrate the GroupSectionSpec and Migrate the DataDiffSection.
  3. 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
}
info

Lazy Collections can only be used inside KComponents. 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 SingleComponentSections, DataDiffSections and other custom GroupSections 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 SingleComponentSections​

The easiest part of Sections children migration is dealing with SingleComponentSections. The following GroupSectionSpec renders two components as SingleComponentSections:

@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 DataDiffSections​

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)))
}
}
}