切腹のイラスト

AndroidのServiceでComposeを使う(令和最新版)

{
  date: "",
  category: "/memo",
  tags: ["Android", "Jetpack Compose"]
}

だいぶ沼ったのでメモ。

動機

筆者はAndroid初心者で,Viewをコネコネしたことがない。 Composeで済ませられるなら全てComposeで済ませたい。

というわけで,InputMethodServiceのViewをComposeで作成した。

問題

StackOverflowやGitHub code searchを頼りに,以下のようなコードを書いた。 が,動かなかった。(´・_・`)

package net.dyama.hogehoge

import android.inputmethodservice.InputMethodService
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.ViewTreeLifecycleOwner

@Composable
fun Keyboard() {
  Box {
    Text("hello!")
  }
}

class HogeIME: InputMethodService(), LifecycleOwner {           //
  private val view by lazy {
    ComposeView(this).apply {
      setContent {
        Keyboard()
      }
    }
  }

  override fun onCreate() {                                     //
    super.onCreate()                                            //
    ViewTreeLifecycleOwner.set(view, this)                      //
  }                                                             //

  // InputMethodService
  override fun onCreateInputView() = view

  // LifecycleOwner                                             //
  private val lifecycle = LifecycleRegistry(this)               //
  override fun getLifecycle() = lifecycle                       //
}
E/AndroidRuntime: FATAL EXCEPTION: main
    Process: net.dyama.hogehoge, PID: xxxxx
    java.lang.IllegalStateException: ViewTreeLifecycleOwner not found from android.widget.LinearLayout{xxxxxxx... android:id/parentPanel}
        at androidx.compose.ui.platform.WindowRecomposer_androidKt.createLifecycleAwareWindowRecomposer(WindowRecomposer.android.kt:349)
        at androidx.compose.ui.platform.WindowRecomposer_androidKt.createLifecycleAwareWindowRecomposer$default(WindowRecomposer.android.kt:324)
        at androidx.compose.ui.platform.WindowRecomposerFactory$Companion$LifecycleAware$1.createRecomposer(WindowRecomposer.android.kt:168)
        at androidx.compose.ui.platform.WindowRecomposerPolicy.createAndInstallWindowRecomposer$ui_release(WindowRecomposer.android.kt:224)
        at androidx.compose.ui.platform.WindowRecomposer_androidKt.getWindowRecomposer(WindowRecomposer.android.kt:299)
        at androidx.compose.ui.platform.AbstractComposeView.resolveParentCompositionContext(ComposeView.android.kt:242)
        at androidx.compose.ui.platform.AbstractComposeView.ensureCompositionCreated(ComposeView.android.kt:249)
        at androidx.compose.ui.platform.AbstractComposeView.onAttachedToWindow(ComposeView.android.kt:281)
        at android.view.View.dispatchAttachedToWindow(View.java:21290)
        at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3491)
        at android.view.ViewGroup.addViewInner(ViewGroup.java:5291)
        at android.view.ViewGroup.addView(ViewGroup.java:5077)
        at android.view.ViewGroup.addView(ViewGroup.java:5049)
        at android.inputmethodservice.InputMethodService.setInputView(InputMethodService.java:2242)
        at android.inputmethodservice.InputMethodService.updateInputViewShown(InputMethodService.java:2077)
        at android.inputmethodservice.InputMethodService.prepareWindow(InputMethodService.java:2646)
        at android.inputmethodservice.InputMethodService.showWindow(InputMethodService.java:2575)
        at android.inputmethodservice.InputMethodService$InputMethodImpl.showSoftInput(InputMethodService.java:923)
        at android.inputmethodservice.InputMethodService$InputMethodImpl.showSoftInputWithToken(InputMethodService.java:897)
        at android.inputmethodservice.IInputMethodWrapper.executeMessage(IInputMethodWrapper.java:232)
        at com.android.internal.os.HandlerCaller$MyHandler.handleMessage(HandlerCaller.java:44)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loopOnce(Looper.java:201)
        at android.os.Looper.loop(Looper.java:288)
        at android.app.ActivityThread.main(ActivityThread.java:7872)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)

半泣きになりながらしばらくゴニョゴニョして,以下のことがわかった。

  • AbstractComposeView(ConposeViewの継承元)はいろんなところでCompositionContextというものを使っている
  • AbstractComposeViewは,Viewのツリーを探し回ってもCompositionContextが存在しないとき,Recomposer(implements CompositionContext)を,WindowRecomposerPolicy.createAndInstallWindowRecomposer(rootView)によって生成する
  • createAndInstallWindowRecomposercreateLifecycleAwareWindowRecomposerを呼び出し,rootViewViewTreeLifecycleOwnerを探す → null
  • ViewTreeLifecycleOwner.get(view.rootView) → 非null
  • なんで?

後日談。と言うか、今回のオチ。

今の最新のリリース版のライブラリを使っているのだが,ここら辺は絶賛改築中らしく,次のBetaリリースを導入するとコンパイルすら通らなくなってしまった。

というわけで,WindowRecomposer.android.ktを参考に自分でRecomposerを生やした。 そして,エラー内容が変わった(!!!!)ので,その他の修正も加えた。

ライブラリのバージョン

version catalog (settings.gradle.kts)
  versionCatalogs {
    create("libs") {
      // versions
      version("android-plugin", "7.4.1")
      version("kotlin", "1.7.20")
      version("compose", "1.3.2")

      // libraries
      library("androidx-core", "androidx.core:core-ktx:1.9.0")
      library("androidx-lifecycle-runtime", "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
      library("androidx-activity-compose", "androidx.activity:activity-compose:1.6.1")
      library("androidx-compose-ui", "androidx.compose.ui", "ui").versionRef("compose")
      library("androidx-compose-ui-tooling-preview", "androidx.compose.ui", "ui-tooling-preview").versionRef("compose")
      library("androidx-compose-material3", "androidx.compose.material3:material3:1.0.1")
      library("androidx-datastore", "androidx.datastore:datastore-preferences:1.0.0")

      library("junit", "junit:junit:4.13.2")
      library("androidx-test-ext-junit", "androidx.test.ext:junit:1.1.5")
      library("androidx-test-espresso-core", "androidx.test.espresso:espresso-core:3.5.1")
      library("androidx-compose-ui-test-junit4", "androidx.compose.ui", "ui-test-junit4").versionRef("compose")
      library("androidx-compose-ui-tooling", "androidx.compose.ui", "ui-tooling").versionRef("compose")
      library("androidx-compose-ui-test-manifest", "androidx.compose.ui", "ui-test-manifest").versionRef("compose")

      // plugins
      plugin("android-application", "com.android.application").versionRef("android.plugin")
      plugin("android-library", "com.android.library").versionRef("android.plugin")
      plugin("kotlin-android", "org.jetbrains.kotlin.android").versionRef("kotlin")
    }
  }
dependencies (app/build.gradle.kts)
dependencies {
  implementation(libs.androidx.core)
  implementation(libs.androidx.lifecycle.runtime)
  implementation(libs.androidx.activity.compose)
  implementation(libs.androidx.compose.ui)
  implementation(libs.androidx.compose.ui.tooling.preview)
  implementation(libs.androidx.compose.material3)
  implementation(libs.androidx.datastore)

  testImplementation(libs.junit)
  androidTestImplementation(libs.androidx.test.ext.junit)
  androidTestImplementation(libs.androidx.test.espresso.core)
  androidTestImplementation(libs.androidx.compose.ui.test.junit4)
  debugImplementation(libs.androidx.compose.ui.tooling)
  debugImplementation(libs.androidx.compose.ui.test.manifest)
}

参考

問題が解決した後に本質情報を見つけた。