InputMethodService 与 Jetpack Compose - ComposeView 导致:组合到不传播 ViewTreeLifecycleOwner 的视图中

Posted

技术标签:

【中文标题】InputMethodService 与 Jetpack Compose - ComposeView 导致:组合到不传播 ViewTreeLifecycleOwner 的视图中【英文标题】:InputMethodService with Jetpack Compose - ComposeView causes: Composed into the View which doesn't propagate ViewTreeLifecycleOwner 【发布时间】:2021-04-21 15:26:39 【问题描述】:

您可以在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 状态

您必须将 ComposeView 附加到 ViewTreeLifecycleOwner。 ViewTreeLifecycleOwner 允许在保留组合的同时重复附加和分离视图。 ComponentActivity、FragmentActivity 和 AppCompatActivity 都是实现 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$1$1$3.invoke(Wrapper.kt:261)
[...]

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

【问题讨论】:

文档似乎是错误的,因为您不能“实施”ViewTreeLifecycleOwner。我猜他们的意思是你需要调用ViewTreeLifecycleOwner.set(),传入ViewLifecycleOwner。我的猜测是您需要将代码从 LifecycleService 复制到您的代码中,因此您的服务本身就是您可以使用的 LifecycleOwner 【参考方案1】:

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

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

【讨论】:

【参考方案2】:

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

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

但是,在构建 IME 应用时,您无法访问 Activity 或 Fragment。

因此,您必须实现自己的“所有者”类,与从InputMethodService 获得的生命周期回调绑定。这样做的方法如下:

    创建一个单独的类来负责处理生命周期所有权任务:
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 的类中,覆盖回调并将这些消息中继到我们在步骤 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()
    

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

来源:

Yannick's answer on this page Ian Lake's answer Answer on Compose Issue Tracker

【讨论】:

以上是关于InputMethodService 与 Jetpack Compose - ComposeView 导致:组合到不传播 ViewTreeLifecycleOwner 的视图中的主要内容,如果未能解决你的问题,请参考以下文章

从 Activity 向服务发送消息 - Android

Android 横屏时禁止输入法全屏

在Jetson TX2上安装OpenCV(3.4.0)

[转]软键盘用法总结

安卓应用专用软键盘

Jetpack Compose开源:基于Kotlin的响应式编程方案,简化UI开发