Kotlin / Compose
Write native Android components and functions in Kotlin.
Kotlin gives you full Android SDK access. Use Jetpack Compose for declarative UI or Android Views for classic layouts.
Setup
Run the setup command to download the Kotlin compiler toolchain:
npx nativ setup-kotlinThis downloads the following into your Gradle cache (or a local .nativ/kotlin-cache/ fallback):
- kotlin-compiler-embeddable + kotlin-stdlib — the compiler used by Metro's Kotlin daemon
- android.jar — Android API stubs for type resolution (downloaded automatically if Android SDK is not installed)
- d8 (R8) —
.classto.dexconverter for loading compiled Kotlin on device
No Android SDK or Android Studio installation is required for Kotlin hot-reload.
Compose setup
If you're using Jetpack Compose (@Composable functions), run the additional setup command:
npx nativ setup-composeThis builds three JARs needed for standalone Compose compilation outside of Gradle:
- compose-pretransform — provides
rememberinline body stubs for the Compose compiler plugin - compose-wrappers — non-inline wrappers for
Box,Column,Row, andSpacer - compose-host —
ComposeView.setContentwrapper for hot-reload
This command includes everything setup-kotlin does, so you only need to run one or the other.
Compose components
Mark a @Composable function with // @nativ_component:
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
// @nativ_component
@Composable
fun ComposeCard(title: String) {
var count by remember { mutableIntStateOf(0) }
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFC6A1B9A), RoundedCornerShape(12.dp))
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = if (title.isNotEmpty()) title else "Compose Card",
color = Color.White,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Taps: $count",
color = Color.White.copy(alpha = 0.7f),
fontSize = 14.sp
)
}
}
}import ComposeCard from './ComposeCard';
<ComposeCard
title="Hello from Compose!"
style={{ width: '100%', height: 100, borderRadius: 12, overflow: 'hidden' }}
/>Android View components
For classic Android Views, use // @nativ_component on a plain function:
import android.graphics.Color
import android.graphics.Typeface
import android.view.Gravity
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.TextView
// @nativ_component
fun KotlinCounter(parent: ViewGroup, props: Map<String, Any?>) {
val context = parent.context
val title = props["title"] as? String ?: "Kotlin Counter"
val bgColor = when (props["color"] as? String) {
"blue" -> Color.parseColor("#1565C0")
"green" -> Color.parseColor("#2E7D32")
"purple" -> Color.parseColor("#6A1B9A")
else -> Color.parseColor("#E91E63")
}
val root = LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
setBackgroundColor(bgColor)
setPadding(48, 32, 48, 32)
}
root.addView(TextView(context).apply {
text = title
setTextColor(Color.WHITE)
textSize = 16f
typeface = Typeface.DEFAULT_BOLD
})
parent.addView(root, FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
))
}import KotlinCounter from './KotlinCounter';
<KotlinCounter
title="Hello from Kotlin!"
color="purple"
style={{ width: '100%', height: 80, borderRadius: 12, overflow: 'hidden' }}
/>Functions
Use // @nativ_export(sync) to export plain Kotlin functions:
// @nativ_export(sync)
fun factorial(n: Int): Long {
var result = 1L
for (i in 2..n) result *= i
return result
}
// @nativ_export(sync)
fun isPalindrome(text: String): Boolean {
val clean = text.lowercase().filter { it.isLetterOrDigit() }
return clean == clean.reversed()
}
// @nativ_export(sync)
fun greetKotlin(name: String): String {
return "Hey $name, from Kotlin!"
}import { factorial, isPalindrome, greetKotlin } from './kotlin_utils';
<Text>factorial(10) = {String(factorial?.(10))}</Text>
<Text>isPalindrome("racecar") = {String(isPalindrome?.("racecar"))}</Text>
<Text>{greetKotlin?.("Nativ")}</Text>Async functions
Use // @nativ_export(async) for functions that do heavy work or I/O:
// @nativ_export(async)
fun fetchUser(id: Int): String {
// Runs on a background thread — won't block the UI
Thread.sleep(1000)
return "{\"id\": $id, \"name\": \"User $id\"}"
}import { fetchUser } from './async_utils';
const user = await fetchUser(42);Hot-reload
Kotlin hot-reload compiles to .dex files loaded via DexClassLoader:
- Edit your
.ktfile - Save
- Metro compiles it with
kotlinc+ Compose compiler plugin - The device loads the new
.dex - The component re-renders
State in remember blocks is preserved across reloads.