You can find a sample project to reproduce the issue on Github
I've been trying to use Jetpack Compose for a Keyboard UI. Ultimately, When I try to inflate the Keyboard via the InputMethodService
class IMEService : InputMethodService() {
override fun onCreateInputView(): View = KeyboardView(this)
}
By using this view
class KeyboardView(context: Context) : FrameLayout(context) {
init {
val view = ComposeView(context).apply {
setContent {
Keyboard() //<- This is the actual compose UI function
}
}
addView(view)
}
}
or
class KeyboardView2 constructor(
context: Context,
) : AbstractComposeView(context) {
@Composable
override fun Content() {
Keyboard()
}
}
However, when I try to use the keyboard I get the following error
java.lang.IllegalStateException: Composed into the View which doesn't propagate ViewTreeLifecycleOwner!
at androidx.compose.ui.platform.AndroidComposeView.onAttachedToWindow(AndroidComposeView.kt:599)
at android.view.View.dispatchAttachedToWindow(View.java:19676)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3458)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3465)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3465)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3465)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3465)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3465)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3465)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3465)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2126)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1817)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7779)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1031)
at android.view.Choreographer.doCallbacks(Choreographer.java:854)
at android.view.Choreographer.doFrame(Choreographer.java:789)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1016)
at android.os.Handler.handleCallback(Handler.java:914)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:227)
at android.app.ActivityThread.main(ActivityThread.java:7582)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:539)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:953)
The official documentation states
You must attach the ComposeView to a ViewTreeLifecycleOwner. The ViewTreeLifecycleOwner allows the view to be attached and detached repeatedly while preserving the composition. ComponentActivity, FragmentActivity and AppCompatActivity are all examples of classes that implement ViewTreeLifecycleOwner
However, I cannot use ComponentActivity
, FragmentActivity
, or AppCompatActivity
to inflate the View which calls the compose code. I became stuck with implementing ViewTreeLifecycleOwner. I don't know how to do it.
How can I use @Composable
functions as an Input Method View?
Edit:
As CommonsWare suggested I used the ViewTreeLifecycleOwner.set(...)
method and I also had to implement ViewModelStoreOwner
and SavedStateRegistryOwner
as well:
class IMEService : InputMethodService(), LifecycleOwner, ViewModelStoreOwner,
SavedStateRegistryOwner {
override fun onCreateInputView(): View {
val view = KeyboardView2(this)
ViewTreeLifecycleOwner.set(view, this)
ViewTreeViewModelStoreOwner.set(view, this)
ViewTreeSavedStateRegistryOwner.set(view, this)
return view
}
//Lifecycle Methods
private var lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
override fun getLifecycle(): Lifecycle {
return lifecycleRegistry
}
private fun handleLifecycleEvent(event: Lifecycle.Event) =
lifecycleRegistry.handleLifecycleEvent(event)
override fun onCreate() {
super.onCreate()
handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
override fun onDestroy() {
super.onDestroy()
handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
//ViewModelStore Methods
private val store = ViewModelStore()
override fun getViewModelStore(): ViewModelStore = store
//SaveStateRegestry Methods
private val savedStateRegistry = SavedStateRegistryController.create(this)
override fun getSavedStateRegistry(): SavedStateRegistry = savedStateRegistry.savedStateRegistry
}
Now I get a new error
java.lang.IllegalStateException: You can consumeRestoredStateForKey only after super.onCreate of corresponding component
at androidx.savedstate.SavedStateRegistry.consumeRestoredStateForKey(SavedStateRegistry.java:77)
at androidx.compose.ui.platform.DisposableUiSavedStateRegistryKt.DisposableUiSavedStateRegistry(DisposableUiSavedStateRegistry.kt:69)
at androidx.compose.ui.platform.DisposableUiSavedStateRegistryKt.DisposableUiSavedStateRegistry(DisposableUiSavedStateRegistry.kt:44)
at androidx.compose.ui.platform.AndroidAmbientsKt.ProvideAndroidAmbients(AndroidAmbients.kt:162)
at androidx.compose.ui.platform.WrappedComposition$setContent$1$1$3.invoke(Wrapper.kt:261)
[...]
This is somehow related to the lifecycle event propagation because when I comment out the onCreate
and onDestroy
methods the keyboard works opens without crashes, but the keyboard is not visible
My answer is largely based on Yannick's answer and other linked sources, so credit goes to them.
Essentially, Compose needs three "Owner" classes from the androidx.lifecycle
package to work: LifecycleOwner
, ViewModelStoreOwner
, and SavedStateRegistryOwner
. AppCompatActivity
and Fragment
already implement those interfaces, so setting a ComposeView
to them works out-of-the-box.
However, when building an IME app, you don't have access to an Activity or Fragment.
Therefore, you must implement your own "Owner" classes, tied to the lifecycle callbacks you get from the InputMethodService
. Here's how to do that:
class KeyboardViewLifecycleOwner :
LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner {
fun onCreate() {
savedStateRegistryController.performRestore(null)
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
fun onResume() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
}
fun onPause() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
}
fun onDestroy() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
store.clear()
}
/**
Compose uses the Window's decor view to locate the
Lifecycle/ViewModel/SavedStateRegistry owners.
Therefore, we need to set this class as the "owner" for the decor view.
*/
fun attachToDecorView(decorView: View?) {
if (decorView == null) return
ViewTreeLifecycleOwner.set(decorView, this)
ViewTreeViewModelStoreOwner.set(decorView, this)
ViewTreeSavedStateRegistryOwner.set(decorView, this)
}
// LifecycleOwner methods
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
override fun getLifecycle(): Lifecycle = lifecycleRegistry
// ViewModelStore methods
private val store = ViewModelStore()
override fun getViewModelStore(): ViewModelStore = store
// SavedStateRegistry methods
private val savedStateRegistryController = SavedStateRegistryController.create(this)
override fun getSavedStateRegistry(): SavedStateRegistry =
savedStateRegistryController.savedStateRegistry
}
InputMethodService
, override callbacks and relay those messages to an instance of the class we defined on step 1:class MyKeyboardService : InputMethodService() {
private val keyboardViewLifecycleOwner = KeyboardViewLifecycleOwner()
override fun onCreate() {
super.onCreate()
keyboardViewLifecycleOwner.onCreate()
}
override fun onCreateInputView(): View {
//Compose uses the decor view to locate the "owner" instances
keyboardViewLifecycleOwner.attachToDecorView(
window?.window?.decorView
)
return MyComposeView(this)
}
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
keyboardViewLifecycleOwner.onResume()
}
override fun onFinishInputView(finishingInput: Boolean) {
keyboardViewLifecycleOwner.onPause()
}
override fun onDestroy() {
super.onDestroy()
keyboardViewLifecycleOwner.onDestroy()
}
}
By doing this, now Compose has a proper lifecycle to listen to, which it uses to determine when to perform recomposition, etc.
Sources:
After looking for similar implementations in ComponentActivity
I finally came up with a working solution:
class IMEService : InputMethodService(), LifecycleOwner, ViewModelStoreOwner,
SavedStateRegistryOwner {
override fun onCreateInputView(): View {
val view = ComposeKeyboardView(this)
ViewTreeLifecycleOwner.set(view, this)
ViewTreeViewModelStoreOwner.set(view, this)
ViewTreeSavedStateRegistryOwner.set(view, this)
return view
}
//Lifecylce Methods
private var lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
override fun getLifecycle(): Lifecycle {
return lifecycleRegistry
}
private fun handleLifecycleEvent(event: Lifecycle.Event) =
lifecycleRegistry.handleLifecycleEvent(event)
override fun onCreate() {
super.onCreate()
savedStateRegistry.performRestore(null)
handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
override fun onDestroy() {
super.onDestroy()
handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
//ViewModelStore Methods
private val store = ViewModelStore()
override fun getViewModelStore(): ViewModelStore = store
//SaveStateRegestry Methods
private val savedStateRegistry = SavedStateRegistryController.create(this)
override fun getSavedStateRegistry(): SavedStateRegistry = savedStateRegistry.savedStateRegistry
}
I don't know if it's the best implementation in terms of performance but it works fine even on older devices. Improvement ideas are appreciated
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With