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.
-
Create a new space
Log in to Storyblok, select + Add Space → New Space, and follow the steps.
-
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
sharedmodule holding the common UI, plus a thin entry-point module per platform:androidApp,webApp, and aniosAppXcode project.
Add dependencies
Section titled “Add dependencies”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.
[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.
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.
<?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.
Fetch a single story
Section titled “Fetch a single story”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.
package org.example.project
import androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.ui.Modifierimport androidx.lifecycle.compose.collectAsStateWithLifecycleimport com.storyblok.compose.Storyblokimport com.storyblok.compose.provider.blockProviderimport com.storyblok.ktor.Api.Config.Region.*import com.storyblok.ktor.Api.Config.Version.*
@Composablefun 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 preconfiguredhomestory from the space. The function returns aFlowthat 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 theblockProvider.
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’
Create and register blocks
Section titled “Create and register blocks”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:
package org.example.project
import kotlinx.serialization.SerialNameimport kotlinx.serialization.Serializableimport 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.
import com.storyblok.ktor.Api.Config.Version.*import androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.FlowRowimport androidx.compose.foundation.lazy.LazyColumnimport androidx.compose.foundation.lazy.itemsimport androidx.compose.material3.MaterialTheme.typographyimport 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 fourComponentsubclasses:Page,Feature,Teaser, andGrid. - Next, the
Block()composable function renders the nested blocks insidePageandGrid.
Run on every target
Section titled “Run on every target”The same App() composable now renders on all three targets. Run the app on each target:
-
Android: run the following command, or select the
androidApprun configuration in Android Studio.Terminal window ./gradlew :androidApp:installDebug -
iOS: select the
iosApprun configuration in the IDE, pick a simulator, and run. Alternatively, openiosApp/iosApp.xcodeprojin Xcode and run theiosAppscheme. -
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.
Related resources
Section titled “Related resources”Was this page helpful?
This site uses reCAPTCHA and Google's Privacy Policy (opens in a new window).Terms of Service (opens in a new window) apply.
Get in touch with the Storyblok community