Skip to content

Dynamic Navigation in CMP

Use Navigation 3 to set up a catch-all navigation strategy in your Compose Multiplatform (CMP) project and render new stories dynamically.

In the version catalog TOML file, add the Navigation 3 library.

gradle/libs.versions.toml
[versions]
agp = "9.1.1"
…
storyblok = "0.7.1"
navigation3 = "1.1.1"
[libraries]
…
storyblok-material3 = { module = "com.storyblok:storyblok-material3", version.ref = "storyblok" }
navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "navigation3" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
…

To add the dependency, update shared/build.gradle.kts.

shared/build.gradle.kts
…
sourceSets {
commonMain.dependencies {
…
implementation(libs.storyblok.material3)
implementation(libs.navigation3.ui)
}
}

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

Create a StoryKey.kt file in the common source set to define a navigation key that can represent any story, identified by either its UUID or slug.

shared/src/commonMain/kotlin/org/example/project/StoryKey.kt
package org.example.project
import androidx.navigation3.runtime.NavKey
import androidx.savedstate.serialization.SavedStateConfiguration
import com.storyblok.cdn.schema.Component
import com.storyblok.cdn.schema.Story
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
import kotlin.uuid.Uuid
@Serializable
data class StoryKey(
@Transient
val story: Story<out Component>? = null,
@EncodeDefault
val uuid: Uuid? = story?.uuid,
@EncodeDefault
val slug: String? = story?.slug,
) : NavKey
val HomeKey = StoryKey(slug = "home")
val NavConfiguration = SavedStateConfiguration {
serializersModule = SerializersModule {
polymorphic(NavKey::class) {
subclass(StoryKey::class, StoryKey.serializer())
}
}
}

StoryKey is a NavKey that persists the uuid or slug of a story to the back stack. You can also construct a StoryKey with a Story instance for immediate display when navigating between screens.

In your App.kt file, add the code to initialize and use a NavDisplay to render the top of the back stack.

shared/src/commonMain/kotlin/org/example/project/App.kt
…
import androidx.compose.ui.unit.dp
import androidx.compose.runtime.remember
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
@Composable
fun App() {
MaterialTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
val backStack = rememberNavBackStack(NavConfiguration, HomeKey)
Storyblok(
accessToken = "YOUR_ACCESS_TOKEN",
version = Draft,
region = EU, // Choose the correct region from your Space.
…
) {
val story by story("home").collectAsStateWithLifecycle(null)
story?.content?.let { Block(it, Modifier.padding(innerPadding)) }
NavDisplay(
backStack,
Modifier.padding(innerPadding),
entryProvider = entryProvider {
entry<StoryKey> { key ->
val story by remember(key) {
when (key.uuid) {
null -> story(slug = key.slug!!)
else -> story(uuid = key.uuid)
}
}.collectAsStateWithLifecycle(key.story)
story?.content?.let { Block(it) }
}
}
)
}
}
}
}

The single entry<StoryKey> handles every possible story destination. It fetches the story by UUID or slug, populating the initial value of collectAsStateWithLifecycle with the key’s story instance so available content appears immediately.

NavDisplay also handles the platform’s back gesture: the system back button on Android, and the back swipe on iOS.

With this approach, the project renders any new story in the space as soon as the app pushes its key onto the back stack.

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.