Skip to content

Instantly share code, notes, and snippets.

@bmc08gt
Last active May 13, 2025 13:34
Show Gist options
  • Save bmc08gt/9ed7c1ccae28294d2c7b6ec5125cf790 to your computer and use it in GitHub Desktop.
Save bmc08gt/9ed7c1ccae28294d2c7b6ec5125cf790 to your computer and use it in GitHub Desktop.
KMP Software Keyboard Controller
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.SoftwareKeyboardController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
public abstract class KeyboardController(
private val coroutineScope: CoroutineScope,
private val softwareController: SoftwareKeyboardController?
) {
public open var isVisible: Boolean by mutableStateOf(false)
protected set
public fun show() {
softwareController?.show()
}
public fun hide() {
softwareController?.hide()
}
public fun hideIfVisible(block: () -> Unit = { }) {
coroutineScope.launch {
if (isVisible) {
hide()
delay(300)
}
block()
}
}
}
@Composable
public expect fun rememberKeyboardController(): KeyboardController
import android.annotation.SuppressLint
import android.view.View
import android.view.ViewTreeObserver
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import kotlinx.coroutines.CoroutineScope
private class AndroidKeyboardController(
private val view: View,
coroutineScope: CoroutineScope,
softwareKeyboardController: SoftwareKeyboardController?
): KeyboardController(coroutineScope, softwareKeyboardController) {
override var isVisible by mutableStateOf(false)
// Internal setup for visibility tracking
@SuppressLint("ComposableNaming")
@Composable
fun setupVisibilityTracking() {
val viewTreeObserver = view.viewTreeObserver
DisposableEffect(viewTreeObserver) {
val listener = ViewTreeObserver.OnGlobalLayoutListener {
isVisible = ViewCompat.getRootWindowInsets(view)
?.isVisible(WindowInsetsCompat.Type.ime()) ?: false
}
viewTreeObserver.addOnGlobalLayoutListener(listener)
onDispose { viewTreeObserver.removeOnGlobalLayoutListener(listener) }
}
}
}
@Composable
public actual fun rememberKeyboardController(): KeyboardController {
val view = LocalView.current
val softwareController = LocalSoftwareKeyboardController.current
val composeScope = rememberCoroutineScope()
val keyboardController = remember(view, softwareController) {
AndroidKeyboardController(view, composeScope, softwareController)
}
// Trigger visibility tracking
keyboardController.setupVisibilityTracking()
return keyboardController
}
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.interop.LocalUIViewController
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController
import kotlinx.coroutines.CoroutineScope
import platform.Foundation.NSNotificationCenter
import platform.UIKit.UIKeyboardWillHideNotification
import platform.UIKit.UIKeyboardWillShowNotification
private class KeyboardObserver(
private val onStateChanged: (Boolean) -> Unit,
) {
fun watch() {
NSNotificationCenter.defaultCenter.addObserverForName(
name = UIKeyboardWillHideNotification,
`object` = null,
queue = null,
) {
onStateChanged(false)
}
NSNotificationCenter.defaultCenter.addObserverForName(
name = UIKeyboardWillShowNotification,
`object` = null,
queue = null,
) {
onStateChanged(true)
}
}
fun stop() {
NSNotificationCenter.defaultCenter.removeObserver(this)
}
}
private class IOSKeyboardController(
coroutineScope: CoroutineScope,
softwareKeyboardController: SoftwareKeyboardController?
) : KeyboardController(coroutineScope, softwareKeyboardController) {
override var isVisible by mutableStateOf(false)
// Internal setup for visibility tracking
@Composable
fun setupVisibilityTracking() {
val keyboardObserver = remember { KeyboardObserver { isVisible = it } }
DisposableEffect(LocalUIViewController.current) {
keyboardObserver.watch()
onDispose {
keyboardObserver.stop()
}
}
}
}
@Composable
public actual fun rememberKeyboardController(): KeyboardController {
val softwareController = LocalSoftwareKeyboardController.current
val composeScope = rememberCoroutineScope()
val keyboardController = remember(softwareController) {
IOSKeyboardController(composeScope, softwareController)
}
// Trigger visibility tracking
keyboardController.setupVisibilityTracking()
return keyboardController
}
@Composable
fun SomeScreen(goBack: () -> Unit)) {
val keyboard = rememberKeyboardController()
Button(onClick = { keyboard.hideIfVisible(goBack) }) {
Text(text = "Exit")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment