🙌

XMLベースのコードをJetpack Composeに移行する方法 3-ComposeView~旧来のXMLの一部だけ利用

2022/04/25に公開

概要

前々回前回の記事では、旧来の方式のXMLレイアウト・Kotlin/Javaで作成した旧来のカスタムビューをJetpack Composeのコードから、再利用する方法を記述しました。

この記事では、旧来のXMLレイアウトの一部をJetpack Composeで作る/書き直す方法を紹介します。
また、FragmentのView全てをJetpack Composeに置き換えることで、旧来のXMLレイアウトを無くすことも可能です。

旧来のコード

旧来のコードからJetpack Composeを呼び出すので、わかりやすさのために、まずは、旧来のコードを書きます。

ちなみにActivityは一部Jetpack Composeを使ったとしても変更がありません。

class ComposeViewUsageActivity : AppCompatActivity() {

  companion object {
    fun intent(context: Context) = Intent(context, ComposeViewUsageActivity::class.java).apply {
      flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
    }
  }

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.compose_view_activity)
    if (savedInstanceState == null) {
      supportFragmentManager.beginTransaction()
        .replace(R.id.container, ComposeViewUsageFragment.newInstance())
        .commitNow()
    }
  }
}

Fragmentは変わります。今回は、DataBindingを使った場合の記述をしています。

class ComposeViewUsageFragment : Fragment(R.layout.compose_view_fragment) {

  companion object {
    fun newInstance() = ComposeViewUsageFragment()
  }

  private lateinit var viewModel: ComposeViewModel

  private var _binding: ComposeViewFragmentBinding? = null
  private val binding get() = _binding!!

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    viewModel = ViewModelProvider(this)[ComposeViewModel::class.java]
    _binding = DataBindingUtil.bind(view)
    binding.viewModel = viewModel
    binding.lifecycleOwner = viewLifecycleOwner
  }

  override fun onDestroyView() {
    super.onDestroyView()
    _binding = null
  }
}

以下のレイアウトXMLがあったとして、@+id/label2 の部分をJetpack Composeに置き換えたいと思います。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="viewModel"
            type="com.ko2ic.spike.migrate.compose.ui.ComposeViewModel" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:id="@+id/label1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="24dp"
            android:text="Hello"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
        <TextView
            android:id="@+id/label2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="24dp"
            android:text="Hello label2!!"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/label1" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

XMLレイアウトの一部をJetpack Composeで使う方法

XMLレイアウト

@+id/label2@+id/compose_view に変えています。
marginは今回はJetpack Compose側で定義するので消しました。
TextViewandroidx.compose.ui.platform.ComposeView に変えています。
それにと伴いandroid:text も削除しています。

androidx.compose.ui.platform.ComposeView を使いFragmentで実装することで、Jetpack Composeの実装をXMLレイアウトに当て込むことができるようになります。

-        <TextView
-            android:id="@+id/label2"
+        <androidx.compose.ui.platform.ComposeView
+            android:id="@+id/compose_view"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_marginTop="24dp"
-            android:text="Hello label2!!"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toBottomOf="@id/label1" />

Fragment

以下のコードに変更するとTextViewの@+id/label1の表示の「Hello」の下にJetpack Composeで記述した「Hello Compose!!」が表示されます。

   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
     super.onViewCreated(view, savedInstanceState)
     viewModel = ViewModelProvider(this)[ComposeViewModel::class.java]
     _binding = DataBindingUtil.bind(view)
     binding.viewModel = viewModel
     binding.lifecycleOwner = viewLifecycleOwner
+    binding.composeView.apply {
+      setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+      setContent {
+        AppCompatTheme {
+          HelloCompose()
+        }
+      }
+    }
   }

+  @Composable
+  private fun HelloCompose() {
+    Text(
+      "Hello Compose!!",
+      style = MaterialTheme.typography.body2,
+      modifier = Modifier
+        .fillMaxWidth()
+        .padding(24.dp)
+        .wrapContentWidth(Alignment.CenterHorizontally)
+    )
  }

バインディングでXMLのidのcomopose_viewに対して、Jetpack Composeお馴染みのsetContentを呼び出しています。

注目すべきなところは、テーマにAppCompatThemeを設定しているところとsetViewCompositionStrategyです。
これら二つについて次に説明します。

Themeについて

XMLレイアウトで実装してきたプロジェクトでは、すでにXMLベースでのTheme設定をしているはずです。それの再利用をした方が便利です。それを実現するためにライブラリが用意されています。

Material Design Components(MDC)を使用している場合は、以下を追加して、MdcTheme or Mdc3Theme を指定してください。

implementation "com.google.android.material:compose-theme-adapter:1.1.7"
 or 
implementation "com.google.android.material:compose-theme-adapter-3:1.0.7"

AppCompat XML テーマを使用している場合は、以下を追加して、AppCompatThemeを指定してください。

implementation "com.google.accompanist:accompanist-appcompat-theme:0.16.0"

setViewCompositionStrategyについて

Jetpack Composeでは、ビューがウィンドウからデタッチされるたびに、Compositionを破棄するのがデフォルトになっています。setViewCompositionStrategyは、その破棄のタイミングを変更する関数です。
ComposeViewの場合は、フラグメントのライフサイクルに合わせて破棄したいので、DisposeOnViewTreeLifecycleDestroyedを指定します。

FragmentのViewをJetpack Composeにする方法

以下のようにComposeViewを直接インスタンスすれば良いです。XMLレイアウトを介さない際のデメリットは、BindingAdapterが機能しないことでしょうか。

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return ComposeView(requireContext()).apply {
            setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                MaterialTheme {
                    // In Compose world
                    Text("Hello Compose!")
                }
            }
        }
    }

FragmentのViewでComposeViewが二つある場合

以下のような既存のコード(LinearLayout)の内部で、ComposeViewが複数ある場合は、savedInstanceStateが動作するようにidを指定する必要があります。

  override fun onCreateView(...): View = LinearLayout(...).apply {
      addView(ComposeView(...).apply {
          id = R.id.compose_view_x
          ...
      })
      addView(TextView(...))
      addView(ComposeView(...).apply {
          id = R.id.compose_view_y
          ...
      })
    }
  }

idは、res/values/ids.xml などで定義しておく必要があります。

<resources>
    <item name="compose_view_x" type="id" />
    <item name="compose_view_y" type="id" />
</resources>

Preview

Fragment内のJetpack ComposeでもActivityの時と同じようにPreviewは普通に使えます。

  @Preview(
    uiMode = Configuration.UI_MODE_NIGHT_YES,
    showBackground = true
  )
  @Preview
  @Composable
  fun DarkHelloComposePreview() {
    AppCompatTheme {
      HelloCompose()
    }
  }

まとめ

ComposeViewによって、既存のソースを少しづつJetpack Composeに変更していくことができます。
それもXMLレイアウトの一部だけ、フラグメントの全部、BindingAdapterをフルで使い続けたいなど、色々なパターンで少しづつ変更可能なように提供されているのが特徴でしょう。

NewsPicks の Zenn

Discussion