Skip to content

Integrate CMP with Storyblok

Use Storyblok to manage the content of your Compose Multiplatform (CMP) project. One shared implementation runs on Android and iOS, and a web target previews content in Storyblok’s Visual Editor.

This guide has been tested with the following versions:
  • Kotlin Multiplatform Wizard (September 2026)
  • Kotlin 2.4.20
  • Compose Multiplatform 1.12.1
  • Storyblok Compose 0.7.1
  • Storyblok Material 3 0.7.1

Set up the project and create a Storyblok space.

  1. Create a new space

    Log in to Storyblok, select + Add Space → New Space, and follow the steps.

  2. Create a Compose Multiplatform project

    Generate a new project with the Kotlin Multiplatform Wizard. Select Android, iOS, and Web, and choose Share UI for iOS so that every target renders the same Compose code. Download the archive and open it in IntelliJ IDEA or Android Studio.

    The wizard produces a shared module holding the common UI, plus a thin entry-point module per platform: androidApp, webApp, and an iosApp Xcode project.

In the version catalog TOML file, raise android-minSdk to 30 (the Storyblok libraries require Android API 30 or higher), add the Storyblok libraries, and add the Kotlin Serialization plugin.

gradle/libs.versions.toml
[versions]
agp = "9.1.1"
…
android-minSdk = "24"
android-minSdk = "30"
…
composeMultiplatform = "1.12.1"
storyblok = "0.7.1"
[libraries]
…
compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "material3" }
storyblok-compose = { module = "com.storyblok:storyblok-compose", version.ref = "storyblok" }
storyblok-material3 = { module = "com.storyblok:storyblok-material3", version.ref = "storyblok" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
…
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

To apply the serialization plugin, set Kotlin compiler options, and add the dependencies to the common source set, update shared/build.gradle.kts.

shared/build.gradle.kts
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidMultiplatformLibrary)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.kotlinSerialization)
}
kotlin {
…
compilerOptions {
optIn.add("kotlin.uuid.ExperimentalUuidApi")
}
sourceSets {
commonMain.dependencies {
…
implementation(libs.androidx.lifecycle.runtimeCompose)
implementation(libs.storyblok.compose)
implementation(libs.storyblok.material3)
}
}
}

Finally, add the internet permission to the Android manifest.

androidApp/src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
…

Now reload the Gradle project to download the added dependencies and re-analyze the project.

Replace the contents of App.kt with an invocation of the Storyblok composable function. This is the entry point for adding Storyblok content to your composition. Because App.kt lives in commonMain, every target renders it.

shared/src/commonMain/kotlin/org/example/project/App.kt
package org.example.project
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.storyblok.compose.Storyblok
import com.storyblok.compose.provider.blockProvider
import com.storyblok.ktor.Api.Config.Region.*
import com.storyblok.ktor.Api.Config.Version.*
@Composable
fun App() {
MaterialTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Storyblok(
accessToken = "YOUR_ACCESS_TOKEN",
version = Draft,
region = EU, // Choose the correct region from your Space.
blockProvider = blockProvider(
fallback = { it, modifier -> Text("Unknown block type '${it.component}'", modifier) }
) {
/* blocks registered here in the next step */
}
) {
val story by story("home").collectAsStateWithLifecycle(null)
story?.content?.let { Block(it, Modifier.padding(innerPadding)) }
}
}
}
}

The Storyblok composable function provides functions for story fetching and dynamic block rendering inside its trailing lambda scope:

  • First, the story() function fetches the preconfigured home story from the space. The function returns a Flow that emits first from cache (if present), then from the network (once connected).
  • Next, the Block() composable function renders the story content. This function uses the composable function registered for the story’s content type in the blockProvider.

You can now build and run the app. However, since the app registers no block types yet, only the fallback composable passed to the blockProvider function renders:

Unknown block type ‘page’

First, create Component subclasses for each of the preconfigured blocks defined in the block library of the space.

Create a new BlockLibrary.kt file in the common source set:

shared/src/commonMain/kotlin/org/example/project/BlockLibrary.kt
package org.example.project
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import com.storyblok.cdn.schema.Component
@Serializable
@SerialName("feature")
data class Feature(val name: String): Component()
@Serializable
@SerialName("grid")
data class Grid(val columns: List<Component>): Component()
@Serializable
@SerialName("page")
data class Page(val body: List<Component>): Component()
@Serializable
@SerialName("teaser")
data class Teaser(val headline: String): Component()

The @Serializable and @SerialName annotations from the Kotlin Serialization plugin let the SDK deserialize the classes from the Content Delivery API.

Next, register composables for each of these blocks inside the blockProvider in App.kt.

shared/src/commonMain/kotlin/org/example/project/App.kt
import com.storyblok.ktor.Api.Config.Version.*
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.ui.unit.dp
…
blockProvider = blockProvider(
fallback = { it, modifier -> Text("Unknown block type '${it.component}'", modifier) }
) {
/* blocks registered here in the next step */
block<Page> { page, modifier ->
LazyColumn(modifier) {
items(page.body, key = { it.uid }) { Block(it, Modifier.fillMaxWidth()) }
}
}
block<Feature> { feature, modifier ->
Text(feature.name, modifier.padding(16.dp))
}
block<Teaser> { teaser, modifier ->
Text(teaser.headline, modifier.padding(16.dp), style = typography.headlineMedium)
}
block<Grid> { grid, modifier ->
FlowRow(modifier) { grid.columns.forEach { Block(it) } }
}
}
…

The blockProvider function provides functions for component registration and nested component rendering inside its trailing lambda scope:

  • First, the block() function registers a composable function for each of the four Component subclasses: Page, Feature, Teaser, and Grid.
  • Next, the Block() composable function renders the nested blocks inside Page and Grid.

The same App() composable now renders on all three targets. Run the app on each target:

  • Android: run the following command, or select the androidApp run configuration in Android Studio.

    Terminal window
    ./gradlew :androidApp:installDebug
  • iOS: select the iosApp run configuration in the IDE, pick a simulator, and run. Alternatively, open iosApp/iosApp.xcodeproj in Xcode and run the iosApp scheme.

  • Web: run the following command, then open http://localhost:8080.

    Terminal window
    ./gradlew :webApp:wasmJsBrowserDevelopmentRun

Each target shows the complete home story with the teaser, feature, and grid components rendered.

Was this page helpful?

What went wrong?

This site uses reCAPTCHA and Google's Privacy Policy (opens in a new window).Terms of Service (opens in a new window) apply.