Skip to content

What it is

Voyager is a navigation library for Compose Multiplatform with type-safe screens, nested navigators, tabs and integrated screen-scoped models.

A Screen is a class implementing a Content composable. Navigation pushes and pops instances, so arguments are constructor parameters rather than encoded strings.

Installation

implementation("cafe.adriel.voyager:voyager-navigator:1.1.0-beta02")

Getting started

The smallest useful thing you can do with it, and what each part means.

Screens as typed objects
data class BookDetailScreen(val bookId: Int) : Screen {
    @Composable
    override fun Content() {
        val navigator = LocalNavigator.currentOrThrow
        val model = rememberScreenModel { BookDetailModel(bookId) }
        val state by model.state.collectAsState()

        BookDetail(state, onBack = navigator::pop)
    }
}

// Arguments are constructor parameters — no string routes to mistype.
navigator.push(BookDetailScreen(bookId = 42))
navigator.pop()
navigator.replaceAll(HomeScreen)

Navigator(HomeScreen) { navigator ->
    SlideTransition(navigator)
}
Because a screen is a data class, passing the wrong argument type is a compile error — unlike route strings, where a typo surfaces at runtime as a blank screen.

Advanced usage

Where the library earns its place over a simpler alternative.

Screen models and tabs
class BookDetailModel(private val id: Int) : ScreenModel {
    private val _state = MutableStateFlow<UiState>(UiState.Loading)
    val state = _state.asStateFlow()

    init {
        // screenModelScope is cancelled when the screen is popped.
        screenModelScope.launch { _state.value = load(id) }
    }
}

object LibraryTab : Tab {
    override val options: TabOptions
        @Composable get() = TabOptions(index = 0u, title = "Library")

    @Composable override fun Content() = Navigator(BookListScreen)
}

TabNavigator(LibraryTab) {
    Scaffold(bottomBar = { TabRow(...) }, content = { CurrentTab() })
}
screenModelScope ties coroutines to the screen's lifetime, so popping a screen cancels its in-flight work — the equivalent of viewModelScope, but Multiplatform.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

LocalNavigator is null
The composable is outside a Navigator. Use currentOrThrow inside Screen.Content, not in a detached composable.
Screen state is lost on rotation
Screens must be serialisable for Android state restoration. Use data classes with primitive parameters.

Best practices

  • Keep Screen classes serialisable-friendly if you need state restoration on Android.
  • Use rememberScreenModel rather than remembering a model directly, so it survives recomposition.
  • Nest Navigators per tab so each keeps its own back stack.
  • Prefer Jetpack Navigation on Android-only projects; Voyager's advantage is Multiplatform.

Background

Why it exists, and what it was reacting to.

Jetpack Navigation is Android-only and string-route based. Voyager offers navigation as ordinary Kotlin objects, which works across Multiplatform targets and is type-safe by construction.