Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Getting started with the Compose HTML library

In this tutorial, we will create a simple web UI application using the Compose HTML Library.

Prerequisites

You need to have the following software installed before you begin:

  • JDK 11 or later
  • IntelliJ IDEA Community Edition or Ultimate Edition 2020.2 or later (you can use other editors, but for this tutorial we assume you are using IntelliJ IDEA)

Creating a new project

You can create the project by downloading the template here.

If you want to create the project manually, perform the following steps:

1. Create a Kotlin Multiplatform project:

  • Select Gradle on the left menu
  • Tick Kotlin DSL build script
  • Tick Kotlin/Multiplatform

2. Update settings.gradle.kts:

pluginManagement {
    repositories {
        gradlePluginPortal()
        maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
    }
}

3. Update build.gradle.kts:

// Add compose gradle plugin
plugins {
    kotlin("multiplatform") version "2.1.20"
    id("org.jetbrains.compose") version "1.8.2"
}

// Add maven repositories
repositories {
    mavenCentral()
    maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
    google()
}

// Enable JS(IR) target and add dependencies
kotlin {
    js(IR) {
        browser()
        binaries.executable()
    }
    sourceSets {
        val jsMain by getting {
            dependencies {
                implementation(compose.html.core)
                implementation(compose.runtime)
            }
        }
    }
}

5. Add the following directories to the project:

  • src/jsMain/kotlin
  • src/jsMain/resources

6. Add the index.html file to the resources:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Sample</title>
</head>
<body>
  <div id="root"></div>
  <script src="REPLACE_WITH_YOUR_MODULE_NAME.js"></script>
</body>
</html>

7. Add the Main.kt file to the kotlin:

import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.attributes.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.renderComposable

fun main() {
    var count: Int by mutableStateOf(0)

    renderComposable(rootElementId = "root") {
        Div({ style { padding(25.px) } }) {
            Button(attrs = {
                onClick { count -= 1 }
            }) {
                Text("-")
            }

            Span({ style { padding(15.px) } }) {
                Text("$count")
            }

            Button(attrs = {
                onClick { count += 1 }
            }) {
                Text("+")
            }
        }
    }
}

Running the project

Use the command line to run:

./gradlew jsBrowserRun

Instead of manually compiling and executing a Kotlin/JS project every time you want to see the changes you made, you can use the continuous compilation mode:

./gradlew jsBrowserRun --continuous

Or run it from the IDE:

The browser will open localhost:8080:

Common issues when running the project

[webpack-cli] Unable to load '@webpack-cli/serve' command

https://youtrack.jetbrains.com/issue/KT-49124

[webpack-cli] Unable to load '@webpack-cli/serve' command
[webpack-cli] TypeError: options.forEach is not a function
...

There is a temporary workaround:

In build.gradle.kts:

// a temporary workaround for a bug in jsRun invocation - see https://youtrack.jetbrains.com/issue/KT-48273
afterEvaluate {
    rootProject.extensions.configure<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension> {
        versions.webpackDevServer.version = "4.0.0"
        versions.webpackCli.version = "4.9.0"
    }
}

Building the UI with the Compose HTML library

In this tutorial we will look at several examples that use the Composable HTML/CSS DSL to describe the user interface for your web application.

Entry point

The Compose HTML library needs an HTML node that will be a root of its composition. Inside this root node, Compose then manages its own DOM tree.

renderComposable(rootElementId = "root") {
    // content goes here
}

HTML tags

Let's have a look at the Composable for a Div tag (most other tags have the same signature):

Div(
    attrs = {
        // specify attributes here
        style {
            // specify inline style here
        }
    }
) {
    // div content goes here
}

For convenience, some tags like Input, A, Form, or Img allow you to specify some extra parameters in the signature that are specific to the respective HTML tag. For example, let’s look at the Input tag:

Input(
    type = InputType.Text, // All InputTypes supported
    attrs = {}
)

We can use the type parameter which is provided for our convenience, or can use the attrs block to specify the input type:

Input(attrs = { type(InputType.Text) })

Text

The Text allows you to add text content to an HTML tag. Besides the text content it represents, it does not have any parameters:

Text("Arbitrary text")

If you want to apply styles to text, it needs to be wrapped in a container with a style applied, like a Span or P:

Span(
    attrs = { style { color(Color.red) } } // inline style
) {
    Text("Red text")
}

This corresponds to the following HTML code:

<span style="color: red;">Red text</span>

Attributes

The attrs parameter (which we’ve already seen in some of the previous examples) allows us to specify element's attributes and properties.

The most flexible way to define attributes is by using the attr function, which allows you to specify the attribute name and its value.

Div(
    attrs = {
        attr(attr = "custom_attr", value = "its_value")
    }
) { /* content */ }

However, with this approach, Compose is not able to validate that the attribute exists on the HTML element, or is valid. This is why we also provide a set of helper functions for common attributes.

Common attributes

Here are some examples of common attributes that are available for most Composables representing HTML tags:

attrs = {
    id("elementId")
    classes("cl1", "cl2")
    hidden(false)
    title("title")
    draggable(Draggable.Auto)
    dir(DirType.Auto)
    lang("en")
    contentEditable(true)
}

Element specific attributes

Depending on the element you are working with, you may also have access to some specific attributes – attributes that are only meaningful for this particular tag. For example, the A tag provides some specific attributes, that are specific to hyperlinks:

A(
    attrs = {
        href("https://localhost:8080/page2")
        target(ATarget.Blank)
        rel(ARel.Next)
        hreflang("en")
        download("https://...")
    }
) {}

Some other elements that provide specific attributes include:

  • Button
  • Form
  • Input
  • Option
  • Select
  • OptGroup
  • TextArea
  • Img

To discover all attributes that are available in your current scope, you can use your IDE’s autocomplete feature. As we evolve these APIs, we also plan to add detailed documentation for them.

Events

You can declare event listeners in the attrs block:

Button(
    attrs = { 
        onClick { println("Button clicked") }
    }
) { Text("Button") }

There are more examples about events handling here - Events Handling

Style

There are ways to set the style for a component:

  • Using inline styles
  • Using stylesheets

You can declare inline styles via the style block of a component:

Div(
    attrs = {
        style {
            display(DisplayStyle.Flex)
            padding(20.px)
            
            // custom property
            property("font-family", "Arial, Helvetica, sans-serif")
        }
    }
) { /* content goes here */ }

You can find a more detailed overview of the style DSL, as well as additional examples here - Style DSL

Runnable example

import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.attributes.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.renderComposable

fun main() {
    renderComposable(rootElementId = "root") {
        Div(
            attrs = {
                // specify attributes here
                style {
                    // specify inline style here
                }
            }
        ) {
            Text("A text in <div>")
        }

        Input(
            type = InputType.Text, // All InputTypes supported
            attrs = {}
        )

        Text("Arbitrary text")

        Span({
            style { color(Color.red) } // inline style
        }) {
            Text("Red text")
        }

        Div(
            attrs = {
                id("elementId")
                classes("cl1", "cl2")
                hidden()
                title("title")
                draggable(Draggable.Auto)
                dir(DirType.Auto)
                lang("en")
                contentEditable(true)

                // custom attr
                attr(attr = "custom_attr", value = "its_value")
            }
        ) { /* content */ }

        A(
            attrs = {
                href("https://localhost:8080/page2")
                target(ATarget.Blank)
                hreflang("en")
                download("https://...")
            }
        ) { Text("Link") }

        Button(
            attrs = {
                onClick { println("Button clicked") }
            }
        ) { Text("Button") }

        Div({
            style {
                display(DisplayStyle.Flex)
                padding(20.px)

                // custom property
                property("font-family", "Arial, Helvetica, sans-serif")
            }
        }) { Text("Text in Div with inline style") }
    }
}

Controlled and Uncontrolled inputs

Input components have two modes: Controlled and Uncontrolled.

Controlled vs Uncontrolled behaviour

Let's create two inputs using different modes and compare them:


// Uncontrolled
Input(type = InputType.Text) {
    defaultValue("Initial Value") // optional
    onInput { event -> println(event.value) }
}

// Controlled
Input(type = InputType.Text) {
    value("Some value") // calling value(...) is necessary to make input "Controlled"
    onInput { event -> println(event.value) }
}

If you try running these snippets you'll see following behaviour:

  • Uncontrolled text input will show "Initial value". Typing will make corresponding changes to the input's state.
  • Controlled text input will show "Some value". But typing will not cause any changes.
  • Both inputs will receive an event in onInput { } handler

In the example above, we set hardcoded value - value("Some value"). Therefore, typing does nothing. Under the hood, controlled input "restores" its state according to last known value.

Using MutableState with Controlled Input

To make Controlled Input more useful we can use MutableState<*> to keep input's value:

val inputState = remember { mutableStateOf("Some Text") }

Input(type = InputType.Text) {
    value(inputState.value)
    onInput { event -> println(event.value) }
}

We can see that, inputState never mutates. If we run such an example as is, we'll see the same behaviour as when value(...) was hardcoded. But if we had some code that updates inputState, then Input would recompose and new value would be shown.

In most cases, inputState needs to be changed in onInput event handler:

val inputState = remember { mutableStateOf("Some Text") }

Input(type = InputType.Text) {
    value(inputState.value)
    onInput { event -> inputState.value = event.value }
}

Conclusion

Uncontrolled input changes its content independently while Controlled input's content can be changed only by external state (such as MutableState).

In most cases Controlled input is the default choice.

Convenient controlled inputs

Here is a list of Composable functions which represent controlled inputs of different types:

  • CheckboxInput
  • DateInput
  • DateTimeLocalInput
  • EmailInput
  • FileInput
  • MonthInput
  • NumberInput
  • PasswordInput
  • RadioInput
  • RangeInput
  • SearchInput
  • TelInput
  • TextInput
  • TimeInput
  • UrlInput
  • WeekInput

Example:

val inputState = remember { mutableStateOf("Some Text") }

TextInput(value = inputState.value) {
    onInput { event -> inputState.value = event.value }
}

Style DSL

In this tutorial we have a look at how to style the components using the Style DSL. It’s a typesafe DSL for style sheets, which you can use to express CSS rules in your Kotlin code, and even modify styles based on the state of your Compose application.

Inline Style

You can declare inline styles via the style block of a component

Div({
    style {
        display(DisplayStyle.Flex)
        padding(20.px)
        
        // custom property (or not supported out of a box)
        property("font-family", "Arial, Helvetica, sans-serif")
    }
}) { /* content goes here */ }

In HTML, it will look like this:

<div style="display: flex; padding: 20px; font-family: Arial, Helvetica, sans-serif;"></div>

Stylesheet

An alternative way is to define a Stylesheet that contains rules:

object AppStylesheet : StyleSheet() {
    val container by style { // container is a class
        display(DisplayStyle.Flex)
        padding(20.px)

        // custom property (or not supported out of a box)
        property("font-family", "Arial, Helvetica, sans-serif")
    }
}

// Stylesheet needs to be mounted:
renderComposable("root") {
    Style(AppStylesheet)
    
    Container {
        Text("Content")
    }
}

@Composable
fun Container(content: @Composable () -> Unit) {
    Div(
        attrs = { classes(AppStylesheet.container) }
    ) {
        content()
    }
}

In HTML, it will look like this:

<style></style>
<div class="AppStylesheet-container">Content</div>

Selectors examples

The Style DSL also provides a way to combine and unify selectors:

object AppStylesheet : StyleSheet() {
    
    init {
        // `universal` can be used instead of "*": `universal style {}`
        "*" style { 
            fontSize(15.px)
            padding(0.px)
        }
        
        // raw selector
        "h1, h2, h3, h4, h5, h6" style {
            property("font-family", "Arial, Helvetica, sans-serif")
            
        }

        // combined selector
        type("A") + attr( // selects all tags <a> with href containing 'jetbrains'
            name = "href",
            value = "jetbrains",
            operator = CSSSelector.Attribute.Operator.Equals
        ) style {
            fontSize(25.px)
        }
    }
    
    // A convenient way to create a class selector
    // AppStylesheet.container can be used as a class in component attrs
    val container by style {
        color(Color.red)
        
        // hover selector for a class
        self + hover() style { // self is a selector for `container`
            color(Color.green)
        }
    }
}

Media query example

To specify media queries, you can use the media function, which takes the related query, and a block of styles:

object AppStylesheet : StyleSheet() {
    val container by style {
        padding(48.px)

        media(mediaMaxWidth(640.px)) {
            self style {
                padding(12.px)
            }
        }
    }
}

CSS Variables

The style DSL also provides support for CSS variables.

object MyVariables {
    // declare a variable
    val contentBackgroundColor by variable<CSSColorValue>()
}

object MyStyleSheet: StyleSheet() {
    
    val container by style {
        //set variable's value for the `container` scope
        MyVariables.contentBackgroundColor(Color("blue"))
    }
    
    val content by style {
        // get the value
        backgroundColor(MyVariables.contentBackgroundColor.value())
    }

    val contentWithDefaultBgColor by style {
        // default value can be provided as well
        // default value is used when the value is not previously set
        backgroundColor(MyVariables.contentBackgroundColor.value(Color("#333")))
    }
}

Runnable example

import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.renderComposable

object MyVariables {
    // declare a variable
    val contentBackgroundColor by variable<CSSColorValue>()
}

object MyStyleSheet: StyleSheet() {

    val container by style {
        //set variable's value for the `container` scope
        MyVariables.contentBackgroundColor(Color("blue"))
    }

    val content by style {
        // get the value
        backgroundColor(MyVariables.contentBackgroundColor.value())
    }

    val contentWithDefaultBgColor by style {
        // default value can be provided as well
        // default value is used when the value is not previously set
        backgroundColor(MyVariables.contentBackgroundColor.value(Color("#333")))
    }
}

object AppStylesheet : StyleSheet() {
    val container by style { // container is a class
        display(DisplayStyle.Flex)
        padding(20.px)

        // custom property (or not supported out of a box)
        property("font-family", "Arial, Helvetica, sans-serif")
    }
}

@Composable
fun Container(content: @Composable () -> Unit) {
    Div(
        attrs = { classes(AppStylesheet.container) }
    ) {
        content()
    }
}

fun main() {
    renderComposable(rootElementId = "root") {
        Div({
            style {
                display(DisplayStyle.Flex)
                padding(20.px)

                // custom property (or not supported out of a box)
                property("font-family", "Arial, Helvetica, sans-serif")
            }
        }) { /* content goes here */ }


        Style(AppStylesheet)

        Container {
            Text("Content")
        }
    }
}

Using Effects

The Compose HTML library introduces a few dom-specific effects on top of existing effects from Compose.

ref in AttrsBuilder

Under the hood, ref uses DisposableEffect

ref can be used to retrieve a reference to a html element. The lambda that ref takes in is not Composable. It will be called only once when an element added into a composition. Likewise, the lambda passed in onDispose will be called only once when an element leaves the composition.

Div(attrs = {
    ref { htmlDivElement ->
       // htmlDivElement is a reference to the HTMLDivElement
       onDispose {
          // add clean up code here
       }
    }
}) {
    // Content()
}

Only one ref can be used per element. Calling it more than once will dismiss earlier calls.

For example, ref can be used to add and remove some event listeners not provided out of the box.

DisposableRefEffect (deprecated)

Deprecated:

Consider using DisposableEffect. Its scope provides scopeElement - a reference to the underlying HTML element:

DisposableEffect(key) {
    scopeElement.innerText = key
    onDispose { scopeElement.innerText = "" }
}

Under the hood, DisposableRefEffect uses DisposableEffect

DisposableRefEffect is similar to ref, since it also provides a reference to an element. At the same time it has few differences.

  • DisposableRefEffect can be added only within a content lambda of an element, while ref can be used only in attrs scope.
  • Unlike ref, DisposableRefEffect can be used as many times as needed and every effect will be unique.
  • DisposableRefEffect can be used with a key and without it. When it's used with a key: Any, the effect will be disposed and reset when key value changes. When it's used without a key, then it behaves like ref - the effect gets called only once when an element enters the composition, and it's disposed only when the element leaves the composition.
Div {
    // without a key
    DisposableRefEffect { htmlDivElement ->
        // htmlDivElement is a reference to the HTMLDivElement
        onDispose {
            // add clean up code here
        }
    }
}


var state by remember { mutableStateOf(1) }

Div {
    // with a key. 
    // The effect will be called for every new state's value
    DisposableRefEffect(state) { htmlDivElement ->
        // htmlDivElement is a reference to the HTMLDivElement
        onDispose {
            // add clean up code here
        }
    }
}

DomSideEffect (deprecated)

Deprecated:

Consider using SideEffect. If a reference to an underlying HTML element is needed, consider using DisposableEffect and scopeElement within its scope.


Under the hood, DomSideEffect uses SideEffect

DomSideEffect as well as DisposableRefEffect can be used with a key and without it.

Unlike DisposableRefEffect, DomSideEffect without a key is invoked on every successful recomposition. With a key, it will be invoked only when the key value changes.

Same as SideEffect, DomSideEffect can be helpful when there is a need to update objects not managed by Compose. In case of web, it often involves updating HTML nodes, therefore DomSideEffect provides a reference to an element in the lambda.

Code Sample using effects

The code below showcases how it's possible to use non-composable components in Compose by applying DomSideEffect and DisposableRefEffect.

import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.Composable
import kotlinx.browser.document
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.renderComposable
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLParagraphElement


// Here we pretend that `RedBoldTextNotComposableRenderer`
// wraps a UI logic provided by 3rd party library that doesn't use Compose

object RedBoldTextNotComposableRenderer {
    fun unmountFrom(root: HTMLElement) {
        root.removeChild(root.firstChild!!)
    }

    fun mountIn(root: HTMLElement) {
        val pElement = document.createElement("p") as HTMLParagraphElement
        pElement.setAttribute("style", "color: red; font-weight: bold;")
        root.appendChild(pElement)
    }

    fun renderIn(root: HTMLElement, text: String) {
        (root.firstChild as HTMLParagraphElement).innerText = text
    }
}

// Here we define a Composable wrapper for the above code. Here we use DomSideEffect and DisposableRefEffect. 
@Composable // @param `show: Boolean` was left here intentionally for the sake of the example
fun ComposableWrapperForRedBoldTextFrom3rdPartyLib(state: Int, show: Boolean) {
    Div(attrs = {
        style {
            backgroundColor(Color.lightgray)
            width(100.px)
            minHeight(40.px)
            padding(30.px)
        }
    }) {
        if (!show) {
            Text("No content rendered by the 3rd party library")
        }

        Div {
            if (show) {
                // Update the content rendered by "non-compose library" according to the `state`
                DomSideEffect(state) { div ->
                    RedBoldTextNotComposableRenderer.renderIn(div, "Value = $state")
                }
            }

            DisposableRefEffect(show) { div ->
                if (show) {
                    // Let "non-compose library" control the part of the page.
                    // The content of this div is independent of Compose. 
                    // It will be managed by RedBoldTextNotComposableRenderer 
                    RedBoldTextNotComposableRenderer.mountIn(div)
                }
                onDispose {
                    if (show) {
                        // Clean up the html created/managed by "non-compose library"
                        RedBoldTextNotComposableRenderer.unmountFrom(div)
                    }
                }
            }
        }
    }
}

fun main() {
    var state by mutableStateOf(0)
    var showUncontrolledElements by mutableStateOf(false)

    renderComposable(rootElementId = "root") {

        ComposableWrapperForRedBoldTextFrom3rdPartyLib(state = state, show = showUncontrolledElements)

        Div {
            Label(forId = "checkbox") {
                Text("Show/hide text rendered by 3rd party library")
            }

            CheckboxInput(checked = showUncontrolledElements) {
                id("checkbox")
                onInput {
                    showUncontrolledElements = it.value
                }
            }
        }

        Button(attrs = {
            onClick { state += 1 }
        }) {
            Text("Incr. count ($state)")
        }
    }
}

Events handling

You can add event listeners in the attrs block:

onClick

Button(
    attrs = {
        onClick { event -> 
            // event is of `SyntheticMouseEvent` type    
            println("button clicked at ${event.movementX}, ${event.movementY}")
            
            val nativeEvent = event.nativeEvent // [MouseEvent](https://developer.mozilla.org/en/docs/Web/API/MouseEvent)
        }
    }
) {
    Text("Button")
}

onInput

val text = remember { mutableStateOf("") }

TextArea(
    value = text.value,
    attrs = {
        onInput {
            text.value = it.value
        }
    }
)

Other event handlers

For events that don't have their own configuration functions in the attrs block, you can use addEventListener with the name of the event, options, and an pass an eventListener which receives a SyntheticEvent. In this example, we're defining the behavior of a Form element when it triggers the submit event:

Form(attrs = {
    this.addEventListener("submit") {
        console.log("Hello, Submit!")
        it.preventDefault()
    }
})

There are more event listeners supported out of the box. You can find all supported event listeners in the source code.

Runnable example

import androidx.compose.runtime.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.renderComposable

fun main() {
    renderComposable(rootElementId = "root") {
        Button(
            attrs = {
                onClick { event ->
                    println("button clicked at ${event.movementX}, ${event.movementY}")
                }
            }
        ) {
            Text("Button")
        }

        val text = remember { mutableStateOf("") }

        TextArea(
            value = text.value,
            attrs = {
                onInput {
                    text.value = it.value
                }
            }
        )

        Span {
            Text("Typed text = ${text.value}")
        }
    }
}

Using test-utils for unit testing

Dependencies

It's necessary to add compose.html.testUtils to jsTest dependencies:

sourceSets {
    val jsMain by getting {
        dependencies {
            implementation(compose.html.core)
            implementation(compose.runtime)
            //....
        }
    }
    val jsTest by getting {
        implementation(kotlin("test-js"))
        implementation(compose.html.testUtils)
        //...
    }
}

Example

// This is a function that we want to test
@Composable
fun TestButton(text: String, onButtonClick: () -> Unit) {
    Button(attrs = {
        onClick { onButtonClick() }
    }) {
        Text(text)
    }
}

Let's add a test to ensure that button has correct text, and it's onClick works properly.

import org.jetbrains.compose.web.testutils.ComposeWebExperimentalTestsApi
import org.jetbrains.compose.web.testutils.runTest
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import org.w3c.dom.HTMLButtonElement
import kotlin.test.Test
import kotlin.test.assertEquals

@OptIn(ComposeWebExperimentalTestsApi::class)
class TestsForButton {

    @Test
    fun testButton() = runTest {
        var counter by mutableStateOf(1)
        
        composition {
            TestButton(text = "$counter") {
                counter++
            }
        }

        assertEquals("<button>1</button>", root.innerHTML)

        (root.firstChild!! as HTMLButtonElement).click()
        waitForRecompositionComplete()
        assertEquals("<button>2</button>", root.innerHTML)

        counter = 10
        waitForRecompositionComplete()
        assertEquals("<button>10</button>", root.innerHTML)
    }
}

Let's break it down:

runTest { ... }

Provides the TestScope with useful functions to configure the test.

composition { ... }

Takes a @Composable block with a content that we want to test. It will automatically build and mount DOM into root element.

root

It's not supposed to be used for elements manipulation. It's mostly useful to make assertions on the html content (e.g. root.innerHtml)

nextChild() and currentChild()

Under the hood nextChild() iterates over root children, providing convenient access to them.

currentChild() doesn't move the iterator and returns the same element every time until nextChild() called.

waitForRecompositionComplete()

It suspends until recomposition completes. It's useful when state changes, and we want to test that content updates as well. waitForRecompositionComplete needs to be called after state change and before assertions.

waitForChanges(id: String)

It suspends until any change occur in the element with id. It's also useful to ensure that state changes make corresponding updates to the content.