Skip to content

Integrate Android with Storyblok

Use Storyblok to manage the content of your Android project.

This guide has been tested with the following versions:
  • Android Studio Quail 2 | 2026.1.2
  • Storyblok Compose 0.3.0
  • Storyblok Material 3 0.3.0

Set up the project and create a Storyblok space.

  1. Create a new space

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

  2. Create an Android project

    Create a new Android project in Android Studio following the official Create a project guide. Select the Empty Activity template to get a minimal Jetpack Compose setup, and set the Minimum SDK to API 30 or higher.

In the version catalog TOML file, add the Storyblok libraries and the Kotlin Serialization plugin.

gradle/libs.versions.toml
[versions]
agp = "9.2.1"
composeBom = "2026.02.01"
storyblok = "0.3.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
storyblok-compose-android = { group = "com.storyblok", name = "storyblok-compose-android", version.ref = "storyblok" }
storyblok-material3-android = { group = "com.storyblok", name = "storyblok-material3-android", version.ref = "storyblok" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

Update app/build.gradle.kts to apply the serialization plugin, set Kotlin compiler options, and add the dependencies.

app/build.gradle.kts
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
}
android {
}
kotlin {
compilerOptions {
optIn.add("kotlin.uuid.ExperimentalUuidApi")
}
}
dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.storyblok.compose.android)
implementation(libs.storyblok.material3.android)
}

Finally, add the internet permission to the AndroidManifest.xml.

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

Now select Sync Project with Gradle Files to download the added dependencies and re-analyze the project.

Replace the following in MainActivity.kt with an invocation of the Storyblok composable function. This is the entry point for adding Storyblok content to your composition.

app/src/main/java/com/example/myapplication/MainActivity.kt
import com.example.myapplication.ui.theme.MyApplicationTheme
import androidx.compose.runtime.getValue
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.*
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyApplicationTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(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 is used to fetch 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 is invoked with 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 no block types have been registered yet, only the content of the fallback composable supplied via the blockProvider class is rendered:

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:

app/src/main/java/com/example/myapplication/BlockLibrary.kt
package com.example.myapplication
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 classes are marked with @Serializable and @SerialName annotations from the Kotlin Serialization plugin so they can be deserialized from the Content Delivery API.

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

app/src/main/java/com/example/myapplication/MainActivity.kt
import com.storyblok.ktor.Api.Config.Version.*
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
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, _ -> Text("Unknown block type '${it.component}'") }
) {
/* 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 is used to register a composable function for each of the four Component subclasses: Page, Feature, Teaser, and Grid.
  • Next, the Block() composable function is invoked to render the nested blocks inside Page and Grid.

Run the app again and see the complete home story with the teaser, feature, and grid components being 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.