https://kotlinlang.org logo
#compose
Title
# compose
b

Bradleycorn

02/28/2021, 4:37 PM
While we're on the subject of
ModalBottomSheetLayout
... I have one setup to show a simple
Text
composable in the bottom sheet. But if I try to pass in the text to show, it doesn't work. The bottom sheet doesn't even show up. But if I "hard code" the text, it works fine. Code posted in the thread ...
Copy code
@ExperimentalMaterialApi
@Composable
fun BottomSheetScreen(message: Int?) {
    val bottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
    val scope = rememberCoroutineScope()

    if (message != null) {
        scope.launch { bottomSheetState.show() }
    }

    ModalBottomSheetLayout(
        modifier = Modifier.fillMaxSize(),
        sheetContent = {
            BottomSheet(message = message)
        },
        sheetState =bottomSheetState
    ) {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("The message is:")
            Text(message?.let { stringResource(id = it) } ?: "")
        }
    }
}


@Composable
fun BottomSheet(@StringRes message: Int?) {
    val output = message?.let { stringResource(id = it)} ?: ""

    // this doesn't work. I've logged the "output" and verified that is has proper text. 
    Text(text = output, modifier = Modifier.padding(vertical = 50.dp))

    // If I do this instead, it works fine:
    //Text(text = stringResource(R.string.message), modifier = Modifier.padding(vertical = 50.dp)) 
}
In the above, note that in the
BottomSheet
composable, if I try to set the text of the
Text
composable, using the passed in
message
string resource, it doesn't work. The bottom sheet does not get displayed at all. However, if I don't used the passed in message, and instead just set the
Text
composable using a "hard coded" string resource, all of a sudden everything works great. What gives? I have verified via debugging and log messages that the passed in
message
is a valid string resource identifier, and that the
stringResource
composable properly converts it to the correct string.
s

Se7eN

02/28/2021, 4:45 PM
I don't know if this should work or not but normally I'd do stuff like this using `remember`:
Copy code
val output = remember(message) { message?.let { stringResource(id = it)} ?: "" }
b

Bradleycorn

02/28/2021, 4:46 PM
Can't do that ...
stringResource
is a composable function, and you can't call composables from the block of a
remember
s

Se7eN

02/28/2021, 4:49 PM
Oh yeah
b

Bradleycorn

02/28/2021, 4:52 PM
FWIW, the main
BottomSheetScreen
composable is getting called like this, from the main Acitivity:
Copy code
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            MaterialTheme {
                    Surface(color = MaterialTheme.colors.background) {

                        var message by remember { mutableStateOf<Int?>(null)}

                        rememberCoroutineScope().launch {
                            delay(3_000)
                            message = R.string.message
                        }
                        BottomSheetScreen(message = message)
                    }

                }
            }
        }
}
j

jossiwolf

02/28/2021, 6:11 PM
Hmmm, what Compose version are you on? I just tried to repro this and it works for me.
b

Bradleycorn

02/28/2021, 6:11 PM
beta1 Well that's the thing ... I copied the above to a brand new project and it works there. But in the project that I wrote it in ... it doesn't ...
And here's where people reply with, "well then you're doing something wrong in the project where it doesn't work" ... To which i would reply, it's also a new project, the only difference being it was created as an alpha12 project, and converted to beta1.
j

jossiwolf

02/28/2021, 6:20 PM
Hah, fun... When you're saying the bottom sheet doesn't show at all, can you check if the
bottomSheetState.show
call throws a
CancellationException
or completes successfully?
b

Bradleycorn

02/28/2021, 6:21 PM
oooh
didn't think of that
j

jossiwolf

02/28/2021, 6:21 PM
I've seen some issues with that, but I don't think that's it
b

Bradleycorn

02/28/2021, 6:21 PM
yeah, I wouldn't think so, but you never know ... worth a shot
👍 1
j

jossiwolf

02/28/2021, 6:22 PM
Random thought: Can you try to create a completely new activity, totally barebones, no special flags and host your code in there?
b

Bradleycorn

02/28/2021, 6:25 PM
omg
Failure(kotlinx.coroutines.JobCancellationException: ScopeCoroutine was cancelled; job=ScopeCoroutine{Cancelled}@cd2659c)
errr
actually, that might be expected
ah .. no. not expected
So that is the problem
But ... why?
j

jossiwolf

02/28/2021, 6:30 PM
Nice
Debugging that one is... fun
b

Bradleycorn

02/28/2021, 6:31 PM
yeah, good times
j

jossiwolf

02/28/2021, 6:31 PM
So it's cancelled whenever the animation is interrupted
Which in most cases happens when another animation is requested
(At least from my experience)
Things you might want to hook breakpoints into:
SwipeableState#processNewAnchors
- see if that's called
SwipeableState#snapTo
SwipeableState#animateTo
afaik these are what can cause a new animation to run
Another thing to try: Try delaying the
show
call by 200ms
b

Bradleycorn

02/28/2021, 6:42 PM
yeah, that "fixes" it
j

jossiwolf

02/28/2021, 6:42 PM
Nice
b

Bradleycorn

02/28/2021, 6:43 PM
hehe I guess
I still want to understand WHY
j

jossiwolf

02/28/2021, 6:43 PM
ModalBottomSheetLayout
measures the sheet content when laying out everything. The composition of the body content is dependent on the sheet's height. I think what's happening is that the sheet's height changes when the text isn't
null
anymore, causing the body (and the swipeable anchors) to recompose
b

Bradleycorn

02/28/2021, 6:44 PM
yeah, that makes sense.
Still doesn't explain why it works in the one project and not in the other
j

jossiwolf

02/28/2021, 6:45 PM
When the anchors are set, it tries to settle into the new correct state (which is still the old one)
That's the fun of race conditions 😛
Does your
MainActivity
have any flags?
b

Bradleycorn

02/28/2021, 6:47 PM
Yeah, that's what it boils down to ... There's a race condition down in there somewhere No. Really about the ONLY difference between the 2 activities is that in the one where it is broken, I have added Hilt to the project, so the activity is marked as an
@AndroidEntryPoint
(though I'm not actually having anything injected into it yet).
j

jossiwolf

02/28/2021, 6:49 PM
When you mark the other one as
@AndroidEntryPoint
, can you repro it there?
b

Bradleycorn

02/28/2021, 6:50 PM
haven't tried yet, but I will
👍 1
Gotta jump through all the Hilt setup hoops
But I mean ... the implementation I have ... for a case where your bottom sheet content isn't static ... It sure seems like a decent implementation .... right?
j

jossiwolf

02/28/2021, 6:52 PM
In any case, file an issue! cc @matvei, look! Somebody encountered the bug I created with the first version of my CL
🎉 1
Yup
b

Bradleycorn

02/28/2021, 6:56 PM
yeah, I'll file an issue. Trying to get a consistent repoducable version. I hate to file without cause as we've seen, if you just create a new project and put the code it, it's fine.
👍 1
b

BenjO

02/28/2021, 7:06 PM
As an alternative, you can copy your project and strip everything but this composable. If it still doesn't work you have your sample to fill the bug tracker ^^
b

Bradleycorn

02/28/2021, 7:08 PM
yeah. At the moment, I'm going the opposite route ... copying the broken project to the new, piece by piece, seeing which piece breaks it. Adding Hilt did not break it.
💪 2
j

jossiwolf

02/28/2021, 7:13 PM
Some more questions: • You were saying hardcoding the text works. When you hardcode it in your activity (just set a String instead of a resource Int) and pass that down instead, I'm guessing the issue doesn't happen? • Do you have a lot of strings?
b

Bradleycorn

02/28/2021, 7:40 PM
I've tried so many things, that I don't remember exactly ... I have tried using pure strings instead of resources and still ran into the problem ... but, at that point I had some additional complexities where there were some Flows and States involved. I may not have done a basic test with pure strings. As for my string resources ... no, there's only like 3 strings in the xml file
Tested with pure strings instead of using string resources ... issue persists
s

Shakil Karim

02/28/2021, 7:52 PM
@Bradleycorn I had a same issue i think that's because your calling
Copy code
if (message != null) {
        scope.launch { bottomSheetState.show() }
    }
before setting the bottomSheetState to ModalBottomSheetLayout that's why applying delay works.
b

Bradleycorn

02/28/2021, 7:54 PM
@Shakil Karim -- I tried moving the coroutine down AFTER calling the ModalBottomSheetLayout composable, and that had no effect. Also, that wouldn't explain why the code DOES work in a fresh project
s

Shakil Karim

02/28/2021, 7:56 PM
@Bradleycorn Ah ok, then it's a separate issue.
j

jossiwolf

02/28/2021, 8:00 PM
Hm, yeah, the question was a bit of a shot in the dark. I was thinking if the retrieval of the
stringResource
could be holding things up
b

Bradleycorn

02/28/2021, 8:00 PM
yeah
Holy cow I found it!
@jossiwolf you were right all along
ModalBottomSheetLayout
 measures the sheet content when laying out everything. The composition of the body content is dependent on the sheet's height. I think what's happening is that the sheet's height changes when the text isn't 
null
 anymore, causing the body (and the swipeable anchors) to recompose
it works in my demo app because the string to be displayed was short and fit on a single line (i.e. the same height as when it's an empty string). In my other app, the string to be displayed was a longer error message that wraps to a 2nd line ... thus the height of the bottom sheet changes ... and ... 💥.
mind blown 1
j

jossiwolf

02/28/2021, 9:42 PM
Yayyy
Lol I'm happy I was right and not at the same time 😄
Let's see what the Compose folks say when they see the ticket
j

justasm

03/01/2021, 7:51 AM
As mentioned — https://kotlinlang.slack.com/archives/CJLTWPH7S/p1614537608220200?thread_ts=1614530232.195400&amp;cid=CJLTWPH7S Delay the show, e.g.
Copy code
val scope = rememberCoroutineScope()
val sheetState = rememberModalBottomSheetState()

// in onClick or other callback..
scope.launch {
  delay(1)
  sheetState.show()
}

// alternatively, in composition like in the example in this thread
LaunchedEffect(message) {
  delay(1)
  sheetState.show()
}
m

matvei

03/01/2021, 11:18 AM
Yeah, nasty issue. We need to fix it as some point soon. This is the issue to track: http://issuetracker.google.com/180977981 The crash is not the same, but the idea us the same. The crash is different because we trigger
show
at the different point of time, the one in the bug is more correct. the rule of thumb with coroutine launches is that you should never launch directly from the composition (like in your example above). We have special tools e.g
LaunchedEffect
to launch smth as a side effect, or
DisposableEffect
and
SideEffect
to just call a lambda as a side effects, where you can later do
scope.launch
. Please use those instead of calling launch from the composition.
a

Alex Bieliaiev

03/03/2021, 10:45 AM
I have the very same problem, but with a slightly different setup -
NavHost
inside a
ModalBottomSheet
. My solution is a bit more verbose, since I don't really like
delay
s:
Copy code
/**
 * That is horrible, but I don't have a better option ATM.
 * Please refer to [androidx.compose.material.SwipeableState.processNewAnchors],
 * specifically that part:
 * val targetOffset = newAnchors.getOffset(currentValue)
 * ?: newAnchors.keys.minByOrNull { abs(it - offset.value) }!!
 * try {
 *     animateInternalToOffset(targetOffset, animationSpec)
 * } catch (c: CancellationException) {
 *     // If the animation was interrupted for any reason, snap as a last resort.
 *     snapInternalToOffset(targetOffset)
 * }
 * The problem is that [androidx.compose.material.SwipeableState.processNewAnchors]
 * cancels expanding animation, then I have to re-launch it first after the initial
 * animateInternalToOffset(targetOffset, animationSpec) and then once again after
 * the snapInternalToOffset(targetOffset).
 */
LaunchedEffect(Unit) {
    try {
        modalBottomSheetState.animateTo(ModalBottomSheetValue.Expanded)
    } finally {
        try {
            modalBottomSheetState.animateTo(ModalBottomSheetValue.Expanded)
        } finally {
            modalBottomSheetState.animateTo(ModalBottomSheetValue.Expanded)
        }
    }
}
😨 1
m

matvei

03/03/2021, 11:14 AM
uuuf, that is quite bad 🙂 Please file a bug, we shouldn't be that "safe" in the swipeable implementation about that
👍 1
b

Bradleycorn

03/03/2021, 2:16 PM
I've already created an issue, here: https://issuetracker.google.com/issues/181593642
@Alex Bieliaiev (and others too) you might want to that issue
👍 1
31 Views