Update
With the new WindowInsets API, it gets easier
First, to return the correct values, you need to set:
WindowCompat.setDecorFitsSystemWindows(window, false)
Then to use Keyboard as a state:
@Composable
fun keyboardAsState(): State<Boolean> {
    val isImeVisible = WindowInsets.ime.getBottom(LocalDensity.current) > 0
    return rememberUpdatedState(isImeVisible)
}
use example:
val isKeyboardOpen by keyboardAsState() // true or false
ps: I've tried to use WindowInsets.isImeVisible, but it returns true in the first call.
Without an experimental API
if you want with the statement, I found this solution:
enum class Keyboard {
    Opened, Closed
}
@Composable
fun keyboardAsState(): State<Keyboard> {
    val keyboardState = remember { mutableStateOf(Keyboard.Closed) }
    val view = LocalView.current
    DisposableEffect(view) {
        val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
            val rect = Rect()
            view.getWindowVisibleDisplayFrame(rect)
            val screenHeight = view.rootView.height
            val keypadHeight = screenHeight - rect.bottom
            keyboardState.value = if (keypadHeight > screenHeight * 0.15) {
                Keyboard.Opened
            } else {
                Keyboard.Closed
            }
        }
        view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener)
        onDispose {
            view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener)
        }
    }
    return keyboardState
}
and to detect/check the value you'll only need this:
val isKeyboardOpen by keyboardAsState() // Keyboard.Opened or Keyboard.Closed