AndroidX navigation + :api
/impl
split for feature modules
#2
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
What
This is an attempted solution to use the
:api
/:impl
modularisation split for feature modules.Why
Constraints:
:impl
modules can only depend on other features':api
moduleThis constraint is key because, when using Jetpack navigation, the destinations are created using extension functions on
NavGraphBuilder
and directly reference screen composables. E.g.:nowinandroid/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/navigation/TopicNavigation.kt
Lines 34 to 46 in afb6741
Making this function
public
and using it where the screen may be needed will result in any using (dependent) modules becoming dependant on the concrete feature implementation. This defeats the purpose of the api-impl split - that dependant modules only depend on the public abstraction, and not concrete implementation.How
The simple version
FeatureNavigator
interface from:api
that provides ascreen
function.:impl
setContent
is called)The additional stuff
Injecting and passing down a
FeatureNavigator
interface for each destination is impractical. So, I created aNavigatorProvider
. It hosts instances of all navigators, and provides a getter for getting any navigator. We only inject this into compose content. We fetch the correct navigator where we want.In practice
One time setup
Navigator
interface:nowinandroid/core/common/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaNavigator.kt
Lines 16 to 36 in d021b97
NavigatorProvider
interface:nowinandroid/core/common/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NiaNavigatorProvider.kt
Lines 7 to 9 in d021b97
NavigatorProviderImpl
that takes a map ofNavigator
class to instancenowinandroid/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavigatorProviderImpl.kt
Lines 7 to 14 in d021b97
NavigatorProvider
into your compose hierarchynowinandroid/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt
Line 137 in d021b97
For the purpose of this sample, I added the
NavigatorProvider
instance to the existingNiaAppState
which was already being passed around. A more practical solution may be to provide it as a static Composition Local and use it directly where needed.Also, I added the
Navigator
andNavigatorProvider
interfaces to the existing:core:common
module. They'd be better located in a lean:core:navigation
module.Setup for each destination/route
Navigator
in the:api
module.Properties
data classActions
classnowinandroid/feature/bookmarks/api/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigator.kt
Lines 7 to 21 in d021b97
internal
implementation for this in the:impl
module (:impl
depends on:api
)nowinandroid/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigatorImpl.kt
Lines 26 to 47 in d021b97
nowinandroid/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/di/BookmarksSingletonModule.kt
Lines 22 to 23 in d021b97
NavigatorProviderImpl
nowinandroid/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/di/BookmarksSingletonModule.kt
Lines 17 to 19 in d021b97
That's all the setup on the implementation side of the destination.
Adding and navigating to the destination
impl
should depend on the destination'sapi
module.For instance, to add
FeatureA
as destination insideFeatureB
's subgraphy,featureB:impl
would depend onfeatureA:api
NavigatorProvider
to get an instance of the destination specific navigator, and call itsscreen
functionnowinandroid/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt
Lines 67 to 75 in d021b97
navigateToRoute
function on the navigatornowinandroid/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt
Lines 172 to 177 in d021b97
Future maybe: annotations
I'm completely unfamiliar with creating custom annotations and using KSP to generate code. If/when I know how to do that, we could get rid of a lot of code here.
We could mark the destination composable with say
@Destination(<RouteClass>)
- similar to how Circuit uses@CircuitInject
and Enro uses@NavigationDestination
.This annotation could be then used as a hook to generate all the code for that route class - the route specific interface, its implementation with that annotated function, maybe even the navigator provider's implementation with all the annotated & generated navigators added in, and all the relevant dagger modules.
With such an annotation based setup in progress, the
:api
module would only need to expose a Route data class/object. But all that's still far in the future.(Missed cost receivers while iterating on this. They'd have made those interface functions so much cleaner)