Google は、黒人コミュニティに対する人種平等の促進に取り組んでいます。取り組みを見る

Compose の相互運用性

Jetpack Compose は、確立されたビューベースの UI アプローチで動作するように設計されています。新しいアプリをビルドする場合、Compose を使用して UI 全体を実装することをおすすめします。ただし、既存のアプリを変更する場合は、アプリを移行しないことをおすすめします。代わりに、Compose を既存の UI 設計と組み合わせることができます。

ビューベースの UI と Compose を組み合わせる方法は主に 2 つあります。

  • Compose の要素を既存の UI に追加するには、まったく新しい Compose ベースの画面を作成するか、既存のフラグメントまたはビュー レイアウトに Compose 要素を追加します。
  • ビューベースの UI 要素をコンポーズ可能な関数に追加できます。これにより、Compose 以外のウィジェットを Compose ベースの設計に追加できます。

Android View での Compose

ビューベースの設計を使用している既存のアプリに Compose ベースの UI を追加できます。

完全に Compose をベースとした画面を新たに作成するには、アクティビティで setContent() メソッドを呼び出し、任意のコンポーズ可能な関数を渡します。

class ExampleActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContent { // In here, we can call composables!
      MaterialTheme {
        Greeting(name = "compose")
      }
    }
  }
}

@Composable
fun Greeting(name: String) {
      Text (text = "Hello $name!")
}

このコードは、Compose のみのアプリで見られるコードに似ています。

Compose UI コンテンツをフラグメントまたは既存の View レイアウトに組み込む場合は、ComposeView を使用し、その setContent() メソッドを呼び出します。ComposeView は Android View です。ComposeViewViewTreeLifecycleOwner にアタッチする必要があります。ViewTreeLifecycleOwner を使用すると、構成を維持したままビューのアタッチとデタッチを繰り返し行えます。ComponentActivityFragmentActivityAppCompatActivity はすべて、ViewTreeLifecycleOwner を実装するクラスの例です。

ComposeView は、他の View と同じように XML レイアウトに追加できます。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/hello_world"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Hello Android!" />

    <ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

Kotlin ソースコードでは、XML で定義されたレイアウト リソースからレイアウトをインフレートします。XML ID を使用して ComposeView を取得し、setContent() を呼び出して Compose を使用します。

class ExampleFragment : Fragment() {

  override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
  ): View {
    // Inflate the layout for this fragment
    return inflater.inflate(
      R.layout.fragment_example, container, false
    ).apply {
      findViewById<ComposeView>(R.id.compose_view).setContent {
        // In Compose world
        MaterialTheme {
          Text("Hello Compose!")
        }
      }
    }
  }
}

2 つの若干異なるテキスト要素(一方が他方の上)

図 1. View UI 階層に Compose 要素を追加するコードの出力を示しています。「Hello Android!」のテキストは TextView ウィジェットで表示されます。「Hello Compose!」のテキストは Compose テキスト要素で表示されます。

また、全画面が Compose で作成されている場合、ComposeView をフラグメントに直接含めることもできます。これにより、XML レイアウト ファイル全体の使用を避けることができます。

class ExampleFragment : Fragment() {

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

同じレイアウトに複数の ComposeView 要素がある場合、savedInstanceState が機能するためには、各要素に一意の ID が必要です。詳細については、SavedInstanceState のセクションをご覧ください。

class ExampleFragment : Fragment() {

  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
        ...
      }
    }
  }
}

ComposeView ID は res/values/ids.xml ファイルで定義されています。

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

Compose での Android View

Compose UI に Android View 階層を含めることができます。このアプローチは特に、Compose でまだ利用できない UI 要素(AdViewMapView など)を使用する場合に便利です。このアプローチでは、設計したカスタムビューを再利用することもできます。

ビュー要素または階層を含めるには、AndroidView コンポーザブルを使用します。AndroidView には、View を返すラムダが渡されます。AndroidView には、ビューがインフレートされるときに呼び出される update コールバックも用意されています。AndroidView は、コールバック内で読み込まれた State が変更されるたびに、再コンポーズを行います。

@Composable
fun CustomView() {
  val selectedItem = remember { mutableStateOf(0) }

  val context = ContextAmbient.current
  val customView = remember {
    // Creates custom view
    CustomView(context).apply {
      // Sets up listeners for View -> Compose communication
      myView.setOnClickListener {
        selectedItem.value = 1
      }
    }
  }

  // Adds view to Compose
  AndroidView({ customView }) { view ->
    // View's been inflated - add logic here if necessary

    // As selectedItem is read here, AndroidView will recompose
    // whenever the state changes
    // Example of Compose -> View communication
    view.coordinator.selectedItem = selectedItem
  }
}

@Composable
fun ContentExample() {
  Column(Modifier.fillMaxSize()) {
    Text("Look at this CustomView!")
    CustomView()
  }
}

XML レイアウトを埋め込むには、androidx.compose.ui:ui-viewbinding ライブラリで提供される AndroidViewBinding API を使用します。そのためには、プロジェクトでビュー バインディングを有効にする必要があります。

@Composable
fun AndroidViewBindingExample() {
  AndroidViewBinding(ExampleLayoutBinding::inflate) {
    exampleView.setBackgroundColor(Color.GRAY)
  }
}

Compose から View システムを呼び出す

Compose フレームワークには、Compose コードでビューベースの UI を操作できるようにする API が多数用意されています。

システム リソース

Compose フレームワークには、Compose コードでビューベースの UI 階層からリソースを取得できるようにする ...Resource() ヘルパー メソッドが用意されています。次に例を示します。

Text(
  text = stringResource(R.string.ok),
  modifier = Modifier.padding(dimensionResource(R.dimen.padding_small))
)

Icon(
  assert = vectorResource(R.drawable.ic_plane),
  tint = colorResource(R.color.Blue700)
)

コンテキスト

ContextAmbient.current プロパティは、現在のコンテキストを提供します。たとえば、次のコードは現在のコンテキストでビューを作成します。

@Composable
fun rememberCustomView(): CustomView {
  val context = ContextAmbient.current
  return remember { CustomView(context).apply { ... } }
}

その他の操作

必要な操作のためのユーティリティが定義されていない場合は、一般的な Compose ガイドラインに従い、データは下に流れ、イベントは上に流れるようにすることをおすすめします(詳しくは Compose の思想をご覧ください)。たとえば、このコンポーザブルは別のアクティビティを起動します。

class ExampleActivity : AppCompatActivity {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // get data from savedInstanceState
    setContent {
      MaterialTheme {
        ExampleComposable(data, onButtonClick = {
          startActivity(...)
        })
      }
    }
  }
}

@Composable
fun ExampleComposable(data: DataExample, onButtonClick: () -> Unit) {
  Button(onClick = onButtonClick) {
    Text(data.title)
  }
}

共通ライブラリとの統合

Compose でお好みのライブラリを使用できます。このセクションでは、有用なライブラリを組み込む方法について説明します。

ViewModel

アーキテクチャ コンポーネントの ViewModel ライブラリを使用している場合、viewModel() 関数を呼び出すことで、任意のコンポーザブルから ViewModel にアクセスできます。

class ExampleViewModel() : ViewModel() { ... }

@Composable
fun MyExample() {
  val viewModel: ExampleViewModel = viewModel()

  ... // use viewModel here
}

viewModel() は、既存の ViewModel を返すか、指定されたスコープで新しく作成します。ViewModel は、スコープが存続している限り保持されます。たとえば、コンポーザブルがアクティビティで使用されている場合、viewModel() は、アクティビティが終了するまで、またはプロセスが強制終了されるまで、同じインスタンスを返します。

@Composable
fun MyExample() {
  // Returns the same instance as long as the activity is alive,
  // just as if you grabbed the instance from an Activity or Fragment
  val viewModel: ExampleViewModel = viewModel()
}

@Composable
fun MyExample2() {
  val viewModel: ExampleViewModel = viewModel() // Same instance as in MyExample
}

ViewModel に依存関係がある場合、viewModel() はオプションの ViewModelProvider.Factory をパラメータとして受け取ります。

データのストリーム

Compose には、Android で最も一般的なストリーム ベースの拡張機能が付属しています。各拡張機能は、それぞれ異なるアーティファクトによって提供されます。

これらのアーティファクトは、リスナーとして登録し、値を State として表します。新しい値が出力されるたびに、Compose は state.value が使用される UI 部分を再コンポーズします。たとえば、このコードでは、exampleLiveData が新しい値を出力するたびに ShowData が再コンポーズされます。

@Composable
fun MyExample() {
  val viewModel: ExampleViewModel = viewModel()
  val dataExample = viewModel.exampleLiveData.observeAsState()

  // Because the state is read here,
  // MyExample recomposes whenever dataExample changes.
  dataExample?.let {
    ShowData(dataExample)
  }
}

Compose の非同期オペレーション

Compose には、コンポーザブル内から非同期オペレーションを実行できるメカニズムが用意されています。

コールバック ベースの API では、MutableStateonCommit() を組み合わせて使用できます。MutableState を使用してコールバックの結果を保存し、結果が変更されたときに影響を受ける UI を再コンポーズします。パラメータが変更されるたびにオペレーションを実行するには、onCommit() を使用します。また、オペレーションが完了する前にコンポーズが終了した場合に保留中のオペレーションを消去するよう、onDispose() メソッドを定義することもできます。次の例は、これらの API がどのように連携するかを示しています。

@Composable
fun fetchImage(url: String): ImageAsset? {
    // Holds our current image, and will be updated by the onCommit lambda below
    var image by remember(url) { mutableStateOf<ImageAsset?>(null) }

    onCommit(url) {
        // This onCommit lambda will be invoked every time url changes

        val listener = object : ExampleImageLoader.Listener() {
            override fun onSuccess(bitmap: Bitmap) {
                // When the image successfully loads, update our image state
                image = bitmap.asImageAsset()
            }
        }

        // Now execute the image loader
        val imageLoader = ExampleImageLoader.get()
        imageLoader.load(url).into(listener)

        onDispose {
            // If we leave composition, cancel any pending requests
            imageLoader.cancel(listener)
        }
    }

    // Return the state-backed image property. Any callers of this function
    // will be recomposed once the image finishes loading
    return image
}

非同期オペレーションが suspend 関数である場合は、代わりに launchInComposition() を使用できます。

/** Example suspending loadImage function */
suspend fun loadImage(url: String): Bitmap

@Composable
fun fetchImage(url: String): ImageAsset? {
    // This holds our current image, and will be updated by the
    // launchInComposition lambda below
    var image by remember(url) { mutableStateOf<ImageAsset?>(null) }

    // launchInComposition will automatically launch a coroutine to execute
    // the given block. If the `url` changes, any previously launched coroutine
    // will be cancelled, and a new coroutine launched.
    launchInComposition(url) {
        image = loadImage(url)
    }

    // Return the state-backed image property
    return image
}

SavedInstanceState

アクティビティまたはプロセスを再作成した後、savedInstanceState を使用して UI の状態を復元します。savedInstanceState は再コンポーズ全体で状態を保持します。さらに、savedInstanceState はアクティビティとプロセスの再作成全体でも状態を保持します。

@Composable
fun MyExample() {
  var selectedId by savedInstanceState<String?> { null }
  ...
}

Bundle に追加されたデータタイプはすべて、自動的に保存されます。Bundle に追加できないものを保存する場合は、複数のオプションがあります。

最も簡単なソリューションは、@Parcelize アノテーションをオブジェクトに追加する方法です。オブジェクトが Parcelable になり、バンドルできます。たとえば、このコードは Parcelable の City データ型を作成し、状態に保存します。

@Parcelize
data class City(name: String, country: String): Parcelable

@Composable
fun MyExample() {
  var selectedCity = savedInstanceState { City("Madrid", "Spain") }
}

なんらかの理由で @Parcelize が適さない場合は、mapSaver を使用して、オブジェクトを Bundle に保存できる値のセットに変換するための独自ルールを定義できます。

data class City(name: String, country: String)

val CitySaver = run {
  val nameKey = "Name"
  val countryKey = "Country"
  mapSaver(
    save = { mapOf(nameKey to it.name, nameKey to it.country) },
    restore = { City(it[nameKey] as String, it[countryKey] as String) }
  )
}

@Composable
fun MyExample() {
  var selectedCity = savedInstanceState(CitySaver) { City("Madrid", "Spain") }
}

マップのキーを定義する必要がないようにするには、listSaver を使用して、そのインデックスをキーとして使用することもできます。

data class City(name: String, country: String)

val CitySaver = listSaver<City, Any>(
  save = { listOf(it.name, it.country) },
  restore = { City(it[0] as String, it[1] as String) }
)

@Composable
fun MyExample() {
  var selectedCity = savedInstanceState(CitySaver) { City("Madrid", "Spain") }
  ...
}

テーマ設定

アプリで Android のマテリアル デザイン コンポーネントを使用している場合は、MDC Compose Theme Adapter ライブラリを使用すると、既存のテーマのタイポグラフィシェイプのテーマ設定を、コンポーザブル内から簡単に再利用できます。

class ExampleActivity : AppCompatActivity {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContent {
      // We use MdcTheme instead of MaterialTheme {}
      MdcTheme {
        ExampleComposable(...)
      }
    }
  }
}

テスト

createAndroidComposeRule() API を使用して、View と Compose コードを組み合わせてテストできます。詳細については、Compose レイアウトのテストをご覧ください。

詳細

Jetpack Compose を既存の UI と統合する方法については、Jetpack Compose への移行の Codelab をご覧ください。