Giter VIP home page Giter VIP logo

raamcosta / compose-destinations Goto Github PK

View Code? Open in Web Editor NEW
3.1K 21.0 129.0 2.05 MB

Annotation processing library for type-safe Jetpack Compose navigation with no boilerplate.

Home Page: https://composedestinations.rafaelcosta.xyz

License: Apache License 2.0

Kotlin 99.33% Mermaid 0.67%
android jetpack-compose kotlin-android navigation ksp safeargs android-library kotlin kotlin-library hacktoberfest

compose-destinations's Introduction

Maven metadata URL License Apache 2.0 Android API kotlin

A KSP library that processes annotations and generates code that uses Official Jetpack Compose Navigation under the hood. It hides the complex, non-type-safe and boilerplate code you would have to write otherwise.
No need to learn a whole new framework to navigate - most APIs are either the same as with the Jetpack Components or inspired by them.

V2 is here! πŸ™Œ

Please consider migrating to it and leaving feedback as GH issue or on our slack channel #compose-destinations!

Main features 🧭

  • Typesafe navigation arguments
  • Simple but configurable navigation graphs setup
  • Navigating back with a result in a simple and type-safe way
  • Getting the navigation arguments from the SavedStateHandle (useful in ViewModels) and NavBackStackEntry in a type-safe way.
  • Navigation animations
  • Destination wrappers to allow reusing Compose logic on multiple screens
  • Bottom sheet screens through integration with Accompanist Navigation-Material
  • Easy deep linking to screens
  • Wear OS support (since versions 1.x.30!)
  • All you can do with Official Jetpack Compose Navigation but in a simpler safer way!

For a deeper look into all the features, check our documentation website.

Materials πŸ“„

Basic Usage πŸ§‘β€πŸ’»

Note

This readme is about v2. If you're now starting to use Compose Destinations, I strongly recommend using v2. It is currently in beta stage, but we don't expect major issues with it, and soon it will reach stable! If you really want to see basic v1 usage, check it here.

1. Annotate your screen Composables with @Destination<RootGraph>:

@Destination<RootGraph> // sets this as a destination of the "root" nav graph
@Composable
fun ProfileScreen() { /*...*/ }

2. Add navigation arguments to the function declaration:

@Destination<RootGraph>
@Composable
fun ProfileScreen(
   id: Int, // <-- required navigation argument
   groupName: String?, // <-- optional navigation argument
   isOwnUser: Boolean = false // <-- optional navigation argument
) { /*...*/ }

Parcelable, Serializable, Enum and classes annotated with @kotlinx.serialization.Serializable (as well as Arrays and ArrayLists of these) work out of the box! You can also make any other type a navigation argument type. Read about it here

Tip

There is an alternative way to define the destination arguments in case you don't need to use them inside the Composable (as is likely the case when using ViewModel). Read more here.

3. Build the project

Or run ksp task (example: ./gradlew kspDebugKotlin), to generate all the Destinations. With the above annotated composable, a ProfileScreenDestination file would be generated (that we'll use in step 4).

4. Use the generated [ComposableName]Destination's invoke method to navigate to it.

It will have the correct typed arguments.

@Destination<RootGraph>(start = true) // sets this as the start destination of the "root" nav graph
@Composable
fun HomeScreen(
   navigator: DestinationsNavigator // or NavController
) {
   /*...*/
   navigator.navigate(ProfileScreenDestination(id = 7, groupName = "Kotlin programmers"))
}

5. Finally, add the NavHost call:

DestinationsNavHost(navGraph = NavGraphs.root)

Note

NavGraphs is a generated file that contains all navigation graphs. root here corresponds to the <RootGraph> we used in the above examples. You're also able to define your own navigation graphs to use instead of <RootGraph>.

This call adds all annotated Composable functions as destinations of the Navigation Host.

That's it! No need to worry about routes, NavType, bundles and strings. All that redundant and error-prone code gets generated for you.

Setup 🧩

Compose destinations is available via maven central.

1. Add the KSP plugin:

Note: The version you chose for the KSP plugin depends on the Kotlin version your project uses.
You can check https://github.com/google/ksp/releases for the list of KSP versions, then pick the last release that matches your Kotlin version. Example: If you're using 1.9.22 Kotlin version, then the last KSP version is 1.9.22-1.0.17.

groovy - build.gradle(:module-name)
plugins {
    //...
    id 'com.google.devtools.ksp' version '1.9.22-1.0.17' // Depends on your kotlin version
}
kotlin - build.gradle.kts(:module-name)
plugins {
    //...
    id("com.google.devtools.ksp") version "1.9.22-1.0.17" // Depends on your kotlin version
}

2. Add the dependencies:

Compose Destinations has multiple active versions. The higher one uses the latest versions for Compose and Accompanist, while the others use only stable versions. Choose the one that matches your Compose version, considering this table:

Compose 1.1 (1.1.x)Maven Central
Compose 1.2 (1.2.x)Maven Central
Compose 1.3 (1.3.x)Maven Central
Compose 1.4 (1.4.x)Maven Central
Compose 1.5 (1.5.x)Maven Central
Compose 1.6 (1.6.x) Maven Central OR Maven Central
Compose 1.7 (1.7.x) Maven Central OR Maven Central

Warning

If you choose a version that uses a higher version of Compose than the one you're setting for your app, gradle will upgrade your Compose version via transitive dependency.

groovy - build.gradle(:module-name)
implementation 'io.github.raamcosta.compose-destinations:core:<version>'
ksp 'io.github.raamcosta.compose-destinations:ksp:<version>'

// V2 only: for bottom sheet destination support, also add
implementation 'io.github.raamcosta.compose-destinations:bottom-sheet:<version>'
kotlin - build.gradle.kts(:module-name)
implementation("io.github.raamcosta.compose-destinations:core:<version>")
ksp("io.github.raamcosta.compose-destinations:ksp:<version>")

// V2 only: for bottom sheet destination support, also add
implementation("io.github.raamcosta.compose-destinations:bottom-sheet:<version>")

Note

If you want to use Compose Destinations in a Wear OS app, replace above core dependency with:
implementation 'io.github.raamcosta.compose-destinations:wear-core:<version>'
this will use Wear Compose Navigation internally.
Read more about the next steps to configure these features here

Community πŸ’¬

Please join the community at Kotlin slack channel: #compose-destinations
Ask questions, suggest improvements, or anything else related to the library.

If you like the library, consider starring and sharing it with your colleagues.

compose-destinations's People

Contributors

extmkv avatar hrafnthor avatar imashnake0 avatar margarita-v avatar mordred avatar mumayank avatar nrobi144 avatar petrstetka avatar raamcosta avatar rafaeltonholo avatar skaldebane avatar sumanabhi avatar vahalaru avatar yasanglass avatar yschimke avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

compose-destinations's Issues

How can we use this with bottom navigator of Scaffold?

Hi, thanks for the exciting library.

I have a question about getting a navigator at the Scaffold bottom navigator.

When using Scaffold with vanilla nav host, we usually do like this,

@Composable
fun RallyApp() {
    RallyTheme {
        val allScreens = RallyScreen.values().toList()
        val navController = rememberNavController()
        val backstackEntry = navController.currentBackStackEntryAsState()
        val currentScreen = RallyScreen.fromRoute(
            backstackEntry.value?.destination?.route
        )
        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreens = allScreens,
                    onTabSelected = { screen -> navController.navigate(screen.name) },
                    currentScreen = currentScreen
                )
            }
        ) { innerPadding ->
            RallyNavHost(
                navController = navController,
                modifier = Modifier.padding(innerPadding)
            )
        }
    }
}

But when using Destinations,

how can we give navigator to the top or bottom bar..? In this situation?

@Composable
fun RallyApp() {
    RallyTheme {
        val allScreens = RallyScreen.values().toList()
        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreens = allScreens,
                    // todo: how can we give navigator?
                    onTabSelected = { screen -> navigator.navigate(screen.name) },
                    currentScreen = currentScreen
                )
            }
        ) { innerPadding ->
            DestinationsNavHost(navGraph = NavGraphs.root)
        }
    }
}

Navigation using Uri

The Jetpack navigation component support navigation via Uri, and it seems like compose destination doesn't support navigation with URI yet, there is an alternative way ?

Support for dialogs

Can dialogs be added to DestinationsNavHost? That doesn't seem to be currently supported.

Define start destination dynamically

There are scenarios where the initial destination depends on a particular condition, for example if a user is connected or not, is there an alternative way to do this?

KSP generated package name

Currently the generated files are stored in com.ramcosta.composedestinations package name. It seems to be it would be better it generated the classes to the app's package name and ideally keep the package name of the destination.

Dependencies to composable and access to DestinationDependenciesContainer

Before, in version 0.9.2, we could provide a DestinationDependenciesContainer to a DestinationsNavHost to provide some rudimentary dependency injection to Destinations.

It seems like this feature is no longer accessible unless I provide my own NavHostEngine due to the default enigine is creating a empty DestinationDependenciesContainer by default. Is this a conscious design decision? I looked into using the manual compose calls but that easily becomes quite messy.

Use alongside a bottom bar

How would this library handle the use of a BottomBarNavGraph ?

I personally often use a bottom bar with its respective composable destinations and found that integrating nested navigation with a navigation graph AND a bottom bar navigation graph is hectic. I'm curious if this library can handle this or if it is something that could be thought of.

When startDestination is changed, the app returns to the initial startDestination.

In my project MainScreen has bottomBar, so start Destination must be re-designated.
Otherwise, a stack is created every time the bottomBar menu is pressed.

LoginScreen-> HomeScreen

@Destination(Routes.Intro.LOGIN,true)
@Composable
fun LoginScreen(
    navigator: DestinationsNavigator,
    navController: NavController
) {
    ConstraintLayout(
        modifier = Modifier.fillMaxSize()
    ) {
        val (loginButton) = createRefs()
        Button(
            onClick = {
                navigateToHome(navigator, navController)
            },
            modifier = Modifier
                .fillMaxWidth()
                .constrainAs(loginButton) {
                    bottom.linkTo(parent.bottom)
                }
        ) {
            Text(text = "login")
        }
    }
}

fun navigateToHome(
    navigator: DestinationsNavigator,
    navController: NavController
) {
    navigator.navigate(Routes.Main.HOME) {
        popUpTo(Routes.Intro.LOGIN) { inclusive = true }
        navController.graph.setStartDestination(Routes.Main.HOME)
    }
}

Setting startDestination with argument doesn't work

Hello,
I want to override the start destination and that destination does have an required argument.

DestinationsNavHost(navController = navController, startDestination = PostScreenDestination(1234))

But when i pass the argument it shows error in the IDE:

Type mismatch.
Required: Destination
Found: Routed

How i should do it?

Thanks in advance.

Different Screen Sizes

Hi, this is a really useful library. Thanks for doing such work.

I'm just opening this issue to ask you for support on Tablets (2 screens at the same time maybe) and Surface Duo with a hinge. Have you planned to do this enhancement?

Thanks

Animated Style pass custom variable

Amazing work with the library, it really makes things easier!

One question, if you want animations between screen you can create custom style for each screen that overrides the default AnimatedNavHostEngine you can define for all screens in that graph. But in styles I can't seem to find a way to pass a variable to it. For example in the documentation under ProfileTransitions.kt you have hardcoded initialOffsetX = 1000, but what if I wanted a different number (for example the exact width of the screen. I can define this for default animations, but not for each individual screen.

For example:

     ProvideWindowInsets {
        AppTheme {

            val navController = rememberAnimatedNavController()
            val viewModel: CustomViewModel = hiltViewModel()

            // We can get width of the screen this way
            BoxWithConstraints {
                val width = constraints.maxWidth
                val navHostEngine = rememberAnimatedNavHostEngine(
                    rootDefaultAnimations = RootNavGraphDefaultAnimations(
                        enterTransition = {
                            slideInHorizontally(
                                initialOffsetX = { width },
                                animationSpec = tween(
                                    durationMillis = 700,
                                    easing = FastOutSlowInEasing
                                )
                            )
                        },
                        exitTransition = {
                            slideOutHorizontally(
                                targetOffsetX = { -width },
                                animationSpec = tween(
                                    durationMillis = 700,
                                    easing = FastOutSlowInEasing
                                )
                            )
                        },
                        popEnterTransition = {
                            slideInHorizontally(
                                initialOffsetX = { -width },
                                animationSpec = tween(
                                    durationMillis = 700,
                                    easing = FastOutSlowInEasing
                                )
                            )
                        },
                        popExitTransition = {
                            slideOutHorizontally(
                                targetOffsetX = { width },
                                animationSpec = tween(
                                    durationMillis = 700,
                                    easing = FastOutSlowInEasing
                                )
                            )
                        }
                    ),
                )

                DestinationsNavHost(
                    navGraph = NavGraphs.root,
                    startRoute = NavGraphs.root.startRoute,
                    engine = navHostEngine,
                    navController = navController,
                    modifier = Modifier
                ) {

                    animatedComposable(AScreenDestination) {
                        AScreen(
                            modifier = Modifier.statusBarsPadding(),
                            viewModel = viewModel,
                          )
                    }

                    animatedComposable(BScreenDestination) {
                        BScreen(
                            modifier = Modifier.statusBarsPadding(),
                            viewModel = viewModel,
                          )
                    }
                }
            }
    }
}

In this case both will have default, then If I wanted to change the transition of BScreen, you would need to add:


object BScreenTransition: DestinationStyle.Animated {

    override fun AnimatedContentScope<NavBackStackEntry>.enterTransition(): EnterTransition? {
        return when (initialState.navDestination) {
            BScreenTransitionDestination ->
                slideInHorizontally(
                    initialOffsetX = { 1000 },
                    animationSpec = tween(2500)
                )
            else -> null
        }
    }

    override fun AnimatedContentScope<NavBackStackEntry>.exitTransition(): ExitTransition? {
        return when (targetState.navDestination) {
            BScreenTransitionDestination ->
                slideOutHorizontally(
                    targetOffsetX = { -1000 },
                    animationSpec = tween(2500)
                )
            else -> null
        }
    }

    override fun AnimatedContentScope<NavBackStackEntry>.popEnterTransition(): EnterTransition? {
        return when (initialState.navDestination) {
            BScreenTransitionDestination  ->
                slideInHorizontally(
                    initialOffsetX = { -1000 },
                    animationSpec = tween(2500)
                )
            else -> null
        }
    }

    override fun AnimatedContentScope<NavBackStackEntry>.popExitTransition(): ExitTransition? {
        return when (targetState.navDestination) {
            BScreenTransitionDestination ->
                slideOutHorizontally(
                    targetOffsetX = { 1000 },
                    animationSpec = tween(2500)
                )
            else -> null
        }
    }
}

Then in BScreen add:

@Destination(
    style = BScreenTransition::class,
)

So the animation style would be applied.
This way you can't pass the width to the BScreenTransition you can only change a fixed number inside this object.
Maybe it can be done and I just don't know how.

Thanks for the help!

Displaying back button in the scaffold

This is more of a feature request and less of a bug/issue.

Problem
How to know that the current screen is a top screen and there is no need to show back button. The counter-part of this issue is also true, which would be how to know the current screen has a predecessor or not.

Requested feature
There should be a way to know when to show the back button in the scaffold, this way we can show the back button based on the graph dynamically instead of hard-coding it in the scaffold.

Suggestion
Add a method in Navigator to check if the current screen has a predecessor or not, if it has then it can return true, else false.

Handling Base class with abstract composable

[ksp] com.ramcosta.composedestinations.codegen.commons.IllegalDestinationsSetup: Destination composable names must be unique: found multiple named 'Screen'

abstract class BaseScreen {

	protected abstract val viewModelClass: Class<VIEW_MODEL>

	@Composable
	protected abstract fun Screen(
		someInterface: SomeInterface,
		bundle: Bundle?
	)

...
}
class ScreenA: BaseScreen (){

....
        @Composable
	@Destination("routeA")
	override fun Screen(
		someInterface:SomeInterface,
		bundle: Bundle?
	) {

}

class ScreenB: BaseScreen (){

....
        @Composable
	@Destination("routeB")
	override fun Screen(
		someInterface:SomeInterface,
		bundle: Bundle?
	) {

}

Is there any way that we can give individual names for Composables?

Version 0.8.3-alpha05

Access to the parent navigation controller(DestinationsNavigator)

First of all, let me say we really appreciate the effort put into this library. We really like how easy it is to do navigation with this library in Android Compose. Thank you very much. πŸ‘πŸ‘πŸ‘Œ

Nevertheless, we stumbled on some problems with our use case. We are planning to start developing with Jetpack Compose and we are now in the phase of researching which navigation approach should we use in our app. For better understanding, here is a github repository to the project with the below-mentioned problem.

What we need is to have multiple DestinationsNavHosts in our app because we are using one DestinationNavHost(root) that is used for the main flow of the app(SplashScreen -> HomeScreen,ExternalDetailsScreen) and another DestinationNavHost(nested) that is used as a container for Drawer menu items(ProfileScreen, InternalDetailsScreen) in the home screen.

OUR APPLICATION FLOW:
Screenshot 2021-12-01 at 09 22 38

So, our pain point in our application is ProfileScreen. There we have 2 buttons. Button with the text "Inside details screen" opens a new screen inside nested DestinationNavHost as is presented with InternalDetailsScreen above in the Image. For that, we use DestinationsNavigator from ProfileScreen and it works fine. But we have a problem with the button that has "External details screen" for a text, and here is expected to show a new screen in the root DestinationNavHost. If we use DestinationsNavigator from ProfileScreen, the screen is presented as InternalDetailsScreen. But we want to replace the content in root DestinationNavHost. Currently, we are solving this with a companion object on MainActivity which stores the DestinationsNavigator in it from HomeScreen and then we access it when we press on the "External details screen" button to navigate to the correct screen.

Is there a capability in this library that we are not seeing to do that kind of navigation with parent DestinationsNavigator? We would really appreciate it if you could point us on how to use that with the current state of the library?

Default value of string not generated properly in nav args

Hi!

I found that If I define a string property with default value, with multiple words, in the generated code, only the first world is appearing:

@Destination(route = "detail")
@Composable
fun DetailScreen(
    id: String = "The choosen one"
) {
}

The generated object:

object DetailScreenDestination: Destination {

    override val route get() = "detail?id={id}"
    
	override val arguments = listOf(
		navArgument("id") {
			type = NavType.StringType
			nullable = false
			defaultValue = "The
		}
	)

    @Composable
    override fun Content(
        navController: NavController,
        navBackStackEntry: NavBackStackEntry,
        scaffoldState: ScaffoldState?
    ) {
        DetailScreen(
			id = navBackStackEntry.arguments?.getString("id") ?: "The
		)
    }
         
    fun withArgs(
		id: String = "The,
    ): String {
        var route = route

        return route
			.replace("{id}", id)
    }
    
}

As you can see, the "The is there but nothing else.

Destination with same composable function name causes error

Hello!

I created two destinations with different routes, but I was lazy, copy pasted the first composable and forgot to change the function name of the composable. This resulted in a rather incomprehensible error:

[ksp] java.io.IOException: Stream Closed
	at java.base/java.io.FileOutputStream.writeBytes(Native Method)
	at java.base/java.io.FileOutputStream.write(FileOutputStream.java:341)
	at com.ramcosta.composedestinations.codegen.commons.UtilsKt.plusAssign(Utils.kt:6)
	at com.ramcosta.composedestinations.codegen.processors.SingleDestinationProcessor.process(SingleDestinationProcessor.kt:36)
	at com.ramcosta.composedestinations.codegen.processors.DestinationsProcessor.process(DestinationsProcessor.kt:16)
	at com.ramcosta.composedestinations.ksp.Processor.process(Processor.kt:42)
	at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension$doAnalysis$4$1.invoke(KotlinSymbolProcessingExtension.kt:186)
	at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension$doAnalysis$4$1.invoke(KotlinSymbolProcessingExtension.kt:184)
	at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.handleException(KotlinSymbolProcessingExtension.kt:278)
	at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.doAnalysis(KotlinSymbolProcessingExtension.kt:184)
	at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(TopDownAnalyzerFacadeForJVM.kt:120)
	at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(TopDownAnalyzerFacadeForJVM.kt:86)
	at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler$analyze$1.invoke(KotlinToJVMBytecodeCompiler.kt:252)
	at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler$analyze$1.invoke(KotlinToJVMBytecodeCompiler.kt:243)
	at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport.analyzeAndReport(AnalyzerWithCompilerReport.kt:113)
	at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze(KotlinToJVMBytecodeCompiler.kt:243)
	at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules$cli(KotlinToJVMBytecodeCompiler.kt:90)
	at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules$cli$default(KotlinToJVMBytecodeCompiler.kt:56)
	at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:169)
	at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:52)
	at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:92)
	at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:44)
	at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:98)
	at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1574)
	at jdk.internal.reflect.GeneratedMethodAccessor113.invoke(Unknown Source)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:359)
	at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
	at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
	at java.base/java.security.AccessController.doPrivileged(Native Method)
	at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
	at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:562)
	at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:796)
	at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:677)
	at java.base/java.security.AccessController.doPrivileged(Native Method)
	at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:676)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at java.base/java.lang.Thread.run(Thread.java:834)

Nested graphs without root destination

Is it possible to create a NavGraph without a root destination like below?

NavHost(
    navController = navController,
    startDestination = "tab_home"
) {
    navigation(
        route = "tab_home",
        startDestination = "start",
    ) {
        composable("start") { /* */ }
        composable("next") { /* */ }
    }
    navigation(
        route = "tab_setting",
        startDestination = "setting"
    ) {
        composable("setting") { /* */ }
    }
}

I added navGraph option to all @Destination but the NavGaphs with Exception was generated.

object NavGraphs {
    val tabHome = NavGraph(/* */)
    val tabSettings = NavGraph(/* */)
    val root: NavGraph = throw RuntimeException("No found destinations for 'root' navigation graph")
}

Is there any solution?

I use version 1.1.5-beta.

Share ViewModel in nestedgraph

I'm working on register part of my application.
I need to share Viewmodel between RegisterPage and MapPage, and also I want to clear ViewModel after closing all Pages in this graph , but when I use dependenciesContainerBuilder for keep ViewModel shared , it's like singleton and prevent making new instance of ViewModel for new nestedgraph , is nay another approach here ?

How to inject ViewModel in the screen?

Hi

I was trying to use the library with the ViewModel with the screen. I usually inject the ViewModel into the screen via arguments as shown below.

SampleScreen

@Composable
@Destination(start = true)
fun SampleScreen(
    viewModel: SampleViewModel,
    navigator: DestinationsNavigator,
) {
         // components
}

When I try the same, I am getting a runtime error.

2022-01-14 03:37:55.843 6495-6495/com.example.project E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.project, PID: 6495
java.lang.RuntimeException: SampleViewModel was requested, but it is not present
    at com.example.project.destinations.SampleScreenDestination.Content(SampleScreenDestination.kt:34)
    at com.ramcosta.composedestinations.DefaultNavHostEngine.CallComposable(DefaultNavHostEngine.kt:144)
    at com.ramcosta.composedestinations.DefaultNavHostEngine.access$CallComposable(DefaultNavHostEngine.kt:30)
    at com.ramcosta.composedestinations.DefaultNavHostEngine$addComposable$1.invoke(DefaultNavHostEngine.kt:104)
    at com.ramcosta.composedestinations.DefaultNavHostEngine$addComposable$1.invoke(DefaultNavHostEngine.kt:103)

Could you please tell me if I missing some steps while setting up the library?

Navigation setup

@Composable
fun NavigationComponent(
    modifier: Modifier = Modifier
) {

    DestinationsNavHost(
        navGraph = NavGraphs.root,
        modifier = modifier
    )
}

How would you suggest injecting the VMs?
How would you use the library with the Hilt library?

Destination Generation fails if the first parameter is non-navigation argument with default value

Hi, i'm trying to add your library to my project, but destinations with viewmodel injection in the constructor doesn't work:

@Destination
@Composable
fun myScreen(
    viewModel: myViewModel = hiltViewModel(),
    myObj: myObj
)

and the autogenerated code has an empty parameter on myScreen():

object myScreenDestination : DirectionDestination {
         
    operator fun invoke() = this
    
    override val route = "my_screen"
    
    @Composable
    override fun Content(
        navController: NavHostController,
        navBackStackEntry: NavBackStackEntry,
        dependencyContainer: DestinationDependenciesContainer
    ) {
		myScreen(,
			myObj= myObj()
		)
    }
    
}

Thanks for your work!

NavGraph doesn't generate Annotations of `nestedNavGraphs`

As you can see in the code below the root NavGraph misses a few Annotations of the auth NavGraph. The root NavGraph only generates the Annotations of it's screens but doesn't take into consideration the Annotations of the nestedNavGraphs like the auth NavGraph.

import com.example.sessions_clean.android.ui.screens.destinations.*

import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material3.ExperimentalMaterial3Api

/**
 * Class generated if any Composable is annotated with `@Destination`.
 * It aggregates all [TypedDestination]s in their [NavGraph]s.
 */
object NavGraphs {

    @ExperimentalMaterialApi
    @ExperimentalFoundationApi
    @ExperimentalMaterial3Api
    @ExperimentalAnimationApi
    val auth = NavGraph(
        route = "auth",
        startDestination = AccountSelectionScreenDestination,
        destinations = listOf(
            AccountSelectionScreenDestination,
            RegisterCompanyScreenDestination,
            LoginScreenDestination,
            RegisterScreenDestination
        )
    )

    @ExperimentalMaterialApi
    @ExperimentalMaterial3Api
    val root = NavGraph(
        route = "root",
        startDestination = HomeScreenDestination,
        destinations = listOf(
            ProfileImageSelectActionDestination,
            HomeScreenDestination,
            ProfileScreenDestination
        ),
        nestedNavGraphs = listOf(
            auth
        )
    )
}

Allowing any type of parameters

The current implementation of Compose Destinations only supports a handful of primitive types a Bundle can save (as far as I understand) plus the navigation controllers as parameters for Destination composables, while using the Jetpack Navigation library allows you to use whatever parameters you like.

I don't know the technical reasons why exactly isn't that a thing here, and I have some ideas (all theoretical) that can be done here. However before throwing a bunch of random thoughts, why are we technically limited here?

Make parametrized destination a class with constructor

Currently, adding parameters adds a withArgs() method. This method has the parameters nicely typed, but the destination spec stays an object. This brings possible runtime issues. E.g. adding a param to param-less destination will crash in runtime.

Please change the syntax of such objects to become classes and parameters will be the contructor parameters.

This:

object MyScreenDestination : Destination {
    override val route = "my_screen/{number}"
    override val arguments = listOf(
        navArgument("number") {
            type = NavType.IntType
            nullable = false
        }
    )
    @Composable
    override fun Content(
        navController: NavHostController,
        navBackStackEntry: NavBackStackEntry,
        situationalParameters: Map<Class<*>, Any>,
    ) {
        // ...
    }
    fun withArgs(
        number: Int,
    ): String { /* ... */ }
}

will become this:

class MyScreenDestination(
     number: Int,
) : Destination {
    override val route = "my_screen/{number}"
        .replace("{number}", number.toString())
    override val arguments = listOf(
        navArgument("number") {
            type = NavType.IntType
            nullable = false
        }
    )
    @Composable
    override fun Content(
        navController: NavHostController,
        navBackStackEntry: NavBackStackEntry,
        situationalParameters: Map<Class<*>, Any>,
    ) {
        // ...
    }
}

compose 1.2.0-alpha01 support in 1.1.5 beta ??

Hey there, does the latest version of compose destination 1.1.5 beta contains changes from compose 1.2.0-alpha01? Because it now supports to skipHalfExpaneded State in bottomsheet which i require.

Question: Minimal navigation example

What is the minimal and cleanest way to make the following navigation between three screens working:

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun Scaffold() {
    Column(modifier = Modifier.fillMaxSize()) {

        DestinationsNavHost(modifier = Modifier.fillMaxWidth().weight(1f))

        Row(
            modifier = Modifier.fillMaxWidth().wrapContentHeight(),
            horizontalArrangement = Arrangement.spacedBy(24.dp)
        ) {
            Button(onClick = { DestinationsNavigator.navigate(Screen1Destination) }) {
                Text("Screen 1")
            }
            Button(onClick = { DestinationsNavigator.navigate(Screen2Destination) }) {
                Text("Screen 2")
            }
            Button(onClick = { DestinationsNavigator.navigate(Screen3Destination) }) {
                Text("Screen 3")
            }
        }
    }
}

@Destination(start = true)
@Composable
fun Screen1() {
    Text("Screen 1")
}

@Destination
@Composable
fun Screen2() {
    Text("Screen 2")
}

@Destination
@Composable
fun Screen3() {
    Text("Screen 3")
}

From what I understand from your Wiki and by looking at your sample, I can only get a DestinationsNavigator impl instance as a param to my screens (via generated code). But in this simple case I need it outside. rememberDestinationsNavController seems to not help me either, since its navigate(...) does not accept my ScreenXDestinations.

Is this not a valid use case? What did I misunderstand?

Multi Module support

I tried using the plugin with a multi module Android project, and D8 doesn't like the generation of classes.

For example:

I have module A and B, which are included in the app module. The app module has

android{
...
    applicationVariants.all {
        sourceSets {
            getByName("main") {
                java.srcDir(File("build/generated/ksp/$name/kotlin"))
            }
        }
    }
...
}

but D8 fails during compilation with the following error:

* What went wrong:
Execution failed for task ':app:mergeLibDexInternalDebug'.
> A failure occurred while executing com.android.build.gradle.internal.tasks.DexMergingTaskDelegate
   > There was a failure while executing work items
      > A failure occurred while executing com.android.build.gradle.internal.tasks.DexMergingWorkAction
         > com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives:
           Type com.ramcosta.composedestinations.AnimatedDestinationStyle$DefaultImpls is defined multiple times: <module1-path>\build\.transforms\7673bf153d1e0b34756cfeb713417353\transformed\classes\classes.dex, <module2-path>\build\.transforms\2692adfb593ffb4aebe8fa9a5adf48f0\transformed\classes\classes.dex
           Learn how to resolve the issue at https://developer.android.com/studio/build/dependencies#duplicate_classes.

Is it something I'm missing? Do you have a multi module example that I can follow? Thanks.

Annotations not imported in the generated files

Hello @raamcosta !

I want to use some experimental things in compose and they requires some annotations.
For example here i am using ListItem which is experminal currently:

@ExperimentalMaterialApi
@Destination
@Composable
fun SimpleScreen() {
    ListItem { Text("Hello") }
}

When i try to build the app, i get an error in the generated NavGraphs file: Unresolved reference: ExperimentalMaterialApi like this:

Screenshot_2021-12-05_23-49-45

And when i import it, and try again, it regenerate the same file and recreate the same issue.
The build stuck there...

I guess that the code generator doesn't import these annoations.

Is there a work around or am i doing it wrong?

Finally, i really enjoy the library, great work :)

Question: Struggling with project setup

I would like to try your library since it looks really promising.
However, I am struggling with the very basics in terms of project setup.

build.gradle:

classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:1.5.31-1.0.0"

app/build.gradle:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.google.devtools.ksp'

android {
    ...
}
kotlin {
    sourceSets {
        main {
            kotlin.srcDir("build/generated/ksp/debug/kotlin")
        }
        main {
            kotlin.srcDir("build/generated/ksp/release/kotlin")
        }
    }
}
dependencies {
   ...
    implementation 'io.github.raamcosta.compose-destinations:core:0.9.1-beta'
    ksp 'io.github.raamcosta.compose-destinations:ksp:0.9.1-beta'
}

No matter if I add sourceSets for only debug, only release or both (as above), whenever I switch the build variant, I get the following error:

e: ...\app\build\generated\ksp\debug\kotlin\com\ramcosta\composedestinations\CoreAnimationExtensions.kt: (15, 7): Redeclaration: DefaultAnimationParams

Without switching build variants I can build the project.

I assume the problem is on my side. Could you please elaborate on how to properly setup my project for debug and release? Anything (else) I am doing wrong?

Please let me know if you need more details about my setup.

Erroneus dependency on material design in DestinationsNavHost.kt

Hi!
The generated DestinationsNavHost.kt actually depends on material design:

import androidx.compose.material.*

Even if none of the material components are used (of course I'd say!), it breaks my build as androidx.compose.material is not a dependency of my application.

I guess that line should be removed!

Feature Request: Add a log interceptor for destinations swich

This repo is awesome, I've been having a lot of headaches lately writing navigation that doesn't support SafeArgument and this library has helped me a lot.
Do you have any idea of ​​adding an interceptor when the destination jumps, such as recording logs, track event and reporting. This is helpful for app analysis.

In my project, I use the following code to monitor Compose's lifecycle.

@Composable
inline fun LifeCycleEventEffect(
  keyName: String = "lifecycle",
  crossinline enterEventLambda: () -> Unit,
  crossinline exitEventLambda: () -> Unit
) {
  DisposableEffect(key1 = keyName, effect = {
    enterEventLambda()
    object : DisposableEffectResult {
      override fun dispose() {
        exitEventLambda()
      }
    }
  })
}

For logs

@Composable
fun LifeCycleLogEffect(screenName: String, logger: Logger) = LifeCycleEventEffect(
  enterEventLambda = { logger.debug("enter $screenName") },
  exitEventLambda = { logger.debug("exit $screenName") }
)

previewing @Destination Screen in @Preview function

Much appreciate the effort you put to make the library. Which really makes navigation in Android Compose a lot easier. Thank you very much.

By the way, I stumbled upon the problem today, which is how can I preview a @destination Screen in @Preview function.
The MainScreen needs DestinationsNavigator, and I don't know how do I get that in my @Preview function.

Here is an Example for better understanding

@Destination(start = true)
@Composable
fun MainScreen(
    navigator: DestinationsNavigator
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(horizontal = 24.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Button(
            onClick = {
                navigator.navigate(SecondScreenDestination(User("Vikash","Today")))
            }
        ) {
            Text("Hello", fontSize = 18.sp)
        }
    }
}

@Preview
@Composable
fun DefaultPreview() {
    MainScreen(?????)
}

How can i use inclusive option?

This is the code that sets the Splash screen as the starting point and goes to the Home screen after 2 seconds.
When press BackKey on the Home screen, it goes back to the Splash screen.

@Destination(start = true)
@Composable
fun SplashScreen(
    navigator: DestinationsNavigator
) {
    Column(modifier = Modifier.fillMaxSize()) {
        Text(text = "splash screen")
    }
    delay(2000L)
    navigator.navigate(HomeScreenDestination())
}
@Destination
@Composable
fun HomeScreen(
    navigator: DestinationsNavigator
) {
    Column(modifier = Modifier.fillMaxSize()) {
        Text(text = "home screen")
    }
}

Is there any way to prevent this happening?
Thanks.

Destionation annotation & interface name clash

Currently the Destination's annotation and interface name are same and fqn/import alias has to be used.
This is quite inconvenient. It would be great if one of them get renamed.

Second, it seems that KSP won't pickup an import-aliased Destination annotation.

Testing screens that uses DestinationsNavigator and Parcelable args

Hi,

Previously i was using this to mock the NavHostController, but cannot find a way to do the same with DestinationsNavController.

private val navController = TestNavHostController(ApplicationProvider.getApplicationContext())

I could make it work on the startDestination of the NavGraphs, but i think this is not the correct way.

image

On the detail screen i receive parcelable args, and i'm not able to mock the args on the Destination.
When i tried to run the test, i receive this exception and cannot mock the args:

image

I wanna know if already exists a way to mock or test the screens with this library, receiving the args from the DestinationNavigator.

Pass data to previous composable in lib

I have 2 Screens: Screen A and Screen B. From Screen A, I open Screen B. And when I return Screen B to Screen A, I want to transfer data back to Screen A.

Is there any solution for this?

[feature request] Use composable function name as route

Hi!

What do you think about generating the route from the composable function name? Keep the current route parameter but generate a default value for it , maybe adding a pre- or postfix to the generated string, something like this.

java.lang.IllegalStateException: You need to use 'rememberAnimatedNavHostEngine' to get an engine that can use BottomSheet and pass that into the 'DestinationsNavHost'

Hi I Want implement bottom sheet but got this error

@Composable
@Destination(style = DestinationStyle.BottomSheet::class)
fun ColumnScope.TestBottomScreen(
    navigator: DestinationsNavigator,
) {

}

and this is my DestinationsNavHost code :

val navController = rememberAnimatedNavController()

val bottomSheetNavigator = rememberBottomSheetNavigator()
navController.navigatorProvider += bottomSheetNavigator
ModalBottomSheetLayout(bottomSheetNavigator) {
    DestinationsNavHost(
        navController = navController,
        navGraph = NavGraphs.root
    )
}

How to use popUpTo

 navController.navigate(destination) {
                popUpTo("summary_screen") { inclusive = true }
           }

navigator.navigate(destination)

I am using above code to pop summary screen from backstack before navigating but its not working. Am i missing something?

Support Array and ArrayList navigation arguments

[ksp] com.ramcosta.composedestinations.codegen.commons.IllegalDestinationsSetup: Composable 'AmountScreen': 'navArgsDelegate' cannot have arguments that are not navigation types.
at com.ramcosta.composedestinations.codegen.commons.DestinationWithNavArgsMapper.getNavArgs(DestinationWithNavArgsMapper.kt:24)
at com.ramcosta.composedestinations.codegen.commons.DestinationWithNavArgsMapper.map(DestinationWithNavArgsMapper.kt:12)
at com.ramcosta.composedestinations.codegen.CodeGenerator.generate(CodeGenerator.kt:26)
at com.ramcosta.composedestinations.ksp.processors.Processor.process(Processor.kt:44)
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension$doAnalysis$4$1.invoke(KotlinSymbolProcessingExtension.kt:186)
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension$doAnalysis$4$1.invoke(KotlinSymbolProcessingExtension.kt:184)
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.handleException(KotlinSymbolProcessingExtension.kt:278)
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.doAnalysis(KotlinSymbolProcessingExtension.kt:184)
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(TopDownAnalyzerFacadeForJVM.kt:120)
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(TopDownAnalyzerFacadeForJVM.kt:86)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler$analyze$1.invoke(KotlinToJVMBytecodeCompiler.kt:252)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler$analyze$1.invoke(KotlinToJVMBytecodeCompiler.kt:243)
at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport.analyzeAndReport(AnalyzerWithCompilerReport.kt:113)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze(KotlinToJVMBytecodeCompiler.kt:243)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules$cli(KotlinToJVMBytecodeCompiler.kt:90)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules$cli$default(KotlinToJVMBytecodeCompiler.kt:56)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:169)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:52)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:92)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:44)
at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:98)
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1574)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:359)
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:562)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:796)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:677)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:676)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:834)

@Parcelize
data class AmountScreenArgs(
    val sendingAmount: Float,
    val exchangeRate: Float,
    val purposeList: List<TransferPurpose>
):Parcelable


@Parcelize
data class TransferPurpose(
    val id: Int,
    val purpose: String,
    var is_selected:Boolean = false
): Parcelable


@ExperimentalMaterialApi
@ExperimentalCoilApi
@ExperimentalAnimationApi
@Composable
@Destination(navArgsDelegate = AmountScreenArgs::class)
fun AmountScreen(
    navigator: DestinationsNavigator,
    navController: NavController
) {
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.