Is it possible to wait for effects down a composab...
# compose
j
Is it possible to wait for effects down a composable hierarchy to run before rendering the UI? I.e. ComposableA enters the composition, runs some effects, calls a child composable which also runs effects. I would like to prevent rendering any UI down this hierarchy until both effects have run, is this possible? In React I think this would be achievable with useLayoutEffect
a
Can you describe your use case?
j
Trying to determine if it’s possible to implement a navigation builder syntax similar to react-navigation where you can compose screens declaratively by nesting routes:
Copy code
Route<ScreenA> {
   SomeSharedUI()
   
   Route<ScreenB> {
     ScreenB()
   }

   Route<ScreenC> {
     ScreenC()
   }
}
This means the Route<T>() builder will need to run in a composable context. I want the start destination to be the first route positionally, which would be ScreenB. To achieve this, Route<T>() runs an effect to push its first child route onto the backstack, which will render in the next composition. The problem is this effect will run after SomeSharedUI() has rendered causing a cascade of UI appearing. I want the entire route hierarchy to render in a single frame
a
A formula I've used quite a bit for builders is:
Copy code
@Composable
fun Thing(builder: FooBuilder.() -> Unit) {
  val currentBuilder by rememberUpdatedState(builder)
  val foo by remember { derivedStateOf { FooBuilder().apply(builder).build() } }
it works for navigation route builders, LazyColumn adapters, etc.
it ensures the built structure is always up to date and available during composition, but it does strictly separate
@Composable
content from building the object. You would need to wrap your
SomeSharedUI()
call in something like
Copy code
Route<ScreenA> {
  Shared {
    SomeSharedUI()
  }
  // ...
j
Yeah, I’ve been using something pretty similar to that to keep the structure up to date. My goal was specifically to see if it’s possible to mix the @Composable content with the builder unfortunately, since it makes for a pretty convenient routing syntax
a
yeah as you've found though, it makes for some very circuitous data dependencies depending on what you let the caller express
composition really wants strict inputs and outputs
and effects are firmly outputs
you can do a little bit with things like SubcomposeLayout for things that are strictly children, but that comes with an overhead and warped expectations created for callers that really aren't worth the syntactic sugar you're gaining over a single wrapper for the shared content like the above
if you treat this as a little more of a nested navigation style you could probably do something like:
Copy code
RouteSharedContent<ScreenA> {
  SomeSharedUI()

  Routes {
    Route<ScreenB> {
      ScreenB()
    }
    Route<ScreenC> {
      ScreenC()
    }
  }
}
you kind of have to pick what part you want to have the wrappers though to keep your route declarations and the content of those routes separate
hmm, I suppose you could collapse those, treat
Copy code
Route<ScreenB> {
  ScreenB()
}
the same as
Copy code
Routes {
  Route<ScreenB> {
    ScreenB()
  }
}
and probably still make it work in a single pass
but this is also still a lot of infrastructure to avoid writing a content wrapper as a helper for the shared content, and just using it in a few different routes
Copy code
@Composable
fun MyContentScaffold(..., content: @Composable () -> Unit) {
  // stuff
  content()
}

// in your routing definitions
Route<ScreenB> {
  MyContentScaffold(..) {
    ScreenB()
  }
}
Route<ScreenC> {
  MyContentScaffold(..) {
    ScreenC()
  }
}
depending on how much shared state you want to keep around in the scaffold as you navigate. But even then,
movableContentOf {}
is going to give you some more options there too.
not to mention standard state hoisting and just pulling your custom scaffold state out of the routing composable to share it manually
j
Yeah I was about to mention the use case for sharing state across destinations, but I’ve never heard of movableContentOf
a
it's not merged yet but should show up in some upcoming compose-runtime:1.2 alphas soon
it's the new name of the composition as a value feature that has been discussed in this channel here and there
roughly speaking it'd let you do:
Copy code
val sharedContent = remember { movableContentOf { SomeSharedUI() } }

RoutingTable {
  Route(...) {
    sharedContent()
    ScreenA()
  }
  Route(...) {
    sharedContent()
    ScreenB()
  }
}
and that instance of
SomeSharedUI()
would retain all of its remembered composition state as it moves between destinations
(with some limitations if you're displaying both destinations concurrently during transition animations; this is why it can still be useful to represent this sort of thing as nested navigational structure for shared chrome elements)
j
That does sound great, look forward to seeing it. It still feels a bit odd defining the shared content outside the routing table making it accessible to all routes. My original thought was by using composition as the routing table you’d simply be able to declare it in the scoped region of the navigation table where you need it (only accessible to the routes which it should be shared amongst)
a
I'd probably do this with some sort of nested navigation arrangement for that reason
treat the outer and inner parts as separate routing tables, so to speak
j
Yep, in my implementation a Route<T>() is just a nested route table, my only problem is I couldn’t get it happening in a single pass. I think it’s only a problem for the initial display when we need to choose a starting route
A throwaway remember inside Route<T>() solves it as a workaround instead of effects, how much trouble will this get me in
Copy code
remember(backStackIsEmpty) {
    if (backStackIsEmpty) backStack.push(route)
    null
}