Casey Brooks
11/21/2022, 6:05 PMCLOVIS
11/21/2022, 8:24 PMCLOVIS
11/21/2022, 8:24 PMCasey Brooks
11/21/2022, 8:34 PMRoute
and NavGraph
to be interfaces so that eventually this could be supported, and it would be user’s choice as to which they wanted to use.
This library doesn’t distinguish between “up” and “back” like Android does. To be honest, I’ve never much understood the difference between the two, I just know if you do the wrong thing, your navigation won’t always work like you expect it should…CLOVIS
11/21/2022, 8:36 PMclass MyParam : Parameters() {
val id by parameter<Int>()
val date by parameter<String>()
}
which is essentially just building your template, with type information during declaration.Casey Brooks
11/21/2022, 8:40 PMCLOVIS
11/21/2022, 8:43 PMTo be honest, I’ve never much understood the difference between the two.
In a nutshell, if you were previously at
/users/me
and navigated to your last post `/post/1234`:
• backward navigation (back button) brings you to /users/me
• upward navigation (arrow in the top left corner) brings you to /posts
Most of the time though, they do the same thing, because most navigation is towards a child screen. The main example is when navigating to the Android settings from the Quick Settings tiles in the notifications: backward brings you back to the home screen, whereas upward brings you to the settings menu (except of course it's not the case anymore in Android 13).CLOVIS
11/21/2022, 8:44 PMCasey Brooks
11/21/2022, 8:52 PMCLOVIS
11/21/2022, 8:54 PMCasey Brooks
11/21/2022, 9:03 PMReplaceTopDestination
with the “parent destination”?CLOVIS
11/21/2022, 9:16 PMpushState
), or a lateral navigation to a parent route (e g. JS' replaceState
)? I'm not sure.Casey Brooks
11/21/2022, 9:18 PMxxfast
11/23/2022, 1:54 PMfeature/1/details/section
) and one of the reasons why I don't quite like androidx.compose-navigation. Enums definitely makes this much more palatable but not a big fan of using strings to represent routes - although I do understand specific use-cases why this is necessary (deep-links for instance). Perhaps I'm biased but string-based routes always feels like a web-pattern that is out-of-place in Android. Android is android and we definitely don't have any address bar that the users can see or type in, so to model routes like so is a futile exercise 😅
Another criticism I have is the introduction of a type hierarchy to Compose; where views themselves don't have concrete types but are pure functions. I really adore the fact Compose didn't introduce a type hierarchy (unlike Traditional views, SwiftUI or Flutter) and instead opted in for a more functional approach with composable functions. Yet, most navigation libraries that are geared towards compose (like voyeger, appyx, decompose and now ballast-navigation) use type hierarchy for screens. I do understand that most of these libraries offer more than just navigation - they also offer state management in a lifecycle aware manner. However these tend to come with an opinionated architecture like MVI - which is not bad by itself - but sometimes you just want navigation without opting in to a whole different architecture.
Back when my app was only Android - I really hated fragment navigation (for) and instead opted in for Conductor which had a much simpler API). I stayed far far away from androidx.navigation-components
For my multiplatform app - I wanted to be able to do-my-own architecture and not be bound to a particular architecture that is enforced by a routing library. This is why I didn't go all in on the libraries I mentioned above. Ultimately I landed on using decompose - as it does provide a simple router which doesn't enforce any architecture and it doesn't introduce a type hierarchy.
Anyways, hope you take these criticisms with a grain of salt 🙂 Hoping to see Ballast Navigation come to life someday and actually get to use it.Casey Brooks
11/23/2022, 4:17 PMxxfast
11/23/2022, 10:38 PMclass HomeScreen : Screen {
@Composable
override fun Content() {
val screenModel = rememberScreenModel<HomeScreenModel>()
// ...
}
}
Here we have to extend Screen
to use the library. This brings in a type hierarchy to a view system that otherwise type-free.
Another example from Appyx
class RootNode(
buildContext: BuildContext
) : Node(
buildContext = buildContext
) {
@Composable
override fun View(modifier: Modifier) {
Text("Hello world!")
}
}
Here we have to extend Node
to use the library. This brings in a type hierarchy to a view system that otherwise type-free.
This is also true for Decompose
class SomeParent(componentContext: ComponentContext) : ComponentContext by componentContext {
val counter: Counter = Counter(childContext(key = "Counter"))
}
But the important distinction here (as @Arkadii Ivanov pointed out) is that this is not tied to the view system (as the views are pluggable). Decompose has made a clear distinction between state management and navigation and it doesn't force you to subclass/extend these library classes just to use navigation aspects of the library. In the end, with decompose - all you need are functions. No type hierarchy necessary
@Composable
fun MainContent() {
val navigation = remember { StackNavigation<Screen>() }
ChildStack(
source = navigation,
initialStack = { listOf(Screen.List) },
handleBackButton = true,
animation = stackAnimation(fade() + scale()),
) { screen ->
when (screen) {
is Screen.List -> ListContent(onItemClick = { navigation.push(Screen.Details(text = it)) })
is Screen.Details -> DetailsContent(text = screen.text, onBack = navigation::pop)
}
}
}
I know it does sound like a contradiction in my above statement - but both statements are true
I’m not trying to make the library general enough to make everyone happy, I’m working toward something specific which may not right be for you, and that’s totally fine.I definitely agree with this sentiment 🙂 I know I have done this too. It takes a lot of discipline to build up opinions on how to do something. However I think it is worth considering keeping navigation and state-management decoupled as much as possible. Although, I do understand that this is easier said than done
Casey Brooks
11/24/2022, 12:03 AMRoute
interface, it actually doesn’t impose its types anywhere else beyond that. The only thing that the Route
interface is there for, is to tell the Router how to match a URL to a Route, and from there all you need to do “routing” is collect a StateFlow
and switch on a big exhaustive when
block of that enum, neither of which requires any special type hierarchy. Decompose does have a “pluggable” View system, which decouples the Views from the library types, but Ballast Navigation doesn’t even need that. Any connection from the Router state to your View doesn’t need to be “pluggable”, because you essentially end up with a raw StateFlow<List<YourEnumRoute>>
, which you can do anything with very simply without needing special support for a given UI system. In fact, the example apps include Android using this navigation system with Fragments completely without Compose, and nothing changes on the part of the navigation library or how you setup and use the Routes to support that.
And to your point about “keeping navigation and state-management decoupled as much as possible”, it actually is a fully decoupled navigation library, but in interest of keeping the documentation and library opinionated, I’ve omitted that from the docs. Ballast itself is the state management engine, but I was very intentional about keeping anything from the core library outside of anything needed for the routing aspects beyond the initial setup. Part of the goal of the library is to be opinionated and avoid documentation that details every possible way to use the library, which makes it hard to get started. Instead, I’ve focused on the primary use-case which gets you set up simply and quickly.
That said, I’ll post a snippet below that actually fully implements the routing logic without any hard connection to the base Ballast library, so you actually could plug it your preferred state management system (or just leave it purely in Compose as-is). But actually using it like this, it loses many of the things that I would deem pretty crucial to proper routing, especially transactional updates, protection of the state, and the ability to request route changes outside of the UI (for example, the repository layer getting a 401 and redirecting to the login screen). So it could be used in a way without directly tying it to Ballast at all, but proper state management is pretty critical to a good router and I wanted Ballast and its other modules like undo/redo to be a big feature of using this router, along with making the library work out-of-the-box without needing to go find/build your own state management subsystem to integrate with it.
@OptIn(ExperimentalBallastApi::class)
fun MainContent() {
Column {
val navGraph = remember { NavGraph.fromEnum(AppScreen.values()) }
var routerState: Backstack<AppScreen> by remember {
val initialRoute = AppScreen.PostList
val initialRouteUrl = initialRoute.directions()
val initialDestination = Destination.Match(initialRouteUrl, initialRoute)
mutableStateOf(listOf(initialDestination))
}
MainContent(routerState) { input ->
val navigator = object : BackstackNavigator<AppScreen> {
override val backstack: Backstack<AppScreen>
get() = routerState
override fun matchDestination(
destinationUrl: String,
extraAnnotations: Set<RouteAnnotation>
): Destination<AppScreen> {
return navGraph.findMatch(UnmatchedDestination.parse(destinationUrl, extraAnnotations))
}
override fun updateBackstack(block: (Backstack<AppScreen>) -> Backstack<AppScreen>) {
routerState = block(routerState)
}
}
with(input) {
navigator.navigate()
}
}
}
}
@OptIn(ExperimentalBallastApi::class)
@Composable
fun MainContent(
routerState: Backstack<AppScreen>,
updateRoute: (RouterContract.Inputs<AppScreen>) -> Unit,
) {
// ...
}
xxfast
11/27/2022, 11:32 PMRoute
, I have strong opinions against string-based routes in Android - but I understand there's specific use cases that warrants it. But if I have to have string-based routes - your approach is a very good implementation of this; where I can rely on some type safety.
Looking at the further examples you pointed out - looks like BalllestNavigation
can be fully decoupled from the ballest state management and with a little bit of code you can make it work with your own state management library - which is pretty sweet 👍 I agree that your navigation library doesn't enforce types for your screens the same way voyager or Appyx does. Perhaps it is better to point this out in the docs - but I respect if you want to keep your docs opinionated toward ballest state management