InputMethodService with Jetpack Compose - ComposeView 原因:组合到不传播 ViewTreeLifecycleOwner 的视图中

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

您可以在 Github

上找到一个示例项目来重现该问题

我一直在尝试将 Jetpack Compose 用于键盘 UI。最终,当我尝试通过 InputMethodService

给键盘充气时
class IMEService : InputMethodService() {

    override fun onCreateInputView(): View = KeyboardView(this)
}

通过使用此视图

class KeyboardView(context: Context) : FrameLayout(context)  {

    init {
        val view = ComposeView(context).apply {
            setContent {
                Keyboard() //<- This is the actual compose UI function
            }
        }
        addView(view)
    }

}

class KeyboardView2 constructor(
    context: Context,

    ) : AbstractComposeView(context) {

  
    @Composable
    override fun Content() {
        Keyboard()
    }
}

但是,当我尝试使用键盘时出现以下错误

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)

official documentation 状态

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

但是,我无法使用 ComponentActivityFragmentActivityAppCompatActivity 来扩充调用撰写代码的视图。我陷入了实现 ViewTreeLifecycleOwner 的困境。我不知道该怎么做。

如何使用 @Composable 函数作为输入法视图?

编辑: 正如 CommonsWare 建议的那样,我使用了 ViewTreeLifecycleOwner.set(...) 方法,而且我还必须实现 ViewModelStoreOwnerSavedStateRegistryOwner

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
}

现在我收到一个新错误

  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.invoke(Wrapper.kt:261)
[...]

这在某种程度上与生命周期事件传播有关,因为当我注释掉 onCreateonDestroy 方法时,键盘可以正常打开而不会崩溃,但键盘不可见

ComponentActivity 中寻找类似的实现之后 我终于想出了一个 可行的 解决方案:

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
}

我不知道它是否是性能方面的最佳实现,但即使在旧设备上也能正常工作。改进意见表示赞赏

我的回答主要基于 Yannick 的回答和其他链接来源,因此归功于他们。

本质上,Compose 需要 androidx.lifecycle 包中的三个“所有者”classes 才能工作:LifecycleOwnerViewModelStoreOwnerSavedStateRegistryOwnerAppCompatActivityFragment 已经实现了这些接口,因此为它们设置 ComposeView 可以开箱即用。

但是,在构建 IME 应用程序时,您无权访问 Activity 或片段。

因此,您必须实现自己的“所有者”classes,与您从 InputMethodService 获得的生命周期回调相关联。方法如下:

  1. 创建一个单独的 class 负责处理生命周期所有权任务:
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. 现在从 class 扩展 InputMethodService,覆盖回调并将这些消息中继到我们在步骤 1 中定义的 class 的实例:
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()
    }
}

通过这样做,现在 Compose 有了一个合适的生命周期来监听,它用它来确定何时执行重组等。

来源: