InputMethodService with Jetpack Compose - ComposeView causes: Composed into the View which doesn't propagate ViewTreeLifecycleOwner
Asked Answered
H

3

14

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

Helvellyn answered 16/1, 2021 at 23:25 Comment(1)
The docs would appear to be wrong, insofar as you cannot "implement" ViewTreeLifecycleOwner. My guess is that they mean you need to call ViewTreeLifecycleOwner.set(), passing in the View and a LifecycleOwner. My guess is that you will need to copy the code from LifecycleService into yours, so your service itself is a LifecycleOwner that you can use.Conjugated
H
12

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

Helvellyn answered 17/1, 2021 at 11:34 Comment(3)
This is broken due to ViewTreeSavedState... being broken as of 2023Bitchy
@Bitchy no, it's not, it's just replaced with View.setViewTreeSavedStateRegistryOwner()Watchmaker
but another issue: Cannot access 'performRestore': it is internal in 'SavedStateRegistry' - v1.2.1Watchmaker
A
15

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:

  1. Create a separate class responsible for handling lifecycle ownership tasks:
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
}
  1. Now from the class extending 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:

Adaptive answered 5/4, 2021 at 19:34 Comment(1)
Cannot access 'performRestore': it is internal in 'SavedStateRegistry' - v1.2.1Watchmaker
H
12

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

Helvellyn answered 17/1, 2021 at 11:34 Comment(3)
This is broken due to ViewTreeSavedState... being broken as of 2023Bitchy
@Bitchy no, it's not, it's just replaced with View.setViewTreeSavedStateRegistryOwner()Watchmaker
but another issue: Cannot access 'performRestore': it is internal in 'SavedStateRegistry' - v1.2.1Watchmaker
E
3

Base on @Yannick's answer, I add dependency on "androidx.savedstate:1.2.0" and also modify some code.

@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")

package io.simsim.demo.fetal

import android.app.Service
import android.content.Intent
import android.os.IBinder
import androidx.annotation.CallSuper
import androidx.lifecycle.*
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner

abstract class BaseService :
    Service(),
    LifecycleOwner,
    ViewModelStoreOwner,
    SavedStateRegistryOwner {
    private val _lifecycleRegistry: LifecycleRegistry by lazy { LifecycleRegistry(this) }
    private val _store by lazy { ViewModelStore() }


    override fun getLifecycle(): Lifecycle = _lifecycleRegistry

    override fun getViewModelStore(): ViewModelStore = _store

    override val savedStateRegistry: SavedStateRegistry = SavedStateRegistryController.create(this).savedStateRegistry

    private fun handleLifecycleEvent(event: Lifecycle.Event) =
        _lifecycleRegistry.handleLifecycleEvent(event)

    @CallSuper
    override fun onCreate() {
        super.onCreate()
        // You must call performAttach() before calling performRestore(Bundle)
        savedStateRegistry.performAttach(lifecycle)
        savedStateRegistry.performRestore(null)
        handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
    }

    @CallSuper
    override fun onTaskRemoved(rootIntent: Intent?) {
        super.onTaskRemoved(rootIntent)
        stopSelf()
    }

    override fun onBind(intent: Intent?): IBinder? {
        return null
    }

    @CallSuper
    override fun onDestroy() {
        super.onDestroy()
        handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    }
}

// use ComposeView like this

ComposeView(this).also {
    it.setContent(content)
    ViewTreeLifecycleOwner.set(it, this)
    ViewTreeViewModelStoreOwner.set(it, this)
    it.setViewTreeSavedStateRegistryOwner(this)
}
Eradicate answered 1/8, 2022 at 10:3 Comment(3)
Where is that ComposeView section done?Bitchy
Good answer, though with new versions the apis changed to: setViewTreeLifecycleOwner(lifecycleOwner) , setViewTreeViewModelStoreOwner(viewModelStoreOwner) , setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner)Billfish
Cannot access 'performRestore': it is internal in 'SavedStateRegistry' - v1.2.1Watchmaker

© 2022 - 2024 — McMap. All rights reserved.