Building Android Custom Keyboards with Jetpack Compose
Legacy XML KeyboardView has been deprecated for years, yet developers attempting to use modern Jetpack Compose inside Android’s native InputMethodService encounter mysterious state freezing bugs. Here is how we solved the architecture and open-sourced the solution.
Key Architectural Takeaways
1. The Root Cause of Compose IME Freezes
When developers attempt to return a ComposeView inside onCreateInputView(), initial rendering works fine. However, as soon as you press a key that modifies state (such as toggling Shift or switching to numbers), Compose fails to recompose.
InputMethodService is an old Android framework Service, not an Activity. It does not implement LifecycleOwner, ViewModelStoreOwner, or SavedStateRegistryOwner. Because the view tree lifecycle never reaches Lifecycle.State.RESUMED, Compose’s snapshot observer remains idle.2. The LifecycleInputMethodService Solution
Our library implements a specialized lifecycle owner that manually synchronizes Android service window transitions with AndroidX lifecycle states:
abstract class LifecycleInputMethodService : InputMethodService(),
LifecycleOwner,
ViewModelStoreOwner,
SavedStateRegistryOwner {
private val lifecycleRegistry = LifecycleRegistry(this)
private val store = ViewModelStore()
private val savedStateRegistryController = SavedStateRegistryController.create(this)
override fun onCreate() {
super.onCreate()
savedStateRegistryController.performRestore(null)
// Advance to RESUMED so Compose can observe state changes and recompose!
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
}
}3. Building Your Custom Keyboard
With compose-ime, building a full custom keyboard is as simple as creating standard Compose components:
class MyKeyboardService : ComposeInputMethodService() {
@Composable
override fun KeyboardContent() {
var modifierState by remember { mutableStateOf(ModifierState.Default) }
ComposeImeTheme {
Column(modifier = Modifier.fillMaxWidth().background(Color(0xFF0B0F19))) {
// Terminal Developer Control Row (ESC, TAB, CTRL, ALT, Arrows)
TerminalControlRow(
modifierState = modifierState,
onToggleCtrl = { modifierState = modifierState.toggleCtrl() },
onToggleAlt = { modifierState = modifierState.toggleAlt() },
onSendKeyEvent = { keyCode -> sendDownUpKeyEvents(keyCode) },
onCommitText = { text -> currentInputConnection?.commitText(text, 1) }
)
// QWERTY Key Rows
KeyboardKey(
label = "A",
onClick = { currentInputConnection?.commitText("A", 1) }
)
}
}
}
}Open Source on GitHub
This project is maintained by Epheos LTD under the Apache 2.0 license. Check out the sample app and get started building custom Android keyboards today!
View compose-ime on GitHub