Hello. I am trying too bypass Voyager's navigation...
# compose
v
Hello. I am trying too bypass Voyager's navigation limitation for passing callbacks into it's destination (class Screen). So every Voyager sample shows that FeatureA just coupled to any other FeatureX so it can navigate to it navigator.push(FeatureX() <- coupling). I need to hoist the navigation intent to upper level, likely App level to de-couple features. Isn't it too weird (critically bad) to use static composition local for basically a way to hoist callback? Code sample in the 🧵
Copy code
val LocalAppNavigator: ProvidableCompositionLocal<MyNavigator> = staticCompositionLocalOf {
    error("")
}

interface MyNavigator {
    fun process(request: NavigationRequest)
}

class FeatureA : Screen {
    
    @Composable
    override fun Content() {
        val appNavigator = LocalAppNavigator.current
        
        FeatureScreen(
            onNavigationRequest = {
                appNavigator.process(it)
            }
        )
    }

}

@Composable
fun App() {
    // Voyager
    Navigator(
        screen = FeatureA()
    ) { navigator ->

        val appNavigator = remember(navigator) {
            object : MyNavigator {
                override fun process(request: NavigationRequest) {
                    navigator.push(
                        item = when (request) {
                            is NavigationRequest.ToFeatureX -> FeatureX()
                        }
                    )
                }

            }
        }

        CompositionLocalProvider(LocalAppNavigator provides appNavigator) {
            CurrentScreen()
        }
    }
}
s
That's quite a limitation, does voyager not let you pass whatever callback you want into the screens? How do they expect you to do cross-module navigation there?
v
How do they expect you to do cross-module navigation there?
That exactly where I am currently stuck lol, trying to modularize my project. No, it doesn't. It saves fields of the Screen into android bundle, so only those types are allowed
s
Can you opt out of that saving somehow for specific fields? And rely on passing them in again after a process death?
v
Potentially can opt-out from the restoration at all, but that is no go
s
It looks like the project is no longer actively maintained, as author mentioned he no longer has time to maintain it.
v
Yea, I saw that. The Author mentioned that they have IRL issues and they are the only main contributor. And considering that JP Navigation migrated to KMP it probably doesn't help for smaller project to continue with navigation. And since I modularizing the project, most of the concepts are incapsulated anyway, to make future likely migration smooth
😢 1
I am just trying to figure out how do I navigate between my destinations after I de-couple them. And the hoisting-via-local-composition seem to be working, not much mess, I just not sure if it can somehow do bad for Compose itself
s
If you can't migrate away, just do the composition local approach until you're able to use another library. Gotta compromise somehow I suppose.
z
Fwiw, even if the architecture Im using supports sending results to parent; its nice to have a way to send results all the way to the root parent at times (without sending one result up the chain "manually"). Doing something like your example for that, just outside of compose.
v
If outside of compose it is quite simple, I also have that secondary "global" navigation helper, which can be used anywhere, compose included. Like from Notifications manager to open something. And it is also passed into CompositionLocal from very top or could be just injected either in compose or not. Just passing it into Local feels more "compose way" rather than fetch it from DI
k
I would say it’s not Voyager’s limitation but rather Android’s limitation. In Jetpack Compose’s navigation (and in any other navigation library that wants to properly support the process death), you can also pass only primitives or
@kotlinx.Serializable
or
Parcelable
, so none of them allow for direct passing of callbacks. It is recommended not to overuse CompositionLocals as it introduces hidden dependencies, but if it works for you, then why not Alternatively, you could do
Copy code
val appNavigator = FavoriteDiFramework.inject()
or make
MyNavigator
a singleton, inject it into the presenters, and proces requests by:
Copy code
val screen = ...
findNavController().navigateTo(screen)
s
I would say it's not Voyager's limitation but rather Android's limitation
That would be true if it were not completely trivial to pass callbacks like that in other navigation libraries.
z
If you just separate the callback from the state, and the state dictates the navigation graph, then you can build the same graph after restoring state and have callbacks working. I don't know why this is as uncommon as it is, you will inevitably run into complex scenarios where you need to communicate between components in ways that an external service can't achieve. Decompose and workflow are two libraries that embrace this concept.
k
So, in those libraries, is it possible to do the following?
Copy code
class ViewModel(navigator: Navigator) {
    fun handleSomething() {
        navigator.navigateTo(
            Screen(id) { result ->
                doSomething(result)
            }
        )
    }
}
z
Yes. I believe circuit also does that (another library)
k
Interesting, I can’t imagine how it could be achieved, but I will definitely take a look at them 🙂 thanks!
fist bump 1
I haven’t checked others yet, but Circuit doesn’t support that: https://slackhq.github.io/circuit/navigation/#results Their
Screen
is also
Parcelable
, and they have a separate mechanism (similar to
rememberLauncherForActivityResult
) to deliver results 🙂
a
Decompose achieves that by having separate entities for arguments (aka component configurations) and actual component classes. You instantiate a configuration class (serializable) and pass it to the navigator, you also supply a factory function that creates a new instance of the component classes given a configuration instance. In that factory you call component constructors manually and you can supply any arguments, like callbacks, repositories, use cases, etc. So, a bit more boilerplate code for proper DI and IoC.
very nice 1