diff --git a/search/search_index.json b/search/search_index.json index a08cdf12..cc571fa8 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Overview","text":""},{"location":"#voyager-compose-on-warp-speed","title":"Voyager: Compose on Warp Speed","text":"
A multiplatform navigation library built for, and seamlessly integrated with, Jetpack Compose.
Create scalable Single-Activity apps powered by a pragmatic API:
class HomeScreenModel : ScreenModel {\n // ...\n}\n\nclass HomeScreen : Screen {\n\n @Composable\n override fun Content() {\n val screenModel = rememberScreenModel { HomeScreenModel() }\n // ...\n }\n}\n\nclass SingleActivity : ComponentActivity() {\n\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n\n setContent {\n Navigator(HomeScreen())\n }\n }\n}\n
Turn on the Warp Drive and enjoy the voyage \ud83d\udd96
"},{"location":"#features","title":"Features","text":"By default, Voyager will handle back presses but you can override its behavior. Use the onBackPressed
to manually handle it: return true
to pop the current screen, or false otherwise. To disable, just set to null
.
setContent {\n Navigator(\n initialScreen = HomeScreen,\n onBackPressed = { currentScreen ->\n false // won't pop the current screen\n // true will pop, default behavior\n }\n // To disable:\n // onBackPressed = null\n )\n}\n
"},{"location":"community-projects/","title":"Community Projects Catalog","text":""},{"location":"community-projects/#open-source-libraries-with-voyager-extensions","title":"Open source libraries with Voyager extensions","text":"Warning
Currently Voyager does not provided a built in solution to handle Deeplink and URIs. see #149 and #382
You can initialize the Navigator
with multiple screens, that way, the first visible screen will be the last one and will be possible to return (pop()
) to the previous screens.
val postId = getPostIdFromIntent()\n\nsetContent {\n Navigator(\n HomeScreen,\n PostListScreen(),\n PostDetailsScreen(postId)\n )\n}\n
"},{"location":"faq/","title":"Faq","text":"iOS Swipe Back support { #ios-swipeback }
Voyager does not have a built in support for swipe back yet, we are not 100% conformable with all solutions that have out there and we think we will better of using a community made solution by copying to your code base and be able to change as you want your app to behave.
See this issue each for community build solutions.
Alternatively, we can also discuss in the future a community build solution using NavigationController
under the hood like compose-cupertino
have implemented for Decompose.
Support for predictive back animations { #predictive-back }
Voyager does not have a built in support for predictive back yet, but as well as iOS Swipe Back, the community have build extensions, and snippets with support, see #223 and 144.
Support for result passing between screens { #result-passing }
Voyager does not have a built in support for swipe back yet, we are not 100% conformable with all solutions that have out there, we encourage to use community made solutions by copying to your code base and being free to extend as your code base needs.
See #128 comments for community solutions.
Deeplink support
"},{"location":"lifecycle/","title":"Lifecycle","text":"** Experimental API
Inside a Screen
, you can call LifecycleEffectOnce
to execute a block of code the first time the Screen appears, and optionally define a onDispose
callback for when the Screen is leaving/removed from the Stack:
class PostListScreen : Screen {\n\n @Composable\n override fun Content() {\n LifecycleEffectOnce {\n screenModel.initSomething()\n onDispose {\n // Do something when the screen is leaving/removed from the Stack\n }\n }\n\n // ...\n }\n}\n
"},{"location":"lifecycle/#screenlifecycleowner","title":"ScreenLifecycleOwner","text":" With ScreenLifecycleProvider
interface you can provide a special ScreenLifecycleOwner
that can react when the Screen leaves the Stack
class MyScreenLifecycleOwner : ScreenLifecycleOwner {\n override fun onDispose(screen: Screen) {\n println(\"My ${screen.key} is being disposed\")\n }\n}\n\n\ndata object MyScreen : Screen, ScreenLifecycleProvider {\n @Composable\n fun Content() {\n ...\n }\n\n public override fun getLifecycleOwner(): ScreenLifecycleOwner =\n ScreenLifecycleOwner()\n}\n
"},{"location":"lifecycle/#extending-the-lifecycle-lazily","title":"Extending the Lifecycle lazily","text":"You can access directly the ScreenLifecycleStore
to add a ScreenDisposable
implementation lazily in a lifecycle aware custom API.
If you for example have a custom dependency that should be notified when the Screen is disposed you can use the ScreenLifecycleStore
directly. For example, the Screen Model and ScreenModelStore APIs rely on this API to Store the ScreenModel instance and the Screen owns the ScreenModel instance, when the Screen leaves the Stack, the ScreenModelStore, that implements ScreenDisposable
is notified and can dispose the ScreenModel.
Let\u2019s imagine a dependency called MyDependency
that holds and provides a State for a Screen while it is on the Navigator stack and we want to notify MyDependency
when the Screen leaves the Stack.
class MyCustomAPIWithDisposable(\n private val myDependency: MyDependency\n) : ScreenDisposable {\n public override fun onDispose(screen: Screen) {\n myDependency.disposeStateForScreen(screen)\n }\n}\n\n@Composable\nfun rememberMyDependency(): MyDependency {\n val navigator = LocalNavigator.currentOrThrow\n val myDependency by getMyDependecy() // getting from DI\n remember(myDependency) {\n myDependency.provideStateForScreen(navigator.lastItem)\n ScreenLifecycleStore.get(navigator.lastItem) {\n MyCustomAPIWithDisposable(myDependency)\n }\n }\n\n return myDependency\n}\n
"},{"location":"lifecycle/#screendisposable-for-all-screens","title":"ScreenDisposable for all Screens","text":"You can also provide a ScreenLifecycleOwner
for all Screen in the stack of a Navigator easily by in the Navigator composable start to listen to lastItem
for registering at ScreenLifecycleStore.
Let\u2019s reuse the MyScreenLifecycleOwner
from the example above and provide it to all screens in the navigator.
class MyScreenLifecycleOwner : ScreenDisposable {\n override fun onDispose(screen: Screen) {\n println(\"My ${screen.key} is being disposed\")\n }\n}\n\n@Composable\nfun YourAppComposable() {\n Navigator(...) { navigator ->\n remember(navigator.lastItem) {\n ScreenLifecycleStore.get(navigator.lastItem) {\n MyScreenLifecycleOwner()\n }\n }\n CurrentScreen()\n }\n}\n
"},{"location":"migration-to-1.0.0/","title":"Migration to 1.0.0","text":""},{"location":"migration-to-1.0.0/#screenmodel","title":"ScreenModel","text":""},{"location":"migration-to-1.0.0/#new-module","title":"New Module","text":"Now the ScreenModel API has its own module and no longer are available in voyager-core
, if you are using ScreenModel you should declare the dependency cafe.adriel.voyager:voyager-screenmodel
(see Setup).
Since 1.0.0-rc08 we have introduced the ScreenModel scoped at Navigator Lifecycle, now the API is no longer marked as Experimental.
"},{"location":"migration-to-1.0.0/#deprecation-cycle","title":"Deprecation cycle","text":"Since 1.0.0-rc08
we have renamed the extension coroutineScope
to screenModelScope
, now it was removed from 1.0.0, if you are still using it, just replace with screenModelScope.
The module voyager-androidx
and AndroidScreen
was removed! Since 1.0.0-rc06
we have introduced a new API called NavigatorScreenLifecycleProvider
that provides by default the previous behavior of AndroidScreenLifecycleOwner
on Android target for all Screen.
Important notice: AndroidScreen, different from Screen, it holds the Screen.key
as a uniqueScreenKey
, this is a pretty common requirement, to avoid issues and weird behaviors, we recommend continuing to specify a uniqueScreenKey
if you are not, we also recommend creating a abstract class UniqueScreen
to replace your AndroidScreen
implementation.
abstract class UniqueScreen : Screen {\n override val key: ScreenKey = uniqueScreenKey\n}\n
"},{"location":"migration-to-1.0.0/#apis-promote-to-stable","title":"APIs promote to Stable","text":"NavigatorLifecycleStore
and NavigatorDisposable
TabDisposable
ScreenLifecycleStore.get
: Use register
or new get
function instead.Stack.lastOrNull
: Use lastItemOrNull
insteadNavigator.last
: Use lastItem
insteadScreenModel.coroutineScope
: Use screenModelScope
instead ScreenModelStore.remove
: Use onDispose
instead.Tab.title
: Use options
instead.Tab.icon
: Use options
instead.Add Maven Central to your repositories if needed
repositories {\n mavenCentral()\n}\n
2. Add the desired dependencies to your module\u2019s build.gradle
file dependencies {\n val voyagerVersion = \"1.1.0-beta02\"\n\n // Multiplatform\n\n // Navigator\n implementation(\"cafe.adriel.voyager:voyager-navigator:$voyagerVersion\")\n\n // Screen Model\n implementation(\"cafe.adriel.voyager:voyager-screenmodel:$voyagerVersion\")\n\n // BottomSheetNavigator\n implementation(\"cafe.adriel.voyager:voyager-bottom-sheet-navigator:$voyagerVersion\")\n\n // TabNavigator\n implementation(\"cafe.adriel.voyager:voyager-tab-navigator:$voyagerVersion\")\n\n // Transitions\n implementation(\"cafe.adriel.voyager:voyager-transitions:$voyagerVersion\")\n\n // Android\n\n // Koin integration\n implementation(\"cafe.adriel.voyager:voyager-koin:$voyagerVersion\")\n\n // Hilt integration\n implementation(\"cafe.adriel.voyager:voyager-hilt:$voyagerVersion\")\n\n // LiveData integration\n implementation(\"cafe.adriel.voyager:voyager-livedata:$voyagerVersion\")\n\n // Desktop + Android\n\n // Kodein integration\n implementation(\"cafe.adriel.voyager:voyager-kodein:$voyagerVersion\")\n\n // RxJava integration\n implementation(\"cafe.adriel.voyager:voyager-rxjava:$voyagerVersion\")\n}\n
[versions]\nvoyager = \"1.1.0-beta02\"\n\n[libraries]\nvoyager-navigator = { module = \"cafe.adriel.voyager:voyager-navigator\", version.ref = \"voyager\" }\nvoyager-screenModel = { module = \"cafe.adriel.voyager:voyager-screenmodel\", version.ref = \"voyager\" }\nvoyager-bottomSheetNavigator = { module = \"cafe.adriel.voyager:voyager-bottom-sheet-navigator\", version.ref = \"voyager\" }\nvoyager-tabNavigator = { module = \"cafe.adriel.voyager:voyager-tab-navigator\", version.ref = \"voyager\" }\nvoyager-transitions = { module = \"cafe.adriel.voyager:voyager-transitions\", version.ref = \"voyager\" }\nvoyager-koin = { module = \"cafe.adriel.voyager:voyager-koin\", version.ref = \"voyager\" }\nvoyager-hilt = { module = \"cafe.adriel.voyager:voyager-hilt\", version.ref = \"voyager\" }\nvoyager-kodein = { module = \"cafe.adriel.voyager:voyager-kodein\", version.ref = \"voyager\" }\nvoyager-rxjava = { module = \"cafe.adriel.voyager:voyager-rxjava\", version.ref = \"voyager\" }\n
Current version here.
"},{"location":"setup/#platform-compatibility","title":"Platform compatibility","text":"Multiplatform targets: Android, iOS, Desktop, Mac Native, Web.
Android Desktop Multiplatform voyager-navigator voyager-screenModel voyager-bottom-sheet-navigator voyager-tab-navigator voyager-transitions voyager-koin voyager-kodein voyager-lifecycle-kmp voyager-hilt voyager-rxjava voyager-livedata"},{"location":"stack-api/","title":"Stack API","text":""},{"location":"stack-api/#snapshotstatestack","title":"SnapshotStateStack","text":"Voyager is backed by a SnapshotStateStack:
You will use it to navigate forward (push
, replace
, replaceAll
) and backwards (pop
, popAll
, popUntil
), but the SnapshotStateStack
can also be used as a regular collection outside the Navigator
.
val stack = mutableStateStackOf(\"\ud83c\udf47\", \"\ud83c\udf49\", \"\ud83c\udf4c\", \"\ud83c\udf50\", \"\ud83e\udd5d\", \"\ud83c\udf4b\")\n// \ud83c\udf47, \ud83c\udf49, \ud83c\udf4c, \ud83c\udf50, \ud83e\udd5d, \ud83c\udf4b\n\nstack.lastItemOrNull\n// \ud83c\udf4b\n\nstack.push(\"\ud83c\udf4d\")\n// \ud83c\udf47, \ud83c\udf49, \ud83c\udf4c, \ud83c\udf50, \ud83e\udd5d, \ud83c\udf4b, \ud83c\udf4d\n\nstack.pop()\n// \ud83c\udf47, \ud83c\udf49, \ud83c\udf4c, \ud83c\udf50, \ud83e\udd5d, \ud83c\udf4b\n\nstack.popUntil { it == \"\ud83c\udf50\" }\n// \ud83c\udf47, \ud83c\udf49, \ud83c\udf4c, \ud83c\udf50\n\nstack.replace(\"\ud83c\udf53\")\n// \ud83c\udf47, \ud83c\udf49, \ud83c\udf4c, \ud83c\udf53\n\nstack.replaceAll(\"\ud83c\udf52\")\n// \ud83c\udf52\n
You can also create a SnapshotStateStack
through rememberStateStack()
, it will restore the values after Activity recreation.
Whenever the content changes, the SnapshotStateStack
will emit a StackEvent
. Use the stack.lastEvent
to get the most recent one.
The available events are:
Push
: whenever push
is calledReplace
: whenever replace
and replaceAll
are calledPop
: whenever pop
and popAll
are calledIdle
: default eventThis is very useful for deciding which transition to make.
"},{"location":"stack-api/#sample","title":"Sample","text":"Info
Source code here.
"},{"location":"state-restoration/","title":"State restoration","text":"Voyager by default expect that it screens can be stored inside a Bundle. This means both Java Serializable and Parcelable are supported. By default all Voyager Screen is Java Serializable this means that Screen can be restored if all parameters are Java Serializable.
"},{"location":"state-restoration/#java-serializable","title":"Java Serializable","text":"// \u2714\ufe0f DO\ndata class Post(/*...*/) : Serializable\n\ndata class ValidScreen(\n val userId: UUID, // Built-in serializable types\n val post: Post // Your own serializable types\n) : Screen {\n // ...\n}\n\n// \ud83d\udeab DON'T\nclass Post(/*...*/)\n\ndata class InvalidScreen(\n val context: Context, // Built-in non-serializable types\n val post: Post, // Your own non-serializable types\n val parcelable: SomeParcelable // Android Parcelable is not Java Serializable by default\n) : Screen {\n // ...\n}\n
Not only the params, but the properties will also be restored, so the same rule applies.
// \u2714\ufe0f DO\nclass ValidScreen : Screen {\n\n // Serializable properties\n val tag = \"ValidScreen\"\n\n // Lazily initialized serializable types\n val randomId by lazy { UUID.randomUUID() }\n}\n\n// \ud83d\udeab DON'T\nclass InvalidScreen : Screen {\n\n // Non-serializable properties\n val postService = PostService()\n}\n
"},{"location":"state-restoration/#android-parcelables","title":"Android Parcelables","text":"// \u2714\ufe0f DO\n@Parcelize\ndata class Post(/*...*/) : Parcelable\n\n@Parcelize\ndata class ValidScreen(\n val post: Post // Your own parcelable types\n) : Screen, Parcelable {\n // ...\n}\n\n// \ud83d\udeab DON'T\nclass Post(/*...*/)\n\n@Parcelize\ndata class InvalidScreen(\n val context: Context, // Built-in non-parcelable types\n val post: Post, // Your own non-parcelable types\n val serializable: SomeSerializable // Java Serializable are not Android Parcelable by default\n) : Screen, Parcelable {\n // ...\n}\n
"},{"location":"state-restoration/#enforcing-android-parcelable-on-your-screens","title":"Enforcing Android Parcelable on your screens","text":"You can build your own Screen type for enforcing in at compile time that all yours screens should be Parcelable by creating an interface that implement Parcelable.
interface ParcelableScreen : Screen, Parcelable\n\n// Compile\n@Parcelize\ndata class Post(/*...*/) : Parcelable\n\n@Parcelize\ndata class ValidScreen(\n val post: Post\n) : ParcelableScreen {\n // ...\n}\n\n// Not compile\ndata class Post(/*...*/)\n\n@Parcelize\ndata class ValidScreen(\n val post: Post\n) : ParcelableScreen {\n // ...\n}\n
Starting from version 1.0.0-rc05 you can specify a custom NavigatorSaver to enforce that all Screen is Parcelable by using parcelableNavigatorSaver
.
CompositionLocalProvider(\n LocalNavigatorSaver provides parcelableNavigatorSaver()\n) {\n Navigator(...) {\n ...\n }\n}\n
"},{"location":"state-restoration/#multiplatform-state-restoration","title":"Multiplatform state restoration","text":"When working in a Multiplatform project and sharing the Parameters models with other platforms, your types required to be serializable in a Bundle if you are targeting Android, the easiest way is defining in common code a JavaSerializable
interface that on Android only would implement java.io.Serialiable
, see example below.
// commonMain - module core\nexpect interface JavaSerializable\n\ndata class Post(/*...*/) : JavaSerializable\n\n// androidMain - module core\nactual typealias JavaSerializable = java.io.Serializable\n\n// non AndroidMain (ios, web, etc) - module core\nactual interface JavaSerializable\n\n// android ui module or compose multiplatform module\ndata class ValidScreen(\n val post: Post\n) : Screen\n
"},{"location":"state-restoration/#dependency-injection","title":"Dependency Injection","text":"If you want to inject dependencies through a DI framework, make sure it supports Compose, like Koin and Kodein.
// \u2714\ufe0f DO\nclass ValidScreen : Screen {\n\n @Composable\n override fun Content() {\n // Inject your dependencies inside composables\n val postService = get<PostService>()\n }\n}\n\n// \ud83d\udeab DON'T\nclass InvalidScreen : Screen {\n\n // Using DI to inject non-serializable types as properties\n val postService by inject<PostService>()\n}\n
"},{"location":"state-restoration/#identifying-screens","title":"Identifying screens","text":"The Screen
interface has a key
property used for saving and restoring the states for the subtree. You can override the default value to set your own key.
class HomeScreen : Screen {\n\n override val key = \"CUSTOM_KEY\"\n\n @Composable\n override fun Content() {\n // ...\n }\n}\n
Voyager provides a uniqueScreenKey
property, useful if you don\u2019t want to manage the keys yourself.
override val key = uniqueScreenKey\n
Warning
You should always set your own key if the screen:
Navigator
To use the transitions you should first import cafe.adriel.voyager:voyager-transitions
(see Setup).
Voyager has built-in transitions! When initializing the Navigator
you can override the default content and use, for example, the SlideTransition
.
setContent {\n Navigator(HomeScreen) { navigator ->\n SlideTransition(navigator)\n }\n}\n
Known bug
There is a known bug using any Transition APIs can leaky ScreenModels or ViewModels, this happens because Voyager by default dispose Screens in the next Composition tick after a pop
or replace
is called, but the transition only finish later, so the ScreenModel or ViewModel is re created or cleared to early. For this purpose since Voyager 1.1.0-beta02
we have introduce a new API to fix this issue. For more details on the issue see #106.
Navigator(\n screen = ...,\n disposeBehavior = NavigatorDisposeBehavior(disposeSteps = false),\n) { navigator ->\n SlideTransition(\n navigator = navigator,\n ...\n disposeScreenAfterTransitionEnd = true\n )\n}\n
Warning
Have encounter Screen was used multiple times
crash? Provide a uniqueScreenKey
for your Screens
class ScreenFoo : Screen {\n\n override val key: ScreenKey = uniqueScreenKey\n\n @Composable\n override fun Content() {\n ...\n }\n
"},{"location":"transitions-api/#available-transitions","title":"Available transitions","text":"FadeTransition()
SlideTransition()
ScaleTransition()
"},{"location":"transitions-api/#custom-transitions","title":"Custom transitions","text":"It\u2019s simple to add your own transitions: call ScreenTransition
with a custom transitionModifier
. Use the available params (screen
, transition
and event
) to animate as needed.
@Composable\nfun MyCustomTransition(\n navigator: Navigator,\n modifier: Modifier = Modifier,\n content: ScreenTransitionContent\n) {\n ScreenTransition(\n navigator = navigator,\n modifier = modifier,\n content = content,\n transition = {\n val (initialScale, targetScale) = when (navigator.lastEvent) {\n StackEvent.Pop -> ExitScales\n else -> EnterScales\n }\n\n scaleIn(initialScale) with scaleOut(targetScale)\n }\n )\n}\n\nsetContent {\n Navigator(HomeScreen) { navigator ->\n MyCustomTransition(navigator) { screen ->\n screen.Content()\n }\n }\n}\n
Take a look at the source of the available transitions for working examples.
"},{"location":"transitions-api/#per-screen-transitions-experimental","title":"Per Screen transitions [Experimental]","text":"If you want to define a Enter and Exit transition for a specific Screen, you have a lot of options to do starting from 1.1.0-beta01 Voyager have a new experimental API for this purpose. To animate the content, we use transitions of the target screen in the case of push navigation, otherwise we use transitions of the initial screen
class ExampleSlideScreen : Screen, ScreenTransition {\n override val key: ScreenKey\n get() = uniqueScreenKey\n\n @Composable\n override fun Content() {\n ...\n }\n\n override fun enter(lastEvent: StackEvent): EnterTransition {\n return slideIn { size ->\n val x = if (lastEvent == StackEvent.Pop) -size.width else size.width\n IntOffset(x = x, y = 0)\n }\n }\n\n override fun exit(lastEvent: StackEvent): ExitTransition {\n return slideOut { size ->\n val x = if (lastEvent == StackEvent.Pop) size.width else -size.width\n IntOffset(x = x, y = 0)\n }\n }\n}\n
It\u2019s convenient to use Kotlin delegates for per-Screen transitions. For example, you can create a SlideTransition
and FadeTransition
classes:
class FadeTransition : ScreenTransition {\n\n override fun enter(lastEvent: StackEvent): EnterTransition {\n return fadeIn(tween(500, delayMillis = 500))\n }\n\n override fun exit(lastEvent: StackEvent): ExitTransition {\n return fadeOut(tween(500))\n }\n}\n\nclass SlideTransition : ScreenTransition {\n\n override fun enter(lastEvent: StackEvent): EnterTransition {\n return slideIn { size ->\n val x = if (lastEvent == StackEvent.Pop) -size.width else size.width\n IntOffset(x = x, y = 0)\n }\n }\n\n override fun exit(lastEvent: StackEvent): ExitTransition {\n return slideOut { size ->\n val x = if (lastEvent == StackEvent.Pop) size.width else -size.width\n IntOffset(x = x, y = 0)\n }\n }\n}\n
Then you can use them as delegates in your Screens:
class SlideScreen : Screen, ScreenTransition by SlideTransition() {\n\n @Composable\n override fun Content() {\n ...\n }\n}\n\nclass FadeScreen : Screen, ScreenTransition by FadeTransition() {\n\n @Composable\n override fun Content() {\n ...\n }\n}\n
Also you can use can pass your custom ScreenTransition
instance in ScreenTransition
function, it will be used for default animation.
setContent {\n Navigator(FadeScreen) { navigator ->\n ScreenTransition(\n navigator = navigator,\n defaultTransition = SlideTransition()\n )\n }\n}\n
The API works with any ScreenTransition API, you just need to provide one and the Per Screen transition should.
setContent {\n Navigator(HomeScreen) { navigator ->\n SlideTransition(navigator)\n }\n}\n
CrossfadeTransition
is not supported yet.
class PostListScreen : Screen {\n\n @Composable\n override fun Content() {\n val viewModel = viewModel<PostListViewModel>()\n // ...\n }\n}\n
By default Voyager provides its own LocalViewModelStoreOwner
and LocalSavedStateRegistryOwner
, that way you can safely create ViewModel
s without depending on Activity
or Fragment
.
Info
Voyager provides a similar implementation, the ScreenModel, which does the same as ViewModel
but also works with Compose Multiplatform.
Info
Source code here.
"},{"location":"android-viewmodel/hilt-integration/","title":"Hilt integration","text":"Success
To use the getViewModel
you should first import cafe.adriel.voyager:voyager-hilt
(see Setup).
Add @HiltViewModel
and @Inject
annotations to your ViewModel
.
@HiltViewModel\nclass HomeViewModel @Inject constructor() : ViewModel() {\n // ...\n}\n
Call getViewModel()
to get a new instance.
class HomeScreen : Screen {\n\n @Composable\n override fun Content() {\n val screenModel = getViewModel<HomeScreenModel>()\n // ...\n }\n}\n
"},{"location":"android-viewmodel/hilt-integration/#assistedinject","title":"@AssistedInject","text":"Currently there\u2019s no Assisted Injection support for Hilt ViewModels (issue).
"},{"location":"android-viewmodel/hilt-integration/#sample","title":"Sample","text":"Info
Sample code here.
"},{"location":"android-viewmodel/viewmodel-kmp/","title":"ViewModel KMP","text":"Since 1.1.0-beta01 we have introduce a experimental API for ViewModel KMP. It is under the package cafe.adriel.voyager:voyager-lifecycle-kmp
(see Setup).
You will need to call ProvideNavigatorLifecycleKMPSupport
before all Navigator
calls and it will be working out of the box.
@Composable\nfun MainView() {\n ProvideNavigatorLifecycleKMPSupport {\n Navigator(...)\n }\n}\n\nclass MyScreen : Screen {\n @Composable\n fun Content() {\n val myViewModel = viewModel { MyScreenViewModel() }\n }\n}\n
"},{"location":"android-viewmodel/viewmodel-kmp/#navigator-scoped-viewmodel","title":"Navigator scoped ViewModel","text":"Voyager 1.1.0-beta01 also have introduced the support for Navigator scoped ViewModel and Lifecycle. This will make easy to share a ViewModel cross screen of the same navigator.
class MyScreen : Screen {\n @Composable\n fun Content() {\n val myViewModel = navigatorViewModel { MyScreenViewModel() }\n }\n}\n
"},{"location":"android-viewmodel/viewmodel-kmp/#lifecycle-kmp","title":"Lifecycle KMP","text":"This version also brings the Lifecycle events for Screen lifecycle in KMP, now is possible to a generic third party API that listen to Lifecycle of a Screen in KMP.
"},{"location":"navigation/","title":"Navigation","text":"Success
To use the Navigator
you should first import cafe.adriel.voyager:voyager-navigator
(see Setup).
On Voyager, screens are just classes with a composable function as the entrypoint. To create one, you should implement the Screen
interface and override the Content()
composable function.
You can use data class
(if you need to send params), class
(if no param is required).
class PostListScreen : Screen {\n\n @Composable\n override fun Content() {\n // ...\n }\n}\n\ndata class PostDetailsScreen(val postId: Long) : Screen {\n\n @Composable\n override fun Content() {\n // ...\n }\n}\n
"},{"location":"navigation/#navigator","title":"Navigator","text":"Navigator
is a composable function deeply integrated with Compose internals. It\u2019ll manage the lifecyle, back press, state restoration and even nested navigation for you.
To start using it, just set the initial Screen
.
class SingleActivity : ComponentActivity() {\n\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n\n setContent {\n Navigator(HomeScreen)\n }\n }\n}\n
Use the LocalNavigator
to navigate to other screens. Take a look at the Stack API for the available operations.
class PostListScreen : Screen {\n\n @Composable\n override fun Content() {\n // ...\n }\n\n @Composable\n private fun PostCard(post: Post) {\n val navigator = LocalNavigator.currentOrThrow\n\n Card(\n modifier = Modifier.clickable { \n navigator.push(PostDetailsScreen(post.id))\n // Also works:\n // navigator push PostDetailsScreen(post.id)\n // navigator += PostDetailsScreen(post.id)\n }\n ) {\n // ...\n }\n }\n}\n
If part of your UI is shared between screens, like the TopAppBar
or BottomNavigation
, you can easily reuse them with Voyager.
@Composable\noverride fun Content() {\n Navigator(HomeScreen) { navigator ->\n Scaffold(\n topBar = { /* ... */ },\n content = { CurrentScreen() },\n bottomBar = { /* ... */ }\n )\n }\n}\n
{% hint style=\u201dwarning\u201d %} You should use CurrentScreen()
instead of navigator.lastItem.Content()
, because it will save the Screen\u2019s subtree for you (see SaveableStateHolder). {% endhint %}
Info
Source code here.
"},{"location":"navigation/bottomsheet-navigation/","title":"BottomSheet navigation","text":"Success
To use the BottomSheetNavigator
you should first import cafe.adriel.voyager:voyager-bottom-sheet-navigator
(see Setup).
Voyager provides a specialized navigator for ModalBottomSheetLayout
: the BottomSheetNavigator
.
Call it and set the back layer content. The BottomSheet content (or the front layer) will be set on demand.
setContent {\n BottomSheetNavigator {\n BackContent()\n }\n}\n
You can also use the default Navigator
to navigate on your back layer content.
setContent {\n BottomSheetNavigator {\n Navigator(BackScreen())\n }\n}\n
The BottomSheetNavigator
accepts the same params as ModalBottomSheetLayout
.
@Composable\npublic fun BottomSheetNavigator(\n modifier: Modifier = Modifier,\n hideOnBackPress: Boolean = true,\n scrimColor: Color = ModalBottomSheetDefaults.scrimColor,\n sheetShape: Shape = MaterialTheme.shapes.large,\n sheetElevation: Dp = ModalBottomSheetDefaults.Elevation,\n sheetBackgroundColor: Color = MaterialTheme.colors.surface,\n sheetContentColor: Color = contentColorFor(sheetBackgroundColor),\n // ...\n)\n
Use the LocalBottomSheetNavigator
to show and hide the BottomSheet.
class BackScreen : Screen {\n\n @Composable\n override fun Content() {\n val bottomSheetNavigator = LocalBottomSheetNavigator.current\n\n Button(\n onClick = { \n bottomSheetNavigator.show(FrontScreen())\n }\n ) {\n Text(text = \"Show BottomSheet\")\n }\n }\n}\n
"},{"location":"navigation/bottomsheet-navigation/#sample","title":"Sample","text":"Info
Source code here.
"},{"location":"navigation/multi-module-navigation/","title":"Multi-module navigation","text":"Voyager has built-in support for multi-module navigation. Its API is based on great DI frameworks like Koin and Kodein, so should be familiar to use.
Suppose we have the following modules:
app
: the entrypoint of our app, contains the Activityfeature-home
: contains the root screen (HomeScreen
)feature-posts
: contains screens related to the posts feature (ListScreen
and DetailsScreen
)navigation
: contains the screen providers used to navigate between modules, both feature-home
and feature-posts
imports itTo navigate from HomeScreen
to the screens on feature-posts
module (ListScreen
and DetailsScreen
), we first need to provide them. The navigation
module should declare all shared screens in the app, use the ScreenProvider
interface for that.
sealed class SharedScreen : ScreenProvider {\n object PostList : SharedScreen()\n data class PostDetails(val id: String) : SharedScreen()\n}\n
Now use the ScreenRegistry
to register these providers. This should be done in yourApplication
class.
class MyApp : Application() {\n\n override fun onCreate() {\n super.onCreate()\n\n ScreenRegistry {\n register<SharedScreen.PostList> {\n ListScreen()\n }\n register<SharedScreen.PostDetails> { provider ->\n DetailsScreen(id = provider.id)\n }\n }\n }\n}\n
You can also create a screenModule
into your feature module, that way your Application
class won\u2019t be bloated by too many declarations.
val featurePostsScreenModule = screenModule {\n register<SharedScreen.PostList> {\n ListScreen()\n }\n register<SharedScreen.PostDetails> { provider ->\n DetailsScreen(id = provider.id)\n }\n}\n
app/../MyApp.ktoverride fun onCreate() {\n super.onCreate()\n\n ScreenRegistry {\n featurePostsScreenModule()\n }\n}\n
Finally, call rememberScreen()
(inside a composable function) or ScreenRegistry.get()
to access your screens.
class HomeScreen : Screen {\n\n @Composable\n override fun Content() {\n val navigator = LocalNavigator.currentOrThrow\n val postListScreen = rememberScreen(SharedScreen.PostList)\n val postDetailsScreen = rememberScreen(SharedScreen.PostDetails(id = postId))\n\n // navigator.push(postListScreen)\n // navigator.push(postDetailsScreen)\n }\n}\n
"},{"location":"navigation/multi-module-navigation/#sample","title":"Sample","text":"Info
Source code here.
"},{"location":"navigation/nested-navigation/","title":"Nested navigation","text":""},{"location":"navigation/nested-navigation/#nested-navigators","title":"Nested Navigators","text":"Going a little further, it\u2019s possible to have nested navigators. The Navigator
has a level
property (so you can check how deeper your are) and can have a parent
navigator (if you need to interact with it).
setContent {\n Navigator(ScreenA) { navigator0 ->\n println(navigator.level)\n // 0\n println(navigator.parent == null)\n // true\n Navigator(ScreenB) { navigator1 ->\n println(navigator.level)\n // 1\n println(navigator.parent == navigator0)\n // true\n Navigator(ScreenC) { navigator2 ->\n println(navigator.level)\n // 2\n println(navigator.parent == navigator1)\n // true\n }\n }\n }\n}\n
Another operation is the popUntilRoot()
, it will recursively pop all screens starting from the leaf navigator until the root one.
Info
Source code here.
"},{"location":"navigation/tab-navigation/","title":"Tab navigation","text":"Success
To use the TabNavigator
you should first import cafe.adriel.voyager:voyager-tab-navigator
(see Setup).
Voyager provides a specialized navigator for tabs : the TabNavigator
.
The Tab
interface, like the Screen
, has a Content()
composable function, but also requires a TabOptions
.
object HomeTab : Tab {\n\n override val options: TabOptions\n @Composable\n get() {\n val title = stringResource(R.string.home_tab)\n val icon = rememberVectorPainter(Icons.Default.Home)\n\n return remember {\n TabOptions(\n index = 0u,\n title = title,\n icon = icon\n )\n }\n }\n\n @Composable\n override fun Content() {\n // ...\n }\n}\n
Info
Since tabs aren\u2019t usually reused, its OK to create them as object
.
The TabNavigator
unlike the Navigator
:
current
propertyYou can use it with a Scaffold to easily create the UI for your tabs.
setContent {\n TabNavigator(HomeTab) {\n Scaffold(\n content = { \n CurrentTab() \n },\n bottomBar = {\n BottomNavigation {\n TabNavigationItem(HomeTab)\n TabNavigationItem(FavoritesTab)\n TabNavigationItem(ProfileTab)\n }\n }\n )\n }\n}\n
Warning
Like theCurrentScreen()
, you should use CurrentTab
instead of tabNavigator.current.Content()
, because it will save the Tab\u2019s subtree for you (see SaveableStateHolder).
Use the LocalTabNavigator
to get the current TabNavigator
, and current
to get and set the current tab.
@Composable\nprivate fun RowScope.TabNavigationItem(tab: Tab) {\n val tabNavigator = LocalTabNavigator.current\n\n BottomNavigationItem(\n selected = tabNavigator.current == tab,\n onClick = { tabNavigator.current = tab },\n icon = { Icon(painter = tab.icon, contentDescription = tab.title) }\n )\n}\n
"},{"location":"navigation/tab-navigation/#sample","title":"Sample","text":"Info
Source code here.
"},{"location":"navigation/tab-navigation/#tabnavigator-nested-navigator","title":"TabNavigator + Nested Navigator","text":"For more complex use cases, when each tab should have its own independent navigation, like the Youtube app, you can combine the TabNavigator
with multiple Navigator
s.
Let\u2019s go back to the Tab navigation example.
setContent {\n TabNavigator(HomeTab) {\n // ...\n }\n}\n
But now, the HomeTab
will have it\u2019s own Navigator
.
object HomeTab : Screen {\n\n @Composable\n override fun Content() {\n Navigator(PostListScreen())\n }\n}\n
That way, we can use the LocalNavigator
to navigate deeper into HomeTab
, or the LocalTabNavigator
to switch between tabs.
class PostListScreen : Screen {\n\n @Composable\n private fun GoToPostDetailsScreenButton(post: Post) {\n val navigator = LocalNavigator.currentOrThrow\n\n Button(\n onClick = { navigator.push(PostDetailsScreen(post.id)) }\n )\n }\n\n @Composable\n private fun GoToProfileTabButton() {\n val tabNavigator = LocalTabNavigator.current\n\n Button(\n onClick = { tabNavigator.current = ProfileTab }\n )\n }\n}\n
"},{"location":"screenmodel/","title":"ScreenModel","text":"Success
The ScreenModel API is a part of the modulecafe.adriel.voyager:voyager-screenmodel
(see Setup).
ScreenModel
is just like a ViewModel: designed to store and manage UI-related data in a lifecycle conscious way. It also allows data to survive configuration changes such as screen rotations.
Unlike ViewModel
, ScreenModel
is just an interface. It\u2019s also Android independent and doesn\u2019t requires an Activity
or Fragment
to work.
class HomeScreenModel : ScreenModel {\n\n // Optional\n override fun onDispose() {\n // ...\n }\n}\n
Info
ScreenModel
is integrated with Coroutines, RxJava, LiveData, Koin, Kodein and Hit!
By design, it\u2019s only possible to create a ScreenModel
instance inside a Screen
. Call rememberScreenModel
and provide a factory lambda.
class HomeScreen : Screen {\n\n @Composable\n override fun Content() {\n val screenModel = rememberScreenModel { HomeScreenModel() }\n // ...\n }\n}\n
If you need to have multiple instances of the same ScreenModel
for the same Screen
, add a tag to differentiate them.
val screenModel = rememberScreenModel(tag = \"CUSTOM_TAG\") { HomeScreenModel() }\n
"},{"location":"screenmodel/#sample","title":"Sample","text":"Info
Source code here.
"},{"location":"screenmodel/#navigator-scoped-screenmodel","title":"Navigator scoped ScreenModel","text":"Success
TherememberNavigatorScreenModel
are part of the navigator library.
Starting from 1.0.0rc08
by using the new Navigator extension called rememberNavigatorScreenModel
is possible to have a ScreenModel that is shared cross all Screens from a Navigator and when the Navigator leaves the Composition the ScreenModel is disposed.
class HomeScreen : Screen {\n\n @Composable\n override fun Content() {\n val navigator = LocalNavigator.currentOrThrow\n val screenModel = navigator.rememberNavigatorScreenModel { HomeScreenModel() }\n // ...\n }\n}\n
Info
Each DI library we provide a extension out of the box it is also provided support for Navigator scoped ScreenModel. See Koin, Kodein and Hit!
"},{"location":"screenmodel/coroutines-integration/","title":"Coroutines integration","text":"Success
The screenModelScope
and StateScreenModel
are part of the core library.
The ScreenModel
provides a screenModelScope
property. It\u2019s canceled automatically when the ScreenModel
is disposed.
class PostDetailsScreenModel(\n private val repository: PostRepository\n) : ScreenModel {\n\n fun getPost(id: String) {\n screenModelScope.launch {\n val post = repository.getPost(id)\n // ...\n }\n }\n}\n
"},{"location":"screenmodel/coroutines-integration/#state-aware-screenmodel","title":"State-aware ScreenModel","text":"If your ScreenModel
needs to provide a state, use the StateScreenModel
. Set the initial state on the constructor and use mutableState
to change the current state.
class PostDetailsScreenModel(\n private val repository: PostRepository\n) : StateScreenModel<PostDetailsScreenModel.State>(State.Init) {\n\n sealed class State {\n object Init : State()\n object Loading : State()\n data class Result(val post: Post) : State()\n }\n\n fun getPost(id: String) {\n coroutineScope.launch {\n mutableState.value = State.Loading\n mutableState.value = State.Result(post = repository.getPost(id))\n }\n }\n}\n
In your screen use state.collectAsState()
and handle the current state.
class PostDetailsScreen : Screen {\n\n @Composable\n override fun Content() {\n val screenModel = rememberScreenModel<PostDetailsScreenModel>()\n val state by screenModel.state.collectAsState()\n\n when (state) {\n is State.Loading -> LoadingContent()\n is State.Result -> PostContent(state.post)\n }\n }\n}\n
"},{"location":"screenmodel/coroutines-integration/#sample","title":"Sample","text":"Info
Sample code here.
"},{"location":"screenmodel/coroutines-integration/#desktop-note","title":"Desktop Note","text":"Info
If you are targeting Desktop, you should provide the dependency org.jetbrains.kotlinx:kotlinx-coroutines-swing
, the screenModelScope
depends on Dispatchers.Main
provided by this library on Desktop. We don\u2019t include it because this library is incompatible with IntelliJ Plugin, see. If you are targeting Desktop for IntelliJ plugins, this library does not require to be provided.
Success
To use the getScreenModel
you should first import cafe.adriel.voyager:voyager-hilt
(see Setup).
Add @Inject
annotation to your ScreenModel
.
class HomeScreenModel @Inject constructor() : ScreenModel {\n // ...\n}\n
Call getScreenModel()
to get a new instance.
class HomeScreen : AndroidScreen() {\n\n @Composable\n override fun Content() {\n val screenModel = getScreenModel<HomeScreenModel>()\n // ...\n }\n}\n
"},{"location":"screenmodel/hilt-integration/#assistedinject","title":"@AssistedInject","text":"Add @AssistedInject
annotation to your ScreenModel
and provide a ScreenModelFactory
annotated with @AssistedFactory
.
class PostDetailsScreenModel @AssistedInject constructor(\n @Assisted val postId: Long\n) : ScreenModel {\n\n @AssistedFactory\n interface Factory : ScreenModelFactory {\n fun create(postId: Long): PostDetailsScreenModel\n }\n}\n
Call getScreenModel()
and use your factory to create a new instance.
data class PostDetailsScreen(val postId: Long): AndroidScreen() {\n\n @Composable\n override fun Content() {\n val screenModel = getScreenModel<PostDetailsScreenModel, PostDetailsScreenModel.Factory> { factory ->\n factory.create(postId)\n }\n // ...\n }\n}\n
"},{"location":"screenmodel/hilt-integration/#sample","title":"Sample","text":"Info
Sample code here.
"},{"location":"screenmodel/hilt-integration/#navigator-scoped-screenmodel","title":"Navigator scoped ScreenModel","text":"class HomeScreen : Screen {\n\n @Composable\n override fun Content() {\n val navigator = LocalNavigator.currentOrThrow\n val screenModel = navigator.getNavigatorScreenModel<HomeScreenModel>()\n // ...\n }\n}\n
"},{"location":"screenmodel/kodein-integration/","title":"Kodein integration","text":"Success
To use the rememberScreenModel
you should first import cafe.adriel.voyager:voyager-kodein
(see Setup).
Declare your ScreenModel
s using the bindProvider
bind.
val homeModule = DI.Module(name = \"home\") {\n bindProvider { HomeScreenModel() } \n}\n
Call rememberScreenModel()
to get a new instance.
class HomeScreen : Screen {\n\n @Composable\n override fun Content() {\n val screenModel = rememberScreenModel<HomeScreenModel>()\n // ...\n }\n}\n
"},{"location":"screenmodel/kodein-integration/#sample","title":"Sample","text":"Info
Sample code here.
"},{"location":"screenmodel/kodein-integration/#navigator-scoped-screenmodel","title":"Navigator scoped ScreenModel","text":"class HomeScreen : Screen {\n\n @Composable\n override fun Content() {\n val navigator = LocalNavigator.currentOrThrow\n val screenModel = navigator.rememberNavigatorScreenModel<HomeScreenModel>()\n // ...\n }\n}\n
"},{"location":"screenmodel/koin-integration/","title":"Koin integration","text":"Success
To use the getScreenModel
you should first import cafe.adriel.voyager:voyager-koin
(see Setup).
Warning
Since 1.1.0-alpha04 we have rename the getScreenModel
to koinScreenModel
, this is a change to follow Koin Compose naming schema. The previous getScreenModel
is deprecated and will be removed on 1.1.0
Declare your ScreenModel
s using the factory
component.
val homeModule = module {\n factory { HomeScreenModel() } \n}\n
Call getScreenModel()
to get a new instance.
class HomeScreen : Screen {\n\n @Composable\n override fun Content() {\n val screenModel = getScreenModel<HomeScreenModel>()\n // ...\n }\n}\n
"},{"location":"screenmodel/koin-integration/#sample","title":"Sample","text":"Info
Sample code here.
"},{"location":"screenmodel/koin-integration/#navigator-scoped-screenmodel","title":"Navigator scoped ScreenModel","text":"class HomeScreen : Screen {\n\n @Composable\n override fun Content() {\n val navigator = LocalNavigator.currentOrThrow\n val screenModel = navigator.getNavigatorScreenModel<HomeScreenModel>()\n // ...\n }\n}\n
"},{"location":"screenmodel/livedata-integration/","title":"LiveData integration","text":"Success
To use the LiveScreenModel
you should first import cafe.adriel.voyager:voyager-livedata
(see Setup).
If your ScreenModel
needs to provide a state, use the LiveScreenModel
. Set the initial state on the constructor and use mutableState
to change the current state.
class PostDetailsScreenModel(\n private val repository: PostRepository\n) : LiveScreenModel<PostDetailsScreenModel.State>(State.Init) {\n\n sealed class State {\n object Init : State()\n object Loading : State()\n data class Result(val post: Post) : State()\n }\n\n fun getPost(id: String) {\n coroutineScope.launch {\n val result = State.Result(post = repository.getPost(id))\n mutableState.postValue(result)\n }\n }\n}\n
In your screen use state.observeAsState()
and handle the current state.
class PostDetailsScreen : Screen {\n\n @Composable\n override fun Content() {\n val screenModel = rememberScreenModel<PostDetailsScreenModel>()\n val state by screenModel.state.observeAsState()\n\n when (state) {\n is State.Loading -> LoadingContent()\n is State.Result -> PostContent(state.post)\n }\n }\n}\n
"},{"location":"screenmodel/livedata-integration/#sample","title":"Sample","text":"Info
Sample code here.
"},{"location":"screenmodel/rxjava-integration/","title":"RxJava integration","text":"Success
To use the disposables
and RxScreenModel
you should first import cafe.adriel.voyager:voyager-rxjava
(see Setup).
The ScreenModel
provides a disposables
property. It\u2019s cleared automatically when the ScreenModel
is disposed.
class PostDetailsScreenModel(\n private val repository: PostRepository\n) : ScreenModel {\n\n fun getPost(id: String) {\n repository.getPost(id)\n .subscribe { post -> /* ... */ }\n .let(disposables::add)\n }\n}\n
"},{"location":"screenmodel/rxjava-integration/#state-aware-screenmodel","title":"State-aware ScreenModel","text":"If your ScreenModel
needs to provide a state, use the RxScreenModel
. Use mutableState
to change the current state.
class PostDetailsScreenModel(\n private val repository: PostRepository\n) : RxScreenModel<PostDetailsScreenModel.State>() {\n\n sealed class State {\n object Init : State()\n object Loading : State()\n data class Result(val post: Post) : State()\n }\n\n fun getPost(id: String) {\n repository.getPost(id)\n .doOnSubscribe { mutableState.onNext(State.Loading) }\n .subscribe { post -> mutableState.onNext(State.Result(post)) }\n .let(disposables::add)\n }\n}\n
In your screen use state.subscribeAsState()
and handle the current state.
class PostDetailsScreen : Screen {\n\n @Composable\n override fun Content() {\n val screenModel = rememberScreenModel<PostDetailsScreenModel>()\n val state by screenModel.state.subscribeAsState(initial = State.Init)\n\n when (state) {\n is State.Loading -> LoadingContent()\n is State.Result -> PostContent(state.post)\n }\n }\n}\n
"},{"location":"screenmodel/rxjava-integration/#sample","title":"Sample","text":"Info
Sample code here.
"}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Overview","text":""},{"location":"#voyager-compose-on-warp-speed","title":"Voyager: Compose on Warp Speed","text":"A multiplatform navigation library built for, and seamlessly integrated with, Jetpack Compose.
Create scalable Single-Activity apps powered by a pragmatic API:
class HomeScreenModel : ScreenModel {\n // ...\n}\n\nclass HomeScreen : Screen {\n\n @Composable\n override fun Content() {\n val screenModel = rememberScreenModel { HomeScreenModel() }\n // ...\n }\n}\n\nclass SingleActivity : ComponentActivity() {\n\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n\n setContent {\n Navigator(HomeScreen())\n }\n }\n}\n
Turn on the Warp Drive and enjoy the voyage \ud83d\udd96
"},{"location":"#features","title":"Features","text":"By default, Voyager will handle back presses but you can override its behavior. Use the onBackPressed
to manually handle it: return true
to pop the current screen, or false otherwise. To disable, just set to null
.
setContent {\n Navigator(\n initialScreen = HomeScreen,\n onBackPressed = { currentScreen ->\n false // won't pop the current screen\n // true will pop, default behavior\n }\n // To disable:\n // onBackPressed = null\n )\n}\n
"},{"location":"community-projects/","title":"Community Projects Catalog","text":""},{"location":"community-projects/#open-source-libraries-with-voyager-extensions","title":"Open source libraries with Voyager extensions","text":"Warning
Currently Voyager does not provided a built in solution to handle Deeplink and URIs. see #149 and #382
You can initialize the Navigator
with multiple screens, that way, the first visible screen will be the last one and will be possible to return (pop()
) to the previous screens.
val postId = getPostIdFromIntent()\n\nsetContent {\n Navigator(\n HomeScreen,\n PostListScreen(),\n PostDetailsScreen(postId)\n )\n}\n
"},{"location":"faq/","title":"Faq","text":"iOS Swipe Back support { #ios-swipeback }
Voyager does not have a built in support for swipe back yet, we are not 100% conformable with all solutions that have out there and we think we will better of using a community made solution by copying to your code base and be able to change as you want your app to behave.
See this issue each for community build solutions.
Alternatively, we can also discuss in the future a community build solution using NavigationController
under the hood like compose-cupertino
have implemented for Decompose.
Support for predictive back animations { #predictive-back }
Voyager does not have a built in support for predictive back yet, but as well as iOS Swipe Back, the community have build extensions, and snippets with support, see #223 and 144.
Support for result passing between screens { #result-passing }
Voyager does not have a built in support for swipe back yet, we are not 100% conformable with all solutions that have out there, we encourage to use community made solutions by copying to your code base and being free to extend as your code base needs.
See #128 comments for community solutions.
Deeplink support
"},{"location":"lifecycle/","title":"Lifecycle","text":"** Experimental API
Inside a Screen
, you can call LifecycleEffectOnce
to execute a block of code the first time the Screen appears, and optionally define a onDispose
callback for when the Screen is leaving/removed from the Stack:
class PostListScreen : Screen {\n\n @Composable\n override fun Content() {\n LifecycleEffectOnce {\n screenModel.initSomething()\n onDispose {\n // Do something when the screen is leaving/removed from the Stack\n }\n }\n\n // ...\n }\n}\n
"},{"location":"lifecycle/#screenlifecycleowner","title":"ScreenLifecycleOwner","text":" With ScreenLifecycleProvider
interface you can provide a special ScreenLifecycleOwner
that can react when the Screen leaves the Stack
class MyScreenLifecycleOwner : ScreenLifecycleOwner {\n override fun onDispose(screen: Screen) {\n println(\"My ${screen.key} is being disposed\")\n }\n}\n\n\ndata object MyScreen : Screen, ScreenLifecycleProvider {\n @Composable\n fun Content() {\n ...\n }\n\n public override fun getLifecycleOwner(): ScreenLifecycleOwner =\n ScreenLifecycleOwner()\n}\n
"},{"location":"lifecycle/#extending-the-lifecycle-lazily","title":"Extending the Lifecycle lazily","text":"You can access directly the ScreenLifecycleStore
to add a ScreenDisposable
implementation lazily in a lifecycle aware custom API.
If you for example have a custom dependency that should be notified when the Screen is disposed you can use the ScreenLifecycleStore
directly. For example, the Screen Model and ScreenModelStore APIs rely on this API to Store the ScreenModel instance and the Screen owns the ScreenModel instance, when the Screen leaves the Stack, the ScreenModelStore, that implements ScreenDisposable
is notified and can dispose the ScreenModel.
Let\u2019s imagine a dependency called MyDependency
that holds and provides a State for a Screen while it is on the Navigator stack and we want to notify MyDependency
when the Screen leaves the Stack.
class MyCustomAPIWithDisposable(\n private val myDependency: MyDependency\n) : ScreenDisposable {\n public override fun onDispose(screen: Screen) {\n myDependency.disposeStateForScreen(screen)\n }\n}\n\n@Composable\nfun rememberMyDependency(): MyDependency {\n val navigator = LocalNavigator.currentOrThrow\n val myDependency by getMyDependecy() // getting from DI\n remember(myDependency) {\n myDependency.provideStateForScreen(navigator.lastItem)\n ScreenLifecycleStore.get(navigator.lastItem) {\n MyCustomAPIWithDisposable(myDependency)\n }\n }\n\n return myDependency\n}\n
"},{"location":"lifecycle/#screendisposable-for-all-screens","title":"ScreenDisposable for all Screens","text":"You can also provide a ScreenLifecycleOwner
for all Screen in the stack of a Navigator easily by in the Navigator composable start to listen to lastItem
for registering at ScreenLifecycleStore.
Let\u2019s reuse the MyScreenLifecycleOwner
from the example above and provide it to all screens in the navigator.
class MyScreenLifecycleOwner : ScreenDisposable {\n override fun onDispose(screen: Screen) {\n println(\"My ${screen.key} is being disposed\")\n }\n}\n\n@Composable\nfun YourAppComposable() {\n Navigator(...) { navigator ->\n remember(navigator.lastItem) {\n ScreenLifecycleStore.get(navigator.lastItem) {\n MyScreenLifecycleOwner()\n }\n }\n CurrentScreen()\n }\n}\n
"},{"location":"migration-to-1.0.0/","title":"Migration to 1.0.0","text":""},{"location":"migration-to-1.0.0/#screenmodel","title":"ScreenModel","text":""},{"location":"migration-to-1.0.0/#new-module","title":"New Module","text":"Now the ScreenModel API has its own module and no longer are available in voyager-core
, if you are using ScreenModel you should declare the dependency cafe.adriel.voyager:voyager-screenmodel
(see Setup).
Since 1.0.0-rc08 we have introduced the ScreenModel scoped at Navigator Lifecycle, now the API is no longer marked as Experimental.
"},{"location":"migration-to-1.0.0/#deprecation-cycle","title":"Deprecation cycle","text":"Since 1.0.0-rc08
we have renamed the extension coroutineScope
to screenModelScope
, now it was removed from 1.0.0, if you are still using it, just replace with screenModelScope.
The module voyager-androidx
and AndroidScreen
was removed! Since 1.0.0-rc06
we have introduced a new API called NavigatorScreenLifecycleProvider
that provides by default the previous behavior of AndroidScreenLifecycleOwner
on Android target for all Screen.
Important notice: AndroidScreen, different from Screen, it holds the Screen.key
as a uniqueScreenKey
, this is a pretty common requirement, to avoid issues and weird behaviors, we recommend continuing to specify a uniqueScreenKey
if you are not, we also recommend creating a abstract class UniqueScreen
to replace your AndroidScreen
implementation.
abstract class UniqueScreen : Screen {\n override val key: ScreenKey = uniqueScreenKey\n}\n
"},{"location":"migration-to-1.0.0/#apis-promote-to-stable","title":"APIs promote to Stable","text":"NavigatorLifecycleStore
and NavigatorDisposable
TabDisposable
ScreenLifecycleStore.get
: Use register
or new get
function instead.Stack.lastOrNull
: Use lastItemOrNull
insteadNavigator.last
: Use lastItem
insteadScreenModel.coroutineScope
: Use screenModelScope
instead ScreenModelStore.remove
: Use onDispose
instead.Tab.title
: Use options
instead.Tab.icon
: Use options
instead.Add Maven Central to your repositories if needed
repositories {\n mavenCentral()\n}\n
2. Add the desired dependencies to your module\u2019s build.gradle
file dependencies {\n val voyagerVersion = \"1.1.0-beta02\"\n\n // Multiplatform\n\n // Navigator\n implementation(\"cafe.adriel.voyager:voyager-navigator:$voyagerVersion\")\n\n // Screen Model\n implementation(\"cafe.adriel.voyager:voyager-screenmodel:$voyagerVersion\")\n\n // BottomSheetNavigator\n implementation(\"cafe.adriel.voyager:voyager-bottom-sheet-navigator:$voyagerVersion\")\n\n // TabNavigator\n implementation(\"cafe.adriel.voyager:voyager-tab-navigator:$voyagerVersion\")\n\n // Transitions\n implementation(\"cafe.adriel.voyager:voyager-transitions:$voyagerVersion\")\n\n // Koin integration\n implementation(\"cafe.adriel.voyager:voyager-koin:$voyagerVersion\")\n\n // Android\n\n // Hilt integration\n implementation(\"cafe.adriel.voyager:voyager-hilt:$voyagerVersion\")\n\n // LiveData integration\n implementation(\"cafe.adriel.voyager:voyager-livedata:$voyagerVersion\")\n\n // Desktop + Android\n\n // Kodein integration\n implementation(\"cafe.adriel.voyager:voyager-kodein:$voyagerVersion\")\n\n // RxJava integration\n implementation(\"cafe.adriel.voyager:voyager-rxjava:$voyagerVersion\")\n}\n
[versions]\nvoyager = \"1.1.0-beta02\"\n\n[libraries]\nvoyager-navigator = { module = \"cafe.adriel.voyager:voyager-navigator\", version.ref = \"voyager\" }\nvoyager-screenModel = { module = \"cafe.adriel.voyager:voyager-screenmodel\", version.ref = \"voyager\" }\nvoyager-bottomSheetNavigator = { module = \"cafe.adriel.voyager:voyager-bottom-sheet-navigator\", version.ref = \"voyager\" }\nvoyager-tabNavigator = { module = \"cafe.adriel.voyager:voyager-tab-navigator\", version.ref = \"voyager\" }\nvoyager-transitions = { module = \"cafe.adriel.voyager:voyager-transitions\", version.ref = \"voyager\" }\nvoyager-koin = { module = \"cafe.adriel.voyager:voyager-koin\", version.ref = \"voyager\" }\nvoyager-hilt = { module = \"cafe.adriel.voyager:voyager-hilt\", version.ref = \"voyager\" }\nvoyager-kodein = { module = \"cafe.adriel.voyager:voyager-kodein\", version.ref = \"voyager\" }\nvoyager-rxjava = { module = \"cafe.adriel.voyager:voyager-rxjava\", version.ref = \"voyager\" }\n
Current version here.
"},{"location":"setup/#platform-compatibility","title":"Platform compatibility","text":"Multiplatform targets: Android, iOS, Desktop, Mac Native, Web.
Android Desktop Multiplatform voyager-navigator voyager-screenModel voyager-bottom-sheet-navigator voyager-tab-navigator voyager-transitions voyager-koin voyager-kodein voyager-lifecycle-kmp voyager-hilt voyager-rxjava voyager-livedata"},{"location":"stack-api/","title":"Stack API","text":""},{"location":"stack-api/#snapshotstatestack","title":"SnapshotStateStack","text":"Voyager is backed by a SnapshotStateStack:
You will use it to navigate forward (push
, replace
, replaceAll
) and backwards (pop
, popAll
, popUntil
), but the SnapshotStateStack
can also be used as a regular collection outside the Navigator
.
val stack = mutableStateStackOf(\"\ud83c\udf47\", \"\ud83c\udf49\", \"\ud83c\udf4c\", \"\ud83c\udf50\", \"\ud83e\udd5d\", \"\ud83c\udf4b\")\n// \ud83c\udf47, \ud83c\udf49, \ud83c\udf4c, \ud83c\udf50, \ud83e\udd5d, \ud83c\udf4b\n\nstack.lastItemOrNull\n// \ud83c\udf4b\n\nstack.push(\"\ud83c\udf4d\")\n// \ud83c\udf47, \ud83c\udf49, \ud83c\udf4c, \ud83c\udf50, \ud83e\udd5d, \ud83c\udf4b, \ud83c\udf4d\n\nstack.pop()\n// \ud83c\udf47, \ud83c\udf49, \ud83c\udf4c, \ud83c\udf50, \ud83e\udd5d, \ud83c\udf4b\n\nstack.popUntil { it == \"\ud83c\udf50\" }\n// \ud83c\udf47, \ud83c\udf49, \ud83c\udf4c, \ud83c\udf50\n\nstack.replace(\"\ud83c\udf53\")\n// \ud83c\udf47, \ud83c\udf49, \ud83c\udf4c, \ud83c\udf53\n\nstack.replaceAll(\"\ud83c\udf52\")\n// \ud83c\udf52\n
You can also create a SnapshotStateStack
through rememberStateStack()
, it will restore the values after Activity recreation.
Whenever the content changes, the SnapshotStateStack
will emit a StackEvent
. Use the stack.lastEvent
to get the most recent one.
The available events are:
Push
: whenever push
is calledReplace
: whenever replace
and replaceAll
are calledPop
: whenever pop
and popAll
are calledIdle
: default eventThis is very useful for deciding which transition to make.
"},{"location":"stack-api/#sample","title":"Sample","text":"Info
Source code here.
"},{"location":"state-restoration/","title":"State restoration","text":"Voyager by default expect that it screens can be stored inside a Bundle. This means both Java Serializable and Parcelable are supported. By default all Voyager Screen is Java Serializable this means that Screen can be restored if all parameters are Java Serializable.
"},{"location":"state-restoration/#java-serializable","title":"Java Serializable","text":"// \u2714\ufe0f DO\ndata class Post(/*...*/) : Serializable\n\ndata class ValidScreen(\n val userId: UUID, // Built-in serializable types\n val post: Post // Your own serializable types\n) : Screen {\n // ...\n}\n\n// \ud83d\udeab DON'T\nclass Post(/*...*/)\n\ndata class InvalidScreen(\n val context: Context, // Built-in non-serializable types\n val post: Post, // Your own non-serializable types\n val parcelable: SomeParcelable // Android Parcelable is not Java Serializable by default\n) : Screen {\n // ...\n}\n
Not only the params, but the properties will also be restored, so the same rule applies.
// \u2714\ufe0f DO\nclass ValidScreen : Screen {\n\n // Serializable properties\n val tag = \"ValidScreen\"\n\n // Lazily initialized serializable types\n val randomId by lazy { UUID.randomUUID() }\n}\n\n// \ud83d\udeab DON'T\nclass InvalidScreen : Screen {\n\n // Non-serializable properties\n val postService = PostService()\n}\n
"},{"location":"state-restoration/#android-parcelables","title":"Android Parcelables","text":"// \u2714\ufe0f DO\n@Parcelize\ndata class Post(/*...*/) : Parcelable\n\n@Parcelize\ndata class ValidScreen(\n val post: Post // Your own parcelable types\n) : Screen, Parcelable {\n // ...\n}\n\n// \ud83d\udeab DON'T\nclass Post(/*...*/)\n\n@Parcelize\ndata class InvalidScreen(\n val context: Context, // Built-in non-parcelable types\n val post: Post, // Your own non-parcelable types\n val serializable: SomeSerializable // Java Serializable are not Android Parcelable by default\n) : Screen, Parcelable {\n // ...\n}\n
"},{"location":"state-restoration/#enforcing-android-parcelable-on-your-screens","title":"Enforcing Android Parcelable on your screens","text":"You can build your own Screen type for enforcing in at compile time that all yours screens should be Parcelable by creating an interface that implement Parcelable.
interface ParcelableScreen : Screen, Parcelable\n\n// Compile\n@Parcelize\ndata class Post(/*...*/) : Parcelable\n\n@Parcelize\ndata class ValidScreen(\n val post: Post\n) : ParcelableScreen {\n // ...\n}\n\n// Not compile\ndata class Post(/*...*/)\n\n@Parcelize\ndata class ValidScreen(\n val post: Post\n) : ParcelableScreen {\n // ...\n}\n
Starting from version 1.0.0-rc05 you can specify a custom NavigatorSaver to enforce that all Screen is Parcelable by using parcelableNavigatorSaver
.
CompositionLocalProvider(\n LocalNavigatorSaver provides parcelableNavigatorSaver()\n) {\n Navigator(...) {\n ...\n }\n}\n
"},{"location":"state-restoration/#multiplatform-state-restoration","title":"Multiplatform state restoration","text":"When working in a Multiplatform project and sharing the Parameters models with other platforms, your types required to be serializable in a Bundle if you are targeting Android, the easiest way is defining in common code a JavaSerializable
interface that on Android only would implement java.io.Serialiable
, see example below.
// commonMain - module core\nexpect interface JavaSerializable\n\ndata class Post(/*...*/) : JavaSerializable\n\n// androidMain - module core\nactual typealias JavaSerializable = java.io.Serializable\n\n// non AndroidMain (ios, web, etc) - module core\nactual interface JavaSerializable\n\n// android ui module or compose multiplatform module\ndata class ValidScreen(\n val post: Post\n) : Screen\n
"},{"location":"state-restoration/#dependency-injection","title":"Dependency Injection","text":"If you want to inject dependencies through a DI framework, make sure it supports Compose, like Koin and Kodein.
// \u2714\ufe0f DO\nclass ValidScreen : Screen {\n\n @Composable\n override fun Content() {\n // Inject your dependencies inside composables\n val postService = get<PostService>()\n }\n}\n\n// \ud83d\udeab DON'T\nclass InvalidScreen : Screen {\n\n // Using DI to inject non-serializable types as properties\n val postService by inject<PostService>()\n}\n
"},{"location":"state-restoration/#identifying-screens","title":"Identifying screens","text":"The Screen
interface has a key
property used for saving and restoring the states for the subtree. You can override the default value to set your own key.
class HomeScreen : Screen {\n\n override val key = \"CUSTOM_KEY\"\n\n @Composable\n override fun Content() {\n // ...\n }\n}\n
Voyager provides a uniqueScreenKey
property, useful if you don\u2019t want to manage the keys yourself.
override val key = uniqueScreenKey\n
Warning
You should always set your own key if the screen:
Navigator
To use the transitions you should first import cafe.adriel.voyager:voyager-transitions
(see Setup).
Voyager has built-in transitions! When initializing the Navigator
you can override the default content and use, for example, the SlideTransition
.
setContent {\n Navigator(HomeScreen) { navigator ->\n SlideTransition(navigator)\n }\n}\n
Known bug
There is a known bug using any Transition APIs can leaky ScreenModels or ViewModels, this happens because Voyager by default dispose Screens in the next Composition tick after a pop
or replace
is called, but the transition only finish later, so the ScreenModel or ViewModel is re created or cleared to early. For this purpose since Voyager 1.1.0-beta02
we have introduce a new API to fix this issue. For more details on the issue see #106.
Navigator(\n screen = ...,\n disposeBehavior = NavigatorDisposeBehavior(disposeSteps = false),\n) { navigator ->\n SlideTransition(\n navigator = navigator,\n ...\n disposeScreenAfterTransitionEnd = true\n )\n}\n
Warning
Have encounter Screen was used multiple times
crash? Provide a uniqueScreenKey
for your Screens
class ScreenFoo : Screen {\n\n override val key: ScreenKey = uniqueScreenKey\n\n @Composable\n override fun Content() {\n ...\n }\n
"},{"location":"transitions-api/#available-transitions","title":"Available transitions","text":"FadeTransition()
SlideTransition()
ScaleTransition()
"},{"location":"transitions-api/#custom-transitions","title":"Custom transitions","text":"It\u2019s simple to add your own transitions: call ScreenTransition
with a custom transitionModifier
. Use the available params (screen
, transition
and event
) to animate as needed.
@Composable\nfun MyCustomTransition(\n navigator: Navigator,\n modifier: Modifier = Modifier,\n content: ScreenTransitionContent\n) {\n ScreenTransition(\n navigator = navigator,\n modifier = modifier,\n content = content,\n transition = {\n val (initialScale, targetScale) = when (navigator.lastEvent) {\n StackEvent.Pop -> ExitScales\n else -> EnterScales\n }\n\n scaleIn(initialScale) with scaleOut(targetScale)\n }\n )\n}\n\nsetContent {\n Navigator(HomeScreen) { navigator ->\n MyCustomTransition(navigator) { screen ->\n screen.Content()\n }\n }\n}\n
Take a look at the source of the available transitions for working examples.
"},{"location":"transitions-api/#per-screen-transitions-experimental","title":"Per Screen transitions [Experimental]","text":"If you want to define a Enter and Exit transition for a specific Screen, you have a lot of options to do starting from 1.1.0-beta01 Voyager have a new experimental API for this purpose. To animate the content, we use transitions of the target screen in the case of push navigation, otherwise we use transitions of the initial screen
class ExampleSlideScreen : Screen, ScreenTransition {\n override val key: ScreenKey\n get() = uniqueScreenKey\n\n @Composable\n override fun Content() {\n ...\n }\n\n override fun enter(lastEvent: StackEvent): EnterTransition {\n return slideIn { size ->\n val x = if (lastEvent == StackEvent.Pop) -size.width else size.width\n IntOffset(x = x, y = 0)\n }\n }\n\n override fun exit(lastEvent: StackEvent): ExitTransition {\n return slideOut { size ->\n val x = if (lastEvent == StackEvent.Pop) size.width else -size.width\n IntOffset(x = x, y = 0)\n }\n }\n}\n
It\u2019s convenient to use Kotlin delegates for per-Screen transitions. For example, you can create a SlideTransition
and FadeTransition
classes:
class FadeTransition : ScreenTransition {\n\n override fun enter(lastEvent: StackEvent): EnterTransition {\n return fadeIn(tween(500, delayMillis = 500))\n }\n\n override fun exit(lastEvent: StackEvent): ExitTransition {\n return fadeOut(tween(500))\n }\n}\n\nclass SlideTransition : ScreenTransition {\n\n override fun enter(lastEvent: StackEvent): EnterTransition {\n return slideIn { size ->\n val x = if (lastEvent == StackEvent.Pop) -size.width else size.width\n IntOffset(x = x, y = 0)\n }\n }\n\n override fun exit(lastEvent: StackEvent): ExitTransition {\n return slideOut { size ->\n val x = if (lastEvent == StackEvent.Pop) size.width else -size.width\n IntOffset(x = x, y = 0)\n }\n }\n}\n
Then you can use them as delegates in your Screens:
class SlideScreen : Screen, ScreenTransition by SlideTransition() {\n\n @Composable\n override fun Content() {\n ...\n }\n}\n\nclass FadeScreen : Screen, ScreenTransition by FadeTransition() {\n\n @Composable\n override fun Content() {\n ...\n }\n}\n
Also you can use can pass your custom ScreenTransition
instance in ScreenTransition
function, it will be used for default animation.
setContent {\n Navigator(FadeScreen) { navigator ->\n ScreenTransition(\n navigator = navigator,\n defaultTransition = SlideTransition()\n )\n }\n}\n
The API works with any ScreenTransition API, you just need to provide one and the Per Screen transition should.
setContent {\n Navigator(HomeScreen) { navigator ->\n SlideTransition(navigator)\n }\n}\n
CrossfadeTransition
is not supported yet.
class PostListScreen : Screen {\n\n @Composable\n override fun Content() {\n val viewModel = viewModel<PostListViewModel>()\n // ...\n }\n}\n
By default Voyager provides its own LocalViewModelStoreOwner
and LocalSavedStateRegistryOwner
, that way you can safely create ViewModel
s without depending on Activity
or Fragment
.
Info
Voyager provides a similar implementation, the ScreenModel, which does the same as ViewModel
but also works with Compose Multiplatform.
Info
Source code here.
"},{"location":"android-viewmodel/hilt-integration/","title":"Hilt integration","text":"Success
To use the getViewModel
you should first import cafe.adriel.voyager:voyager-hilt
(see Setup).
Add @HiltViewModel
and @Inject
annotations to your ViewModel
.
@HiltViewModel\nclass HomeViewModel @Inject constructor() : ViewModel() {\n // ...\n}\n
Call getViewModel()
to get a new instance.
class HomeScreen : Screen {\n\n @Composable\n override fun Content() {\n val screenModel = getViewModel<HomeScreenModel>()\n // ...\n }\n}\n
"},{"location":"android-viewmodel/hilt-integration/#assistedinject","title":"@AssistedInject","text":"Currently there\u2019s no Assisted Injection support for Hilt ViewModels (issue).
"},{"location":"android-viewmodel/hilt-integration/#sample","title":"Sample","text":"Info
Sample code here.
"},{"location":"android-viewmodel/viewmodel-kmp/","title":"ViewModel KMP","text":"Since 1.1.0-beta01 we have introduce a experimental API for ViewModel KMP. It is under the package cafe.adriel.voyager:voyager-lifecycle-kmp
(see Setup).
You will need to call ProvideNavigatorLifecycleKMPSupport
before all Navigator
calls and it will be working out of the box.
@Composable\nfun MainView() {\n ProvideNavigatorLifecycleKMPSupport {\n Navigator(...)\n }\n}\n\nclass MyScreen : Screen {\n @Composable\n fun Content() {\n val myViewModel = viewModel { MyScreenViewModel() }\n }\n}\n
"},{"location":"android-viewmodel/viewmodel-kmp/#navigator-scoped-viewmodel","title":"Navigator scoped ViewModel","text":"Voyager 1.1.0-beta01 also have introduced the support for Navigator scoped ViewModel and Lifecycle. This will make easy to share a ViewModel cross screen of the same navigator.
class MyScreen : Screen {\n @Composable\n fun Content() {\n val myViewModel = navigatorViewModel { MyScreenViewModel() }\n }\n}\n
"},{"location":"android-viewmodel/viewmodel-kmp/#lifecycle-kmp","title":"Lifecycle KMP","text":"This version also brings the Lifecycle events for Screen lifecycle in KMP, now is possible to a generic third party API that listen to Lifecycle of a Screen in KMP.
"},{"location":"navigation/","title":"Navigation","text":"Success
To use the Navigator
you should first import cafe.adriel.voyager:voyager-navigator
(see Setup).
On Voyager, screens are just classes with a composable function as the entrypoint. To create one, you should implement the Screen
interface and override the Content()
composable function.
You can use data class
(if you need to send params), class
(if no param is required).
class PostListScreen : Screen {\n\n @Composable\n override fun Content() {\n // ...\n }\n}\n\ndata class PostDetailsScreen(val postId: Long) : Screen {\n\n @Composable\n override fun Content() {\n // ...\n }\n}\n
"},{"location":"navigation/#navigator","title":"Navigator","text":"Navigator
is a composable function deeply integrated with Compose internals. It\u2019ll manage the lifecyle, back press, state restoration and even nested navigation for you.
To start using it, just set the initial Screen
.
class SingleActivity : ComponentActivity() {\n\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n\n setContent {\n Navigator(HomeScreen)\n }\n }\n}\n
Use the LocalNavigator
to navigate to other screens. Take a look at the Stack API for the available operations.
class PostListScreen : Screen {\n\n @Composable\n override fun Content() {\n // ...\n }\n\n @Composable\n private fun PostCard(post: Post) {\n val navigator = LocalNavigator.currentOrThrow\n\n Card(\n modifier = Modifier.clickable { \n navigator.push(PostDetailsScreen(post.id))\n // Also works:\n // navigator push PostDetailsScreen(post.id)\n // navigator += PostDetailsScreen(post.id)\n }\n ) {\n // ...\n }\n }\n}\n
If part of your UI is shared between screens, like the TopAppBar
or BottomNavigation
, you can easily reuse them with Voyager.
@Composable\noverride fun Content() {\n Navigator(HomeScreen) { navigator ->\n Scaffold(\n topBar = { /* ... */ },\n content = { CurrentScreen() },\n bottomBar = { /* ... */ }\n )\n }\n}\n
{% hint style=\u201dwarning\u201d %} You should use CurrentScreen()
instead of navigator.lastItem.Content()
, because it will save the Screen\u2019s subtree for you (see SaveableStateHolder). {% endhint %}
Info
Source code here.
"},{"location":"navigation/bottomsheet-navigation/","title":"BottomSheet navigation","text":"Success
To use the BottomSheetNavigator
you should first import cafe.adriel.voyager:voyager-bottom-sheet-navigator
(see Setup).
Voyager provides a specialized navigator for ModalBottomSheetLayout
: the BottomSheetNavigator
.
Call it and set the back layer content. The BottomSheet content (or the front layer) will be set on demand.
setContent {\n BottomSheetNavigator {\n BackContent()\n }\n}\n
You can also use the default Navigator
to navigate on your back layer content.
setContent {\n BottomSheetNavigator {\n Navigator(BackScreen())\n }\n}\n
The BottomSheetNavigator
accepts the same params as ModalBottomSheetLayout
.
@Composable\npublic fun BottomSheetNavigator(\n modifier: Modifier = Modifier,\n hideOnBackPress: Boolean = true,\n scrimColor: Color = ModalBottomSheetDefaults.scrimColor,\n sheetShape: Shape = MaterialTheme.shapes.large,\n sheetElevation: Dp = ModalBottomSheetDefaults.Elevation,\n sheetBackgroundColor: Color = MaterialTheme.colors.surface,\n sheetContentColor: Color = contentColorFor(sheetBackgroundColor),\n // ...\n)\n
Use the LocalBottomSheetNavigator
to show and hide the BottomSheet.
class BackScreen : Screen {\n\n @Composable\n override fun Content() {\n val bottomSheetNavigator = LocalBottomSheetNavigator.current\n\n Button(\n onClick = { \n bottomSheetNavigator.show(FrontScreen())\n }\n ) {\n Text(text = \"Show BottomSheet\")\n }\n }\n}\n
"},{"location":"navigation/bottomsheet-navigation/#sample","title":"Sample","text":"Info
Source code here.
"},{"location":"navigation/multi-module-navigation/","title":"Multi-module navigation","text":"Voyager has built-in support for multi-module navigation. Its API is based on great DI frameworks like Koin and Kodein, so should be familiar to use.
Suppose we have the following modules:
app
: the entrypoint of our app, contains the Activityfeature-home
: contains the root screen (HomeScreen
)feature-posts
: contains screens related to the posts feature (ListScreen
and DetailsScreen
)navigation
: contains the screen providers used to navigate between modules, both feature-home
and feature-posts
imports itTo navigate from HomeScreen
to the screens on feature-posts
module (ListScreen
and DetailsScreen
), we first need to provide them. The navigation
module should declare all shared screens in the app, use the ScreenProvider
interface for that.
sealed class SharedScreen : ScreenProvider {\n object PostList : SharedScreen()\n data class PostDetails(val id: String) : SharedScreen()\n}\n
Now use the ScreenRegistry
to register these providers. This should be done in yourApplication
class.
class MyApp : Application() {\n\n override fun onCreate() {\n super.onCreate()\n\n ScreenRegistry {\n register<SharedScreen.PostList> {\n ListScreen()\n }\n register<SharedScreen.PostDetails> { provider ->\n DetailsScreen(id = provider.id)\n }\n }\n }\n}\n
You can also create a screenModule
into your feature module, that way your Application
class won\u2019t be bloated by too many declarations.
val featurePostsScreenModule = screenModule {\n register<SharedScreen.PostList> {\n ListScreen()\n }\n register<SharedScreen.PostDetails> { provider ->\n DetailsScreen(id = provider.id)\n }\n}\n
app/../MyApp.ktoverride fun onCreate() {\n super.onCreate()\n\n ScreenRegistry {\n featurePostsScreenModule()\n }\n}\n
Finally, call rememberScreen()
(inside a composable function) or ScreenRegistry.get()
to access your screens.
class HomeScreen : Screen {\n\n @Composable\n override fun Content() {\n val navigator = LocalNavigator.currentOrThrow\n val postListScreen = rememberScreen(SharedScreen.PostList)\n val postDetailsScreen = rememberScreen(SharedScreen.PostDetails(id = postId))\n\n // navigator.push(postListScreen)\n // navigator.push(postDetailsScreen)\n }\n}\n
"},{"location":"navigation/multi-module-navigation/#sample","title":"Sample","text":"Info
Source code here.
"},{"location":"navigation/nested-navigation/","title":"Nested navigation","text":""},{"location":"navigation/nested-navigation/#nested-navigators","title":"Nested Navigators","text":"Going a little further, it\u2019s possible to have nested navigators. The Navigator
has a level
property (so you can check how deeper your are) and can have a parent
navigator (if you need to interact with it).
setContent {\n Navigator(ScreenA) { navigator0 ->\n println(navigator.level)\n // 0\n println(navigator.parent == null)\n // true\n Navigator(ScreenB) { navigator1 ->\n println(navigator.level)\n // 1\n println(navigator.parent == navigator0)\n // true\n Navigator(ScreenC) { navigator2 ->\n println(navigator.level)\n // 2\n println(navigator.parent == navigator1)\n // true\n }\n }\n }\n}\n
Another operation is the popUntilRoot()
, it will recursively pop all screens starting from the leaf navigator until the root one.
Info
Source code here.
"},{"location":"navigation/tab-navigation/","title":"Tab navigation","text":"Success
To use the TabNavigator
you should first import cafe.adriel.voyager:voyager-tab-navigator
(see Setup).
Voyager provides a specialized navigator for tabs : the TabNavigator
.
The Tab
interface, like the Screen
, has a Content()
composable function, but also requires a TabOptions
.
object HomeTab : Tab {\n\n override val options: TabOptions\n @Composable\n get() {\n val title = stringResource(R.string.home_tab)\n val icon = rememberVectorPainter(Icons.Default.Home)\n\n return remember {\n TabOptions(\n index = 0u,\n title = title,\n icon = icon\n )\n }\n }\n\n @Composable\n override fun Content() {\n // ...\n }\n}\n
Info
Since tabs aren\u2019t usually reused, its OK to create them as object
.
The TabNavigator
unlike the Navigator
:
current
propertyYou can use it with a Scaffold to easily create the UI for your tabs.
setContent {\n TabNavigator(HomeTab) {\n Scaffold(\n content = { \n CurrentTab() \n },\n bottomBar = {\n BottomNavigation {\n TabNavigationItem(HomeTab)\n TabNavigationItem(FavoritesTab)\n TabNavigationItem(ProfileTab)\n }\n }\n )\n }\n}\n
Warning
Like theCurrentScreen()
, you should use CurrentTab
instead of tabNavigator.current.Content()
, because it will save the Tab\u2019s subtree for you (see SaveableStateHolder).
Use the LocalTabNavigator
to get the current TabNavigator
, and current
to get and set the current tab.
@Composable\nprivate fun RowScope.TabNavigationItem(tab: Tab) {\n val tabNavigator = LocalTabNavigator.current\n\n BottomNavigationItem(\n selected = tabNavigator.current == tab,\n onClick = { tabNavigator.current = tab },\n icon = { Icon(painter = tab.icon, contentDescription = tab.title) }\n )\n}\n
"},{"location":"navigation/tab-navigation/#sample","title":"Sample","text":"Info
Source code here.
"},{"location":"navigation/tab-navigation/#tabnavigator-nested-navigator","title":"TabNavigator + Nested Navigator","text":"For more complex use cases, when each tab should have its own independent navigation, like the Youtube app, you can combine the TabNavigator
with multiple Navigator
s.
Let\u2019s go back to the Tab navigation example.
setContent {\n TabNavigator(HomeTab) {\n // ...\n }\n}\n
But now, the HomeTab
will have it\u2019s own Navigator
.
object HomeTab : Screen {\n\n @Composable\n override fun Content() {\n Navigator(PostListScreen())\n }\n}\n
That way, we can use the LocalNavigator
to navigate deeper into HomeTab
, or the LocalTabNavigator
to switch between tabs.
class PostListScreen : Screen {\n\n @Composable\n private fun GoToPostDetailsScreenButton(post: Post) {\n val navigator = LocalNavigator.currentOrThrow\n\n Button(\n onClick = { navigator.push(PostDetailsScreen(post.id)) }\n )\n }\n\n @Composable\n private fun GoToProfileTabButton() {\n val tabNavigator = LocalTabNavigator.current\n\n Button(\n onClick = { tabNavigator.current = ProfileTab }\n )\n }\n}\n
"},{"location":"screenmodel/","title":"ScreenModel","text":"Success
The ScreenModel API is a part of the modulecafe.adriel.voyager:voyager-screenmodel
(see Setup).
ScreenModel
is just like a ViewModel: designed to store and manage UI-related data in a lifecycle conscious way. It also allows data to survive configuration changes such as screen rotations.
Unlike ViewModel
, ScreenModel
is just an interface. It\u2019s also Android independent and doesn\u2019t requires an Activity
or Fragment
to work.
class HomeScreenModel : ScreenModel {\n\n // Optional\n override fun onDispose() {\n // ...\n }\n}\n
Info
ScreenModel
is integrated with Coroutines, RxJava, LiveData, Koin, Kodein and Hit!
By design, it\u2019s only possible to create a ScreenModel
instance inside a Screen
. Call rememberScreenModel
and provide a factory lambda.
class HomeScreen : Screen {\n\n @Composable\n override fun Content() {\n val screenModel = rememberScreenModel { HomeScreenModel() }\n // ...\n }\n}\n
If you need to have multiple instances of the same ScreenModel
for the same Screen
, add a tag to differentiate them.
val screenModel = rememberScreenModel(tag = \"CUSTOM_TAG\") { HomeScreenModel() }\n
"},{"location":"screenmodel/#sample","title":"Sample","text":"Info
Source code here.
"},{"location":"screenmodel/#navigator-scoped-screenmodel","title":"Navigator scoped ScreenModel","text":"Success
TherememberNavigatorScreenModel
are part of the navigator library.
Starting from 1.0.0rc08
by using the new Navigator extension called rememberNavigatorScreenModel
is possible to have a ScreenModel that is shared cross all Screens from a Navigator and when the Navigator leaves the Composition the ScreenModel is disposed.
class HomeScreen : Screen {\n\n @Composable\n override fun Content() {\n val navigator = LocalNavigator.currentOrThrow\n val screenModel = navigator.rememberNavigatorScreenModel { HomeScreenModel() }\n // ...\n }\n}\n
Info
Each DI library we provide a extension out of the box it is also provided support for Navigator scoped ScreenModel. See Koin, Kodein and Hit!
"},{"location":"screenmodel/coroutines-integration/","title":"Coroutines integration","text":"Success
The screenModelScope
and StateScreenModel
are part of the core library.
The ScreenModel
provides a screenModelScope
property. It\u2019s canceled automatically when the ScreenModel
is disposed.
class PostDetailsScreenModel(\n private val repository: PostRepository\n) : ScreenModel {\n\n fun getPost(id: String) {\n screenModelScope.launch {\n val post = repository.getPost(id)\n // ...\n }\n }\n}\n
"},{"location":"screenmodel/coroutines-integration/#state-aware-screenmodel","title":"State-aware ScreenModel","text":"If your ScreenModel
needs to provide a state, use the StateScreenModel
. Set the initial state on the constructor and use mutableState
to change the current state.
class PostDetailsScreenModel(\n private val repository: PostRepository\n) : StateScreenModel<PostDetailsScreenModel.State>(State.Init) {\n\n sealed class State {\n object Init : State()\n object Loading : State()\n data class Result(val post: Post) : State()\n }\n\n fun getPost(id: String) {\n coroutineScope.launch {\n mutableState.value = State.Loading\n mutableState.value = State.Result(post = repository.getPost(id))\n }\n }\n}\n
In your screen use state.collectAsState()
and handle the current state.
class PostDetailsScreen : Screen {\n\n @Composable\n override fun Content() {\n val screenModel = rememberScreenModel<PostDetailsScreenModel>()\n val state by screenModel.state.collectAsState()\n\n when (state) {\n is State.Loading -> LoadingContent()\n is State.Result -> PostContent(state.post)\n }\n }\n}\n
"},{"location":"screenmodel/coroutines-integration/#sample","title":"Sample","text":"Info
Sample code here.
"},{"location":"screenmodel/coroutines-integration/#desktop-note","title":"Desktop Note","text":"Info
If you are targeting Desktop, you should provide the dependency org.jetbrains.kotlinx:kotlinx-coroutines-swing
, the screenModelScope
depends on Dispatchers.Main
provided by this library on Desktop. We don\u2019t include it because this library is incompatible with IntelliJ Plugin, see. If you are targeting Desktop for IntelliJ plugins, this library does not require to be provided.
Success
To use the getScreenModel
you should first import cafe.adriel.voyager:voyager-hilt
(see Setup).
Add @Inject
annotation to your ScreenModel
.
class HomeScreenModel @Inject constructor() : ScreenModel {\n // ...\n}\n
Call getScreenModel()
to get a new instance.
class HomeScreen : AndroidScreen() {\n\n @Composable\n override fun Content() {\n val screenModel = getScreenModel<HomeScreenModel>()\n // ...\n }\n}\n
"},{"location":"screenmodel/hilt-integration/#assistedinject","title":"@AssistedInject","text":"Add @AssistedInject
annotation to your ScreenModel
and provide a ScreenModelFactory
annotated with @AssistedFactory
.
class PostDetailsScreenModel @AssistedInject constructor(\n @Assisted val postId: Long\n) : ScreenModel {\n\n @AssistedFactory\n interface Factory : ScreenModelFactory {\n fun create(postId: Long): PostDetailsScreenModel\n }\n}\n
Call getScreenModel()
and use your factory to create a new instance.
data class PostDetailsScreen(val postId: Long): AndroidScreen() {\n\n @Composable\n override fun Content() {\n val screenModel = getScreenModel<PostDetailsScreenModel, PostDetailsScreenModel.Factory> { factory ->\n factory.create(postId)\n }\n // ...\n }\n}\n
"},{"location":"screenmodel/hilt-integration/#sample","title":"Sample","text":"Info
Sample code here.
"},{"location":"screenmodel/hilt-integration/#navigator-scoped-screenmodel","title":"Navigator scoped ScreenModel","text":"class HomeScreen : Screen {\n\n @Composable\n override fun Content() {\n val navigator = LocalNavigator.currentOrThrow\n val screenModel = navigator.getNavigatorScreenModel<HomeScreenModel>()\n // ...\n }\n}\n
"},{"location":"screenmodel/kodein-integration/","title":"Kodein integration","text":"Success
To use the rememberScreenModel
you should first import cafe.adriel.voyager:voyager-kodein
(see Setup).
Declare your ScreenModel
s using the bindProvider
bind.
val homeModule = DI.Module(name = \"home\") {\n bindProvider { HomeScreenModel() } \n}\n
Call rememberScreenModel()
to get a new instance.
class HomeScreen : Screen {\n\n @Composable\n override fun Content() {\n val screenModel = rememberScreenModel<HomeScreenModel>()\n // ...\n }\n}\n
"},{"location":"screenmodel/kodein-integration/#sample","title":"Sample","text":"Info
Sample code here.
"},{"location":"screenmodel/kodein-integration/#navigator-scoped-screenmodel","title":"Navigator scoped ScreenModel","text":"class HomeScreen : Screen {\n\n @Composable\n override fun Content() {\n val navigator = LocalNavigator.currentOrThrow\n val screenModel = navigator.rememberNavigatorScreenModel<HomeScreenModel>()\n // ...\n }\n}\n
"},{"location":"screenmodel/koin-integration/","title":"Koin integration","text":"Success
To use the getScreenModel
you should first import cafe.adriel.voyager:voyager-koin
(see Setup).
Warning
Since 1.1.0-alpha04 we have rename the getScreenModel
to koinScreenModel
, this is a change to follow Koin Compose naming schema. The previous getScreenModel
is deprecated and will be removed on 1.1.0
Declare your ScreenModel
s using the factory
component.
val homeModule = module {\n factory { HomeScreenModel() } \n}\n
Call getScreenModel()
to get a new instance.
class HomeScreen : Screen {\n\n @Composable\n override fun Content() {\n val screenModel = getScreenModel<HomeScreenModel>()\n // ...\n }\n}\n
"},{"location":"screenmodel/koin-integration/#sample","title":"Sample","text":"Info
Sample code here.
"},{"location":"screenmodel/koin-integration/#navigator-scoped-screenmodel","title":"Navigator scoped ScreenModel","text":"class HomeScreen : Screen {\n\n @Composable\n override fun Content() {\n val navigator = LocalNavigator.currentOrThrow\n val screenModel = navigator.getNavigatorScreenModel<HomeScreenModel>()\n // ...\n }\n}\n
"},{"location":"screenmodel/livedata-integration/","title":"LiveData integration","text":"Success
To use the LiveScreenModel
you should first import cafe.adriel.voyager:voyager-livedata
(see Setup).
If your ScreenModel
needs to provide a state, use the LiveScreenModel
. Set the initial state on the constructor and use mutableState
to change the current state.
class PostDetailsScreenModel(\n private val repository: PostRepository\n) : LiveScreenModel<PostDetailsScreenModel.State>(State.Init) {\n\n sealed class State {\n object Init : State()\n object Loading : State()\n data class Result(val post: Post) : State()\n }\n\n fun getPost(id: String) {\n coroutineScope.launch {\n val result = State.Result(post = repository.getPost(id))\n mutableState.postValue(result)\n }\n }\n}\n
In your screen use state.observeAsState()
and handle the current state.
class PostDetailsScreen : Screen {\n\n @Composable\n override fun Content() {\n val screenModel = rememberScreenModel<PostDetailsScreenModel>()\n val state by screenModel.state.observeAsState()\n\n when (state) {\n is State.Loading -> LoadingContent()\n is State.Result -> PostContent(state.post)\n }\n }\n}\n
"},{"location":"screenmodel/livedata-integration/#sample","title":"Sample","text":"Info
Sample code here.
"},{"location":"screenmodel/rxjava-integration/","title":"RxJava integration","text":"Success
To use the disposables
and RxScreenModel
you should first import cafe.adriel.voyager:voyager-rxjava
(see Setup).
The ScreenModel
provides a disposables
property. It\u2019s cleared automatically when the ScreenModel
is disposed.
class PostDetailsScreenModel(\n private val repository: PostRepository\n) : ScreenModel {\n\n fun getPost(id: String) {\n repository.getPost(id)\n .subscribe { post -> /* ... */ }\n .let(disposables::add)\n }\n}\n
"},{"location":"screenmodel/rxjava-integration/#state-aware-screenmodel","title":"State-aware ScreenModel","text":"If your ScreenModel
needs to provide a state, use the RxScreenModel
. Use mutableState
to change the current state.
class PostDetailsScreenModel(\n private val repository: PostRepository\n) : RxScreenModel<PostDetailsScreenModel.State>() {\n\n sealed class State {\n object Init : State()\n object Loading : State()\n data class Result(val post: Post) : State()\n }\n\n fun getPost(id: String) {\n repository.getPost(id)\n .doOnSubscribe { mutableState.onNext(State.Loading) }\n .subscribe { post -> mutableState.onNext(State.Result(post)) }\n .let(disposables::add)\n }\n}\n
In your screen use state.subscribeAsState()
and handle the current state.
class PostDetailsScreen : Screen {\n\n @Composable\n override fun Content() {\n val screenModel = rememberScreenModel<PostDetailsScreenModel>()\n val state by screenModel.state.subscribeAsState(initial = State.Init)\n\n when (state) {\n is State.Loading -> LoadingContent()\n is State.Result -> PostContent(state.post)\n }\n }\n}\n
"},{"location":"screenmodel/rxjava-integration/#sample","title":"Sample","text":"Info
Sample code here.
"}]} \ No newline at end of file diff --git a/setup/index.html b/setup/index.html index 37cfb9b6..9ec06f54 100644 --- a/setup/index.html +++ b/setup/index.html @@ -973,11 +973,11 @@