raamcosta / compose-destinations

Annotation processing library for type-safe Jetpack Compose navigation with no boilerplate.
https://composedestinations.rafaelcosta.xyz
Apache License 2.0
3.19k stars 132 forks source link

Deeplinks and BuildConfig, code not generated #178

Open JustJerem opened 2 years ago

JustJerem commented 2 years ago

Hello Rafael, First of all, I would like to thank you for this great library. A real pleasure to use every day ! 👏

My issue is that I trying to use DeepLinks with a changing configuration, thanks to BuildConfig. The application compiles, but it crashes when launching on the phone with this error:

java.lang.IllegalStateException: The NavDeepLink must have an uri, action, and/or mimeType.

And indeed, looking in the generated code, nothing appears in the Deeplinks variable.

override val deepLinks get() = listOf(
    navDeepLink {
    }
)

This works perfectly if I hardcode the URL in the right field, but when I pass it in the BuildConfig, it doesn't generate. And I need it because our application has 4 environments.

Current state :

DashboardScreen.kt

const val DEEPLINK_DASHBOARD_URI = "https://website.com/dashboard"
@Destination(deepLinks = [
    DeepLink(action = DEEPLINK_DASHBOARD_URI)
])
@Composable
fun DashboardScreen(
    viewModel: DashboardViewModel = hiltViewModel(),
    navigator: DestinationsNavigator,
) {
...
}

What I try to achieve :

build.gradle

buildConfigField "String", "DEEPLINK_DASHBOARD_URI", "\"https://website.com/dashboard\""

DashboardScreen.kt

@Destination(deepLinks = [
    DeepLink(action = BuildConfig.DEEPLINK_DASHBOARD_URI)
])
@Composable
fun DashboardScreen(
    viewModel: DashboardViewModel = hiltViewModel(),
    navigator: DestinationsNavigator,
) {
...
}

Is there a solution ? Sorry if I missed something in the documentation.

raamcosta commented 2 years ago

Hi 👋

Thank you for the kind words, glad you like it!

I believe this can be due to ksp task running before the build config class is created when you build the app. But I see how good this could be, I’ll leave this open as a future enhancement and I’ll try to check if I can solve it somehow 🤔

In the meantime, one thing you can do is, right before calling DestinationsNavHost, you copy your root NavGraph (since it is a data class) and reach to the Destination you want to add the deep link, then call this (line 112 - withDeepLink).

https://github.com/raamcosta/compose-destinations/blob/main/compose-destinations/src/main/java/com/ramcosta/composedestinations/dynamic/DynamicDestinationSpec.kt

This will create a copy of the generated Destination with the passes in deep link. And this way you can set deep links at runtime. Remember you have to change the NavGraph you pass to DestinationsNavHost, and replace the destination with this copy of itself in the destinations list that the NavGraph contains. If you want I can show you an example later on.

This method was not intended for this specific use case but I believe it should work, but please let me know 🙂

MichielDroid commented 2 years ago

Hiya! @raamcosta Can you show us an example please? I don't quite understand what you mean.

I have an app with multiple build flavors, and the scheme for the deeplinks is based on the build.FLAVOR.

I wrote this code based on your answer. Works for a little bit, until I open a notification with a deeplink. Then it throws the following exception:

 java.lang.IllegalArgumentException: Registering multiple navigation graphs with same route ('newRoot') is not allowed.
        const val GENERAL_ALERT_DEEPLINK_URL = "${BuildConfig.FLAVOR}://generalAlert"
        const val ALERT_DEEPLINK_URL = "${BuildConfig.FLAVOR}://alert/{alertId}"
        private var navGraph: NavGraph? = null

 internal fun regenerateRootNavGraph(): NavGraph {
        if (navGraph == null) {
            val newRoot = NavGraphs.root.copy(route = "newRoot")

            val destinations = newRoot.destinations.toMutableList()
            destinations.remove(SingleAlertDetailScreenDestination)
            destinations.remove(GeneralAlertDetailsDestination)

            SingleAlertDetailScreenDestination.withDeepLink {
                uriPattern = ALERT_DEEPLINK_URL
            }.routedIn(newRoot)

            GeneralAlertDetailsDestination.withDeepLink {
                uriPattern = GENERAL_ALERT_DEEPLINK_URL
            }.routedIn(newRoot)

            navGraph = newRoot.copy(destinations = destinations)
        }
        return navGraph!!
    }
cvb941 commented 2 years ago

Hi, you can try adding the code below to your build.gradle, it should make the BuildConfig class available to KSP.

ksp {
    allowSourcesFromOtherPlugins = true
}
ryanholden8 commented 2 years ago

Hi, you can try adding the code below to your build.gradle, it should make the BuildConfig class available to KSP.

ksp {
    allowSourcesFromOtherPlugins = true
}

I get this build error when using this option:

Circular dependency between the following tasks:
:app:kaptDebugKotlin
\--- :app:kaptGenerateStubsDebugKotlin
     \--- :app:kspDebugKotlin
          \--- :app:kaptDebugKotlin (*)

@cvb941 - Where you able to get it working? If so, mind sharing a few snippets of the code?

cvb941 commented 2 years ago

Sorry, I just enabled that option and it worked, nothing special. I am not using kapt though.

MichielDroid commented 1 year ago

@ryanholden8 Did you find a solution or maybe a workaround? I think my project requires kapt for Hilt, and ksp for destinations. I also need to set ksp. allowSourcesFromOtherPlugins to true because we have build flavors and we need deeplinks based on build flavors.

I feel like there must be a way to have ksp_gradleTask.dependsOn(BuildConfig_gradleTask), but I'm not very familiar with ksp and gradle.

ryanholden8 commented 1 year ago

@Miiel - We did not, we duplicated the references. Not ideal but not much of a choice.

javierpe commented 1 year ago

@raamcosta I have many issues with this. Did you solve it?

How can I help you to solve it to send Pull Request?

ninovanhooff commented 1 year ago

The way I solved this was to register a single pattern as documented, using an app scheme, for example:

Screen @Destination annotation

deepLinks = [
        DeepLink(
            uriPattern = "myapp://groups/join?token={token}",
        ),
    ]

Then, in the MainActivity, I used traditional url- handling to transform environment (prod, dev, acc) specific https urls in to the registered myapp:// pattern and feed that into the navigator

MainActivity kotlin:


 override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContent {
          val navController = rememberAnimatedNavController()
          DeeplinkHandler(navController)

         AppNavigation(
            navController = navController,
            navGraph = NavGraphs.root,
            startRoute = viewModel.startRoute,
          )

    }

    deeplinkManager.onNewIntent(intent) // important!
}

 override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        this.intent = intent
        deeplinkManager.onNewIntent(intent)
    }

DeeplinkManager

@Singleton
class DeeplinkManager @Inject constructor(
    private val deeplinkParser: DeeplinkParser,
) {

    private val _deeplinkFlow = MutableStateFlow(Uri.EMPTY)
    val deeplinkFlow: Flow<Uri> = _deeplinkFlow

    fun onNewIntent(intent: Intent?) {
        Napier.d {
            "onNewIntent received with data ${intent?.data} and extras ${
                intent?.extras?.keySet()?.toList()
            }"
        }
        if (intent == null) return

        deeplinkParser.parseIntent(intent)?.let {
            _deeplinkFlow.value = it
        }
    }

    fun onDeeplinkHandled() {
        _deeplinkFlow.value = Uri.EMPTY
    }
}

DeeplinkParser: A class transforming https://<dev | acc | prod>.mycompany.com/join?token=ABCDEF to myapp://join/token={token}

DeeplinkHandler

@Composable
    private fun DeeplinkHandler(navController: NavHostController) {
        val deeplink by deeplinkManager.deeplinkFlow.collectAsStateWithLifecycle(
            initialValue = Uri.EMPTY
        )

        if (deeplink != Uri.EMPTY) {
            Napier.d { "Navigating to deeplink $deeplink" }
            navController.navigate(deeplink)
            deeplinkManager.onDeeplinkHandled()
        }
    }
amaranthius commented 9 months ago

A bit late to the party, but just wanted to share a workaround that worked for me. Might be helpful to some folks out there.

I've basically used this suggestion:

ksp {
    allowSourcesFromOtherPlugins = true
}

but with this extra step:

tasks.withType<com.google.devtools.ksp.gradle.KspTaskJvm> {
    mustRunAfter("generateDebugBuildConfig")
}

This little block makes sure that BuildConfig would've been generated by the time KSP starts to do its thing. Bear in mind that, depending on your particular case, there might be more tasks that need to be added to mustRunAfter.

Of course, having an elegant out-of-the-box solution would be way better, but this is not too ugly for a workaround. Hope. this helps!

pm-nchain commented 7 months ago

@raamcosta is it possible to improve error message in this situation? Error like following is kind of misleading.

 java.lang.IllegalArgumentException: Deep link null can't be used to open destination Destination(0xa4735ba4) route=verify_email_username/{email}/{code}.
 Following required arguments are missing: [email, code]
    at androidx.navigation.NavDestination.addDeepLink(NavDestination.kt:355)
    at androidx.navigation.compose.NavGraphBuilderKt.composable(NavGraphBuilder.kt:104)
    at androidx.navigation.compose.NavGraphBuilderKt.composable$default(NavGraphBuilder.kt:78)
    at com.ramcosta.composedestinations.spec.DestinationStyleKt.addActivityDestination(DestinationStyle.kt:158)
    at com.ramcosta.composedestinations.DefaultNavHostEngine.composable(DefaultNavHostEngine.kt:123)
    at com.ramcosta.composedestinations.DestinationsNavHostKt.addNavGraphDestinations(DestinationsNavHost.kt:115)
    at com.ramcosta.composedestinations.DestinationsNavHostKt.access$addNavGraphDestinations(DestinationsNavHost.kt:1)
    at com.ramcosta.composedestinations.DestinationsNavHostKt$addNestedNavGraphs$1$1$1.invoke(DestinationsNavHost.kt:142)
    at com.ramcosta.composedestinations.DestinationsNavHostKt$addNestedNavGraphs$1$1$1.invoke(DestinationsNavHost.kt:141)
raamcosta commented 5 months ago

Does anyone know if this is still the case?

amaranthius commented 5 months ago

Does anyone know if this is still the case?

@raamcosta Is it supposed to be working? Haven't tried since I've put in place the workaround I've mentioned earlier