Hey, i was playing around with the `sharedElement`...
# compose
j
Hey, i was playing around with the
sharedElement
modifier and ended up creating a wrapper modifier for easier usage. It looks like this:
Copy code
@Composable
fun Modifier.sharedElementModifier(
    key: String,
): Modifier {
    val sharedTransitionScope = LocalSharedTransitionScope.current
    val animatedVisibilityScope = LocalAnimatedContentScope.current
    with(sharedTransitionScope) {
        return animatedVisibilityScope?.let {
            return this@sharedElementModifier then Modifier.sharedElement(
                rememberSharedContentState(key = key),
                animatedVisibilityScope = animatedVisibilityScope,
            )
        } ?: this@sharedElementModifier
    }
}
Is something like that possible with the new
Modifier.Node
api? And do you see any performance issues by using this wrapper?
z
The problem with grabbing composition locals when the modifier is created is that the modifier you return could be used somewhere else. Modifier factories also shouldn’t be composable — historically, this is what
composed
was for. But now a modifier node can read composition locals so you don’t need to read them ahead of time.
s
What I have done was just make this modifier
Copy code
fun Modifier.sharedElement(
  sharedTransitionScope: SharedTransitionScope,
  animatedVisibilityScope: AnimatedVisibilityScope,
  state: SharedContentState,
  boundsTransform: BoundsTransform = DefaultBoundsTransform,
  placeHolderSize: PlaceHolderSize = contentSize,
  renderInOverlayDuringTransition: Boolean = true,
  zIndexInOverlay: Float = 0f,
  clipInOverlayDuringTransition: OverlayClip = ParentClip,
): Modifier = with(sharedTransitionScope) {
  this@sharedElement.sharedElement(
    state = state,
    animatedVisibilityScope = animatedVisibilityScope,
    boundsTransform = boundsTransform,
    placeHolderSize = placeHolderSize,
    renderInOverlayDuringTransition = renderInOverlayDuringTransition,
    zIndexInOverlay = zIndexInOverlay,
    clipInOverlayDuringTransition = clipInOverlayDuringTransition,
  )
}
where on the call site you do this
Copy code
.sharedElement(
  LocalSharedTransitionScope.current,
  LocalNavAnimatedVisibilityScope.current,
  rememberSharedContentState(contract.id),
)
Honestly I prefer this one since sometimes you will in fact not want to grab the visibility or transitionscope from your CompositionLocals. You might have a different instance you want to pass in for transitions tied perhaps to something other than your navigation, or to some transitions that are in a smaller scoped transitionScope.
j
Thank you! I ended up creating a similar extension like @Stylianos Gakis
e
I don't think that call site will work since
rememberSharedContentState
is a function on
SharedTransitionScope
s
Right, that looks like another wrapper function I made which also defaults to grabbing the scope from LocalSharedTransitionScope
e
Wouldn't that have the same issue that Zach was mentioning since it's Composable? Or is it caller beware (which is the approach that I took with my helper function)?
s
That function internally is just
Copy code
@Composable
fun rememberSharedContentState(key: Any): SharedContentState {
  return LocalSharedTransitionScope.current.rememberSharedContentState(key)
}
So in the call site when you write
Copy code
.sharedElement(
  LocalSharedTransitionScope.current,
  LocalNavAnimatedVisibilityScope.current,
  rememberSharedContentState(id),
)
You practically write
Copy code
.sharedElement(
  LocalSharedTransitionScope.current,
  LocalNavAnimatedVisibilityScope.current,
  LocalSharedTransitionScope.current.rememberSharedContentState(id),
)
So you use the same scope by default for these two. Of course it's possible to forget about this, and pass a different scope on the first parameter, and use a different scope for the
rememberSharedContentState
call, if that is what you were worried about. Otherwise these are both composition local reads next to each other, they should always properly resolve to the current local which must be the same in the same function call. If you hold the result of
rememberSharedContentState
and pass it further down in some way where the local may have changed, sure that could be a problem too.
t
Sorry to bring this back up. It looks like your CompositionLocals for the LocalSharedTransitionScope seem to be not nullable. Looking at https://developer.android.com/develop/ui/compose/animation/shared-elements#understand-scopes however seems to suggest making your composition locals nullable. How did you define your composition locals?
Also how do you go about providing the AnimatedContentScope to your screen composables? Do you have a top-level AnimatedContent providing the AnimatedContentScope or do you provide it in each composable call of your navgraph?
s
It's a decision you can make yourself. I made them not null, and I expect all navigation screens to provide the local, otherwise it's a crash at runtime. It's a choice you can make yourself. You can make them nullable in order to just fallback to doing nothing instead of actually crashing
You must use the right AnimatedContentScope, which for the screen level transitions will be the one provided for you by your NavHost when you use the composable() function to define a screen. The composable lambda gives you the right scope as the receiver of that lambda.
t
Thank you Stylianos, however I don't quite get how I would define the composition local for the scopes. When I try to pass an object implementing the corresponding interface with only no-ops, intellij shows me visibility-related errors
If you do want to "no-op' instead of crashing, then just make it nullable and default to null instead tbh, that'd be much preferable
t
Ah yeah, of course 🤦 didn't think of just erroring out. Thank you for your help 😊
👍 1