Snappier is an open-source Server-Driven UI multiplatform library based on Compose Multiplatform UI Framework with the goal of being open and customizable.
Currently supported platforms:
- Android (5.0+)
- iOS
- Web (JavaScript)
- Desktop (Java)
Create a Compose Multiplatform project (help)
You can use
In the app's build.gradle.kts
file (outside the src
dir):
Add kotlinx-serialization-json
to the
project (help):
plugins {
//...
alias(libs.plugins.kotlinxSerialization)
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.kotlinx.serialization.json)
}
}
}
For Android, set target Java version to 11, enable API desugaring support, and compose build feature
kotlin {
androidTarget {
// ...
}
jvm("desktop")
js(IR) {
// ...
}
// iOS / Apple...
}
compose.experimental {
web.application {}
}
kotlin {
sourceSets {
val desktopMain by getting
commonMain.dependencies {
implementation("br.com.androidvip.snappier:lib:1.0.0")
// [Compose libs...]
implementation(libs.kotlinx.serialization.json)
implementation(libs.bundles.ktor.common)
}
}
}
@Composable
fun App() {
val snappier = Snappier {
// Optional Composable drawn while the lib is making requests
onLoadingErrorComposable { progress ->
Text("Loading: $progress%")
}
// Optional Composable drawn when the request fails
onErrorComposable { error ->
Text("Error: $error")
}
// If using ktor, set a default method
// Individual requests can still specify their client config (including method)
setHttpMethod(HttpMethod.Get)
// Customize your whole HttpClient
withHttpClient {
// this: HttpClientConfig<*>
}
}.apply {
// Add an observer for component communication
addObserver { event ->
println("Received event: $event")
}
// Register your components
register(listOf(ComponentA(), ComponentB()))
}
MaterialTheme {
// This composable function will fetch and draw a Component from a sample API
snappier.draw("http://localhost:8081/api")
// Or you can make your own requests and map the results to the `Component` class
// and have snappier render it for you
val myOwnComponentFromMyOwnHttpRequest = Component()
snappier.draw(myOwnComponentFromMyOwnHttpRequestc)
val manyComponents = listOf<Component>()
snappier.draw(manyComponents) // Draws as `LazyList`
}
}
For Android, if using Android Studio, set up an Android Emulator, select the "app" run configuration and press the run button.
For Desktop, create a main.kt
file in the myAppName/src/desktopMain/kotlin
directory:
fun main() = application {
Window(onCloseRequest = ::exitApplication, title = "My App Name") {
// App()
}
}
Run the project using:
gradlew desktopRun -DmainClass=MainKt --quiet
For Web/Javascript, create a index.html
file in the myAppName/src/jsMain/resources
directory:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Multiplatform App</title>
<script src="skiko.js"></script>
</head>
<body>
<div>
<canvas id="ComposeTarget" width="800" height="600"></canvas>
</div>
<script src="myAppName.js"></script>
</body>
</html>
Create a main.kt
file in the myAppName/src/jsMain/kotlin
directory:
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
onWasmReady {
CanvasBasedWindow("Multiplatform App") {
App()
}
}
}
Run the project using:
gradlew jsRun
To create a new component, you need to implement the SnappierComponent interface or inherit from an
existing implementation like the library's SnappierObservableComponent
if you need to observe and
react to events. Each component must have a unique id
.
Here's an example of a simple SearchComponent
:
class SearchComponent(
override val data: ComponentData,
) : SnappierObservableComponent(data) {
override val id: String = "search" // Unique identifier for this component type
@Composable
override fun View(data: Element, extras: Map<String, Any?>?) {
// Implement your Composable UI here
Text("This is a Search Component!")
// You can use `data` to access parameters passed from the server
// `data` is an `Element`, with which you can get all basic UI stuff like texts, buttons, etc
val primaryText = data.contents.firstOrNull()?.texts?.firstOrNull()
Text(text = primaryText.orEmpty())
}
override fun onEvent(event: Event) {
super.onEvent(event)
// Handle events specific to this component
println("SearchComponent received event: $event")
}
}
After creating your component, you need to register it with Snappier so it can be rendered. You can do this when initializing Snappier:
val snappier = Snappier {
// ... other configurations
}.apply {
register(SearchComponent())
}
Now, when the backend sends a JSON payload with a component whose id
matches "search",
your SearchComponent will be rendered.
A sample JSON payload can be found in the API sample home controller.
It should render a social app home page example.