Web Browser Navigation¶
Warning
This API is experimental, available since version 3.3.0-alpha01
.
The Web Navigation API is a successor of the old Web Browser History API. It is a more flexible and powerful tool for managing browser URLs and history in a web application. The API is designed to work with different navigation models and provides a way to synchronize the browser URL and history with the navigation state. Currently, the following navigation models are supported:
The Child Slot
navigation model is currently not supported. It will likely require additional API changes, might be implemented in the future.
WebNavigation and WebNavigationOwner interfaces¶
The API consists of two main interfaces:
- WebNavigation - A two-way navigation controller for Web browsers that connects a navigation model (e.g.
Child Stack
) with the browser's navigation history. - WebNavigationOwner - An interface that represents a holder of
WebNavigation
, typically implemented by a Decompose component.
You don't need to implement the WebNavigation
interface directly. Instead, you should implement the WebNavigationOwner
interface in your component using one of the provided functions available for each supported navigation model:
- childStackWebNavigation - For
Child Stack
navigation model. - childPagesWebNavigation - For
Child Pages
navigation model. - childPanelsWebNavigation - For
Child Panels
navigation model.
Only one instance of the WebNavigation
controller is allowed per component. The new API also supports nested navigation, there can only be up to one (zero or one) child WebNavigationOwner
at a time.
Enabling the Web Navigation¶
To enable the Web Navigation, just use the withWebHistory {}
function available for js
(link) and wasmJs
(link) targets separately. The function provides two parameters: the current URL and an instance of StateKeeper
. The URL should typically be used as a deep link to initialize the navigation state. The StateKeeper
automatically saves and restores the navigation state on page reloads, it should be used to create the root ComponentContext
.
Once the Web Navigation is enabled, the browser URL and history will be automatically synchronized with the navigation state. The browser history automatically follows the navigation state changes, and the navigation is automatically performed following the browser history changes.
Configuring the application¶
Using Web Navigation in a single page application requires additional configuration - a catch-all strategy to return the same html resource for all paths. This strategy will be different for different server configurations.
Development configuration¶
The Kotlin/JS browser
target uses webpack-dev-server as a local development server. It can be configured to use the same index.html
file (or your primary html file) for all paths, by setting the devServer.historyApiFallback flag. The Gradle DSL for Kotlin webpack currently does not support the historyApiFallback
flag, so a special configuration file should be used instead.
First, create a directory named webpack.config.d
in the JS app module's directory. Then create a new file named devServerConfig.js
inside that directory. Finally, put the following content to the file:
// <js app module>/webpack.config.d/devServerConfig.js
config.devServer = {
...config.devServer, // Merge with other devServer settings
"historyApiFallback": true
};
Web Navigation with Child Stack¶
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.router.webhistory.WebNavigationOwner
import com.arkivanov.decompose.value.Value
interface MyStackComponent : WebNavigationOwner {
val stack: Value<ChildStack<*, Child>>
sealed class Child {
// Omitted code
}
}
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.childStackWebNavigation
import com.arkivanov.decompose.router.webhistory.WebNavigation
import com.arkivanov.decompose.value.Value
import kotlinx.serialization.Serializable
class DefaultMyStackComponent(
componentContext: ComponentContext,
deepLinkUrl: String?, // Or your favourite data structure, like Uri, etc.
) : MyStackComponent, ComponentContext by componentContext {
private val nav = StackNavigation<Config>()
private val _stack: Value<ChildStack<Config, MyStackComponent.Child>> =
childStack(
source = nav,
serializer = Config.serializer(),
initialStack = { TODO("Use the deepLinkUrl parameter to initialize the stack") },
childFactory = { ... },
)
override val stack: Value<ChildStack<*, MyStackComponent.Child>> = _stack
override val webNavigation: WebNavigation<*> =
childStackWebNavigation(
navigator = nav,
stack = _stack,
serializer = Config.serializer(),
pathMapper = { child -> TODO("Return a path for the child") }, // Optional
parametersMapper = { child -> TODO("Return a Map with parameters for the child") }, // Optional
childSelector = { child -> TODO("Return a WebNavigationOwner for the child") }, // Optional
)
@Serializable
private sealed interface Config {
// Omitted code
}
}
Web Navigation with Child Pages¶
import com.arkivanov.decompose.router.pages.ChildPages
import com.arkivanov.decompose.router.webhistory.WebNavigationOwner
import com.arkivanov.decompose.value.Value
interface MyPagesComponent : WebNavigationOwner {
val pages: Value<ChildPages<*, ...>>
}
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.router.pages.ChildPages
import com.arkivanov.decompose.router.pages.PagesNavigation
import com.arkivanov.decompose.router.pages.childPages
import com.arkivanov.decompose.router.pages.childPagesWebNavigation
import com.arkivanov.decompose.router.webhistory.WebNavigation
import com.arkivanov.decompose.value.Value
import kotlinx.serialization.Serializable
class DefaultMyPagesComponent(
componentContext: ComponentContext,
deepLinkUrl: String?, // Or your favourite data structure, like Uri, etc.
) : MyPagesComponent, ComponentContext by componentContext {
private val nav = PagesNavigation<Config>()
private val _pages: Value<ChildPages<Config, ...>> =
childPages(
source = nav,
serializer = Config.serializer(),
initialPages = { TODO("Use the deepLinkUrl parameter to initialize the navigation") },
childFactory = { ... },
)
override val pages: Value<ChildPages<*, ...>> = _pages
override val webNavigation: WebNavigation<*> =
childPagesWebNavigation(
navigator = nav,
pages = _pages,
serializer = Config.serializer(),
pathMapper = { pages -> TODO("Return a path for the navigation state") }, // Optional
parametersMapper = { pages -> TODO("Return a Map with parameters for the navigation state") }, // Optional
childSelector = { child -> TODO("Return a WebNavigationOwner for the child") }, // Optional
)
@Serializable
private data class Config(...)
}
Web Navigation with Child Panels¶
import com.arkivanov.decompose.router.panels.ChildPanels
import com.arkivanov.decompose.router.webhistory.WebNavigationOwner
import com.arkivanov.decompose.value.Value
interface MyPanelsComponent : WebNavigationOwner {
val panels: Value<ChildPanels<...>>
}
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.router.panels.ChildPanels
import com.arkivanov.decompose.router.panels.PanelsNavigation
import com.arkivanov.decompose.router.panels.childPanels
import com.arkivanov.decompose.router.panels.childPanelsWebNavigation
import com.arkivanov.decompose.router.webhistory.WebNavigation
import com.arkivanov.decompose.value.Value
import kotlinx.serialization.Serializable
class DefaultMyPanelsComponent(
componentContext: ComponentContext,
deepLinkUrl: String?, // Or your favourite data structure, like Uri, etc.
) : MyPanelsComponent, ComponentContext by componentContext {
private val nav = PanelsNavigation<MainConfig, DetailsConfig, Nothing>()
private val _panels: Value<ChildPanels<...>> =
childPanels(
source = nav,
serializers = MainConfig.serializer() to DetailsConfig.serializer(),
initialPanels = { TODO("Use the deepLinkUrl parameter to initialize the navigation") },
mainFactory = { ... },
detailsFactory = { ... },
)
override val panels: Value<ChildPanels<...>> = _panels
override val webNavigation: WebNavigation<*> =
childPanelsWebNavigation(
navigator = nav,
panels = _panels,
serializers = MainConfig.serializer() to DetailsConfig.serializer(),
pathMapper = { panels -> TODO("Return a path for the navigation state") }, // Optional
parametersMapper = { panels -> TODO("Return a Map with parameters for the navigation state") }, // Optional
childSelector = { panels -> TODO("Return a WebNavigationOwner for the navigation state") }, // Optional
)
@Serializable
private data class MainConfig(...)
@Serializable
private data class DetailsConfig(...)
}
Example¶
Please refer to the main sample for a complete example of using the Web Navigation API.