Hello! How can I trigger some code in a remembered...
# compose
g
Hello! How can I trigger some code in a remembered stable-annotated class, besides using
init
? I would like to build a state object that triggers a callback only when the keyboard is closed (opening a bottom sheet in this case) I've used this useful piece of code to know when the keyboard is closed by mutable state and came up with the following (in thread)
Copy code
@Composable
fun rememberOnKeyboardClosedState(
    whenClosed: () -> Unit,
): OnKeyboardClosedState {

    val keyboardController = LocalSoftwareKeyboardController.current
    val keyboardStatus by keyboardStatusAsState()
    val runOpen = remember { mutableStateOf(false) }

    return remember(keyboardStatus, whenClosed, runOpen) {
        OnKeyboardClosedState(
            keyboardController = keyboardController,
            keyboardStatus = keyboardStatus,
            runOpen = runOpen,
            whenClosed = whenClosed
        )
    }
}

/**
 * Runs the callback of whenClosed only when the keyboard is actually closed
 */
@OptIn(ExperimentalComposeUiApi::class)
@Stable
data class OnKeyboardClosedState internal constructor(
    private val keyboardController: SoftwareKeyboardController?,
    private val keyboardStatus: KeyboardStatus,
    private val runOpen: MutableState<Boolean>,
    private val whenClosed: () -> Unit,
) {
    private val rememberScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)

    init {
        if (runOpen.value) {
            rememberScope.launch {
                delay(200) // not necessary but makes it fancier
                open()
            }
            runOpen.value = false
        }
    }

    fun open() {
        if (keyboardStatus == KeyboardStatus.Closed) {
            rememberScope.launch {
                whenClosed()
            }
        } else {
            runOpen.value = true
            keyboardController?.hide()
        }
    }
}
Usage
Copy code
val onKeyboardClosedState = rememberOnKeyboardClosedState {
        handleEvent(ChatUiEvent.OnMoreOptionsClicked)
    }

    // in some onClick function
    onKeyboardClosedState.open()
Is there a better way of achieving this behavior?
s
Does just something like this work?
Copy code
val keyboardStatus by keyboardStatusAsState()
LaunchedEffect(Unit) {
  snapshotFlow { keyboardStatus }
    .drop(1)
    .filter { it == true }
    .collect {
      Do something
    }
}
Drop(1) so that you ignore the first emission that you get on initialization, where it's gonna be false anyway as you open the screen
g
Thank you @Stylianos Gakis 🙂 So with your suggestion I ended up with this which works as well as the previous version with
init
:
Copy code
@Composable
fun rememberOnKeyboardClosedState(
    whenClosed: () -> Unit,
): OnKeyboardClosedState {

    val keyboardController = LocalSoftwareKeyboardController.current
    val keyboardStatus by keyboardStatusAsState()
    val shouldRunOpen = remember { mutableStateOf(false) }

    LaunchedEffect(Unit) {
        snapshotFlow { keyboardStatus }
            .drop(1)
            .filter { it == KeyboardStatus.Closed && shouldRunOpen.value }
            .collect {
                delay(200) // not necessary per say but makes the UI change a little calmer
                whenClosed()
                shouldRunOpen.value = false
            }
    }

    return remember(keyboardStatus) {
        OnKeyboardClosedState(
            keyboardController = keyboardController,
            keyboardStatus = keyboardStatus,
            shouldRunOpen = shouldRunOpen,
            whenClosed = whenClosed
        )
    }
}

/**
 * Runs the callback of whenClosed only when the keyboard is actually closed
 */
@OptIn(ExperimentalComposeUiApi::class)
@Stable
data class OnKeyboardClosedState internal constructor(
    private val keyboardController: SoftwareKeyboardController?,
    private val keyboardStatus: KeyboardStatus,
    private val shouldRunOpen: MutableState<Boolean>,
    private val whenClosed: () -> Unit,
) {
    /**
     * Triggers the [whenClosed] callback the keyboard is closed or trigger closing it and
     * setting a mutable state to run [whenClosed] when the keyboard will be closed
     */
    fun open() {
        if (keyboardStatus == KeyboardStatus.Closed) {
            whenClosed()
        } else {
            // To indeed call whenClosed after the keyboard is closed
            shouldRunOpen.value = true
            // triggers changes of keyboardStatusState
            keyboardController?.hide()
        }
    }
}
I still need to pass a mutable state to the remembered object, to enable the callback when I really intend to open a bottom sheet, but overall it looks indeed better! I'm open for more improvements if you see any 😇
s
Hmmm so you want the callback to trigger if you call
open()
on it while it's still closed? And it doesn't even trigger the callback in that situation
I must be misunderstanding something, because I don't see a reason for the OnKeyboardStateClosed class to exist in the first place, nor for the shouldRunOpen state to exist. Why do you need to turn it off and on again later? It feels like you're doing more than just getting a callback about when the keyboard hides, right?
g
The issue I'm solving to begin with is that when the keyboard is open, a bottom sheet shows itself behind it, so I wanted to close the keyboard, wait for it to be closed, and only then open the bottom sheet That class exists so I can give something to the screen to hold onto so it can trigger that whole operation
s
Ahaa so
open()
means open sheet, not open keyboard really
g
yes! I need to rename that function