---
title: Integrate CMP with Storyblok
description: This guide walks you through integrating Storyblok as a headless CMS, fetching content, and building Compose components to render it across Android, iOS, and web.
url: https://www.storyblok.com/docs/quickstarts/compose-multiplatform
---

# 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`

## Setup

Set up the project and create a Storyblok space.

[No Storyblok account?](https://app.storyblok.com/#/signup)Sign up and create a space for free

1.  **Create a [new space](https://app.storyblok.com/#/me/spaces/new?tab=select-plan)**
    
    Log in to [Storyblok](https://app.storyblok.com/#/me/spaces), select **\+ Add Space** → **New Space**, and follow the steps.
    
2.  **Create a Compose Multiplatform project**
    
    Generate a new project with the [Kotlin Multiplatform Wizard](https://kmp.jetbrains.com/). 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.
    

> [!NOTE]
> The iOS target requires macOS with Xcode installed, since the iOS toolchain and simulators come from Xcode. The Storyblok libraries publish `iosArm64` and `iosSimulatorArm64` (the wizard’s defaults), so the simulator requires Apple Silicon.

## 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.

gradle/libs.versions.toml

```kotlin
[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

```kotlin
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
<?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

Replace the contents of `App.kt` with an invocation of the [`Storyblok`](https://storyblok.github.io/storyblok-kotlin/storyblok-compose/com.storyblok.compose/-storyblok.html) composable function. This is the entry point for adding Storyblok content to your composition. Because `App.kt` lives in `commonMain`, every target renders it.

> [!TIP]
> Learn how to get an [access token](https://www.storyblok.com/docs/concepts/access-tokens#content-delivery-api-access-tokens) for your Storyblok project.

shared/src/commonMain/kotlin/org/example/project/App.kt

```kotlin
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)) }
      }
    }
  }
}
```

> [!WARNING]
> Set the [`region`](https://storyblok.github.io/storyblok-kotlin/ktor-client-storyblok/com.storyblok.ktor/-api/-config/-region/index.html) that matches the server location of your Storyblok space.

The [`Storyblok`](https://storyblok.github.io/storyblok-kotlin/storyblok-compose/com.storyblok.compose/-storyblok.html) composable function provides functions for story fetching and dynamic block rendering inside its trailing lambda [scope](https://storyblok.github.io/storyblok-kotlin/storyblok-compose/com.storyblok.compose/-storyblok-scope/index.html):

-   First, the [`story()`](https://storyblok.github.io/storyblok-kotlin/storyblok-compose/com.storyblok.compose/-storyblok-scope/index.html#816830072%2FFunctions%2F-825163495) 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()`](https://storyblok.github.io/storyblok-kotlin/storyblok-compose/com.storyblok.compose/-block-scope/-block.html) composable function renders the story content. This function uses the composable function registered for the story’s content type in the [`blockProvider`](https://storyblok.github.io/storyblok-kotlin/storyblok-material3/com.storyblok.compose.provider/block-provider.html).

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

First, create [`Component`](https://storyblok.github.io/storyblok-kotlin/content-api-client/com.storyblok.cdn.schema/-component/index.html) 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

```kotlin
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](https://www.storyblok.com/docs/api/content-delivery/v2).

> [!WARNING]
> The serial name must match the technical name given to the block in the block library.

Next, register composables for each of these blocks inside the [`blockProvider`](https://storyblok.github.io/storyblok-kotlin/storyblok-material3/com.storyblok.compose.provider/block-provider.html) in `App.kt`.

shared/src/commonMain/kotlin/org/example/project/App.kt

```kotlin
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`](https://storyblok.github.io/storyblok-kotlin/storyblok-material3/com.storyblok.compose.provider/block-provider.html) function provides functions for component registration and nested component rendering inside its trailing lambda [scope](https://storyblok.github.io/storyblok-kotlin/storyblok-compose/com.storyblok.compose.provider/-block-provider-scope/index.html):

-   First, the [`block()`](https://storyblok.github.io/storyblok-kotlin/storyblok-compose/com.storyblok.compose.provider/-block-provider-scope/block.html) function registers a composable function for each of the four [`Component`](https://storyblok.github.io/storyblok-kotlin/content-api-client/com.storyblok.cdn.schema/-component/index.html) subclasses: `Page`, `Feature`, `Teaser`, and `Grid`.
-   Next, the [`Block()`](https://storyblok.github.io/storyblok-kotlin/storyblok-compose/com.storyblok.compose/-block-scope/-block.html) composable function renders the nested blocks inside `Page` and `Grid`.

## 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 `androidApp` run configuration in Android Studio.
    
    ```bash
    ./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`.
    
    ```bash
    ./gradlew :webApp:wasmJsBrowserDevelopmentRun
    ```
    

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

## Related resources

[Storyblok Kotlin Compose SDK Reference](https://storyblok.github.io/storyblok-kotlin/storyblok-compose/index.html)

[Concept: Blocks](/docs/concepts/blocks)

[Content Delivery API: Retrieve a Single Story](/docs/api/content-delivery/v2/stories/retrieve-a-single-story)

[JetBrains: Compose Multiplatform](https://www.jetbrains.com/compose-multiplatform/)

[JetBrains: Kotlin Multiplatform Wizard](https://kmp.jetbrains.com/)

## Pagination

-   [Previous: Internationalization in Android](/docs/quickstarts/android/internationalization)
-   [Next: Dynamic Navigation in CMP](/docs/quickstarts/compose-multiplatform/dynamic-routing)
