I having doubts about the Compose Navigation and D...
# compose
d
I having doubts about the Compose Navigation and Deep Linking. This is a simple master/detail app navigation. As you can see, I am loading the detail data, when clicking on a master list item. It works. But this wouldn’t work when deep linking to the detail screen, as it would show the DetailView without having loaded the data. Ideally, I would like to associate somehow the
model.loadDetailData(it)
call to the
composable("detail/{item}")
, but I can’t find a Compose API to do that. If I include the call inside the
composable
block, above
DetailView
, such call gets executed at each recomposition, creating a never-ending loop.
Copy code
@Composable
fun Navigation(model: KMPViewModel) {
  val appState by model.stateFlow.collectAsState()
  val navController = rememberNavController()

  NavHost(navController, startDestination = "master") {
    composable("master") {
      MasterView(masterState = appState.masterState,
        onListItemClick = {
          navController.navigate("detail/$it")
          model.loadDetailData(it)
        }
      )
    }
    composable("detail/{item}") {
      DetailView(detailState = appState.detailState)
    }
  }

}
what is the right way to make it work with deep linking?
i
It sounds like your Detail Composable needs to be one loading the data it needs. The same would be the case after process death/recreation or a config change
☝️ 2
d
on SwiftUI it’s easy, by using the
onAppear
function on the DetailView. But on Jetpack Compose I don’t know how to trigger a function when a Composable first appears (and not on subsequent recompositions).
b
LaunchedEffect is probably what you are looking for in terms of “thing that is called only on the first composition”
d
@Bryan Herbst thanks, it sounds like right!
I rewrote the NavHost like this:
Copy code
NavHost(navController, startDestination = "master") {
    composable("master") {
      MasterView(masterState = appState.masterState,
        onListItemClick = { navController.navigate("detail/$it") }
      )
    }
    composable("detail/{item}") { backStackEntry ->
        val item = backStackEntry.arguments?.getString("item")!!
        LaunchedEffect(item) { model.loadDetailData(item) }
        DetailView(detailState = appState.detailState)
    }
}
it works! Thanks again!
@Ian Lake @Bryan Herbst One thing I just noticed is that
LaunchedEffect
is triggering an extra recomposition.
model.loadDetailData()
on its own is causing the state to change twice (correctly, as one is for setting
appState.detailState.isLoading
to true before fetching the data, and one is to set the
appState.detailState.detailData
object after fetching it from the repository. • If I call
model.loadDetailData()
from the master onListItemClick, as in the first example, the recomposition happens 2 times. • If I call
model.loadDetailData()
from the detail LaunchedEffect the recomposition happens 3 times.
i
LaunchedEffect
runs the frame after first composition, so that sounds expected. You could certainly set your default appState to your loading state
d
That way would be a bit “dirty”, as you would need somehow to “reset” the state each time you exit a detail screen. I think it would be ideal to have an API that associates some code to a navigation
composable
and executes it just before the first composition, similar to the
onAppear
function in SwiftUI.
i
That sounds like a consequence to your single
appState
- you could certainly have each destination have its own state that is automatically cleared when that destination is popped off the back stack
d
I can explore it, but from my experience, there are great advantages in having a single app state. You can remove a whole lot of boilerplate code, by having just one StateFlow instance.
i
Scoping state to the right place is a much bigger discussion, but I'd say if you want to scope your state such that is it cleared when the destination is popped, scoping it to the destination (and not hoisting it outside of that scope) seems intuitively more correct than dogmatically hoisting everything when that's specifically not what you want here
d
The solution for all this would be very simple. Just adding an optional parameter to the Navigation Composable, where to specify a block of code to be executed before the first composition.
i
Navigation Compose follows the same rules as every other composable; that will always be the case
That includes how state saving, state hoisting,
LaunchedEffect
, and any other Compose API should be used
d
Ok, I see the reason behind your previous answer now. Yes, it is actually a composable, I was kind of forgetting that. I think being able to run some code before presenting a navigation destination is an essential pattern for a framework that wants to enable Deep Linking. Requiring a separate ViewModel (and State object) for each
Navigation Composable
(excluding the chance to hoist the state outside the scope), doesn’t seem right to me. There are screens, with different navigation addresses, that should be able to share a ViewModel. The current solution of using
LaunchedEffect
and triggering an extra recomposition is definitely not ideal. I think for the moment, I will still run the
loadDetailData()
function from the master list item onClick, as I don’t have the need to implement deep linking. But I hope that this issue will be taken into consideration for the 1.0 version. If it helps, I could file an issue about that.
i
"Requiring", no. Using it when you want state that is tied to the lifetime of that destination, yes. I think you'll find only relying on a global state is going to scale extremely poorly when it comes to memory and modularization in your app. Best of luck.
👍 1
d
I hope Compose Navigation has not been built with this limitation in mind. There should be nothing wrong in having a state object that spans several screens. I don't see any problem in modularization, as long as the AppState is nested in subobjects for the different screens. And in terms of memory, there wouldn't be any issue either, as in each session a user would just visit a very limited amount of screens which would populate their correspondent substates. And in any case, you always have the chance to "nullify" the state of a screen in the (very remote) case it really affects the memory of your device. For at least 90% of apps, I don't really see any need of releasing some of this memory. On the other hand, having a single AppState removes a big amount of tedious boilerplate code. I have already built a few apps with this concept for both JetpackCompose and SwiftUI, and the outcome is outstanding. The only ever issue I found with JetpackCompose is the one I am describing in this post. And it just has to do with deep linking and the lack of an API which allows to run some code to load the data before showing a navigable screen.