Documentation

$onMount / $onDispose

React to a screen or component appearing and leaving.

$onMount

Runs each time the screen or component appears. It is a coroutine — it calls a suspend function or $fetch directly, no wrapper needed:

var data: List<Show>? = $state(null)

$onMount {
  data = $fetch("https://api.example.com/shows")
}

@if (data != null) {
  <LazyColumn>
    @for (show in data) {
      <Text>{show.name}</Text>
    }
  </LazyColumn>
} else {
  <CircularProgressIndicator />
}

$onDispose

Runs each time the screen or component leaves:

var connection: WebSocket? = $state(null)

$onMount {
  connection = WebSocket("wss://api.example.com")
  connection?.connect()
}

$onDispose {
  connection?.close()
}

See it happen

Add $onDispose and watch the disposal code appear beside the mount code.

Connection.wh
var connected = $state(false)

$onMount {
  connected = true
}

<Text>{connected}</Text>
Kotlin waiting
Scroll into view to compile.

$onMount runs when the screen or component appears. It is a coroutine, so it can call a suspend function or $fetch directly.

Navigating away and back

Navigating away and back runs both hooks again, so a screen that loads in $onMount reloads when you return to it. On a screen, a rotation is not a visit and runs neither. A component is composed afresh by a rotation, so its hooks run again.

A screen's $state is not reset between visits — $onMount finds it as the last visit left it. Seed state in the declaration, not in the hook, if it should happen once:

var items = $state(listOf<Item>())   // seeded once, here

$onMount {
  if (items.isEmpty()) items = loadItems()
}

Multiple hooks

A screen or component can have more than one of each:

var data: String? = $state(null)

$onMount {
  println("mounted")
}

$onMount {
  data = "loaded"
}

$onDispose {
  println("disposed")
}

Common Patterns

Data Loading

var posts: List<Post> = $state(listOf())
var error: String? = $state(null)

$onMount {
  try {
    posts = $fetch("https://api.example.com/posts")
  } catch (e: Exception) {
    error = e.message
  }
}

Polling

var status = $state("unknown")

$onMount {
  while (true) {
    status = $fetch("https://api.example.com/status")
    delay(5000)
  }
}

$onDispose {
  // the loop's coroutine is cancelled with it
}

Event Listeners

var batteryLevel = $state(100)
var receiver: BatteryReceiver? = null

$onMount {
  receiver = BatteryReceiver { level -> batteryLevel = level }
  registerReceiver(receiver)
}

$onDispose {
  unregisterReceiver(receiver)
}

Reacting to a change

$onMount runs once per visit. For code that should re-run whenever a value changes, use $effect instead:

var searchQuery = $state("")
var results = $state(listOf<Result>())

$effect {
  val q = searchQuery
  results = if (q.isEmpty()) listOf() else $fetch("https://api.example.com/search?q=$q")
}

<TextField bind:value={searchQuery} />

The block's first line reads searchQuery, so the effect reruns whenever it changes — there is no dependency list to declare or keep in sync.

For data loading that runs before a screen renders, use a screen loader instead of $onMount. A loader gets a loading state and an +error.wh for free.

See Also