InputMethodService с Jetpack Compose - причины ComposeView: добавлены в представление, которое не распространяет ViewTreeLifecycleOwner

Вы можете найти образец проекта для воспроизведения проблемы на Github

Я пытался использовать Jetpack Compose для пользовательского интерфейса клавиатуры. В конечном итоге, когда я пытаюсь надуть клавиатуру через 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)
    }

}

or

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)

В официальной документации говорится

Вы должны прикрепить ComposeView к ViewTreeLifecycleOwner. ViewTreeLifecycleOwner позволяет многократно присоединять и отсоединять представление, сохраняя при этом композицию. ComponentActivity, FragmentActivity и AppCompatActivity - все это примеры классов, реализующих ViewTreeLifecycleOwner.

Однако я не могу использовать ComponentActivity, FragmentActivity или AppCompatActivity для расширения представления, которое вызывает код компоновки. Я застрял в реализации ViewTreeLifecycleOwner. Я не знаю, как это сделать.

Как я могу использовать @Composable функции в качестве представления метода ввода?

Изменить: Как было предложено CommonsWare, я использовал метод ViewTreeLifecycleOwner.set(...), и мне также пришлось реализовать ViewModelStoreOwner и SavedStateRegistryOwner:

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)
[...]

Это как-то связано с распространением событий жизненного цикла, потому что, когда я закомментирую методы onCreate и onDestroy, клавиатура открывается без сбоев, но клавиатура не видна


person Yannick    schedule 16.01.2021    source источник
comment
Документы могут показаться неправильными, поскольку вы не можете реализовать ViewTreeLifecycleOwner. Я предполагаю, что они означают, что вам нужно вызвать ViewTreeLifecycleOwner.set(), передавая View и LifecycleOwner. Я предполагаю, что вам нужно будет скопировать код из LifecycleService в свой, так что ваша служба сама по себе LifecycleOwner, которую вы можете использовать.   -  person CommonsWare    schedule 17.01.2021


Ответы (2)


После поиска аналогичных реализаций в 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
}

Не знаю, лучшая ли это реализация с точки зрения производительности, но она отлично работает даже на старых устройствах. Идеи по улучшению приветствуются

person Yannick    schedule 17.01.2021

Мой ответ во многом основан на ответе Янника и других связанных источниках, поэтому им заслуга.

По сути, для работы Compose необходимы три класса Owner из пакета androidx.lifecycle: LifecycleOwner, ViewModelStoreOwner и SavedStateRegistryOwner. AppCompatActivity и Fragment уже реализуют эти интерфейсы, поэтому установка ComposeView для них работает "из коробки".

Однако при создании приложения IME у вас нет доступа к Activity или Fragment.

Следовательно, вы должны реализовать свои собственные классы Owner, привязанные к обратным вызовам жизненного цикла, которые вы получаете от InputMethodService. Вот как это сделать:

  1. Создайте отдельный класс, отвечающий за обработку задач владения жизненным циклом:
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. Теперь из класса, расширяющего 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 имеет надлежащий жизненный цикл для прослушивания, который он использует, чтобы определить, когда выполнять перекомпоновку и т. Д.

Источники:

person Mauro Banze    schedule 05.04.2021