Compose をアプリ内に導入する際、Compose とビューベースの UI を組み合わせることもできます。ここでは、Compose に簡単に移行するための API、推奨事項、ヒントについて説明します。
ビュー内の Compose
ビューベースの設計を使用している既存のアプリに Compose ベースの UI を追加できます。
完全に Compose をベースとした画面を新たに作成するには、アクティビティで setContent()
メソッドを呼び出し、任意のコンポーズ可能な関数を渡します。
class ExampleActivity : ComponentActivity() { 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 のみのアプリで見られるコードに似ています。
ComposeView の ViewCompositionStrategy
デフォルトでは、ビューがウィンドウからデタッチされるたびに、Compose は Composition を破棄します。ComposeView
や AbstractComposeView
などの Compose UI の View
タイプは、この動作を定義する ViewCompositionStrategy
を使用します。
デフォルトでは、Compose は DisposeOnDetachedFromWindowOrReleasedFromPool
戦略を使用します。ただし、Compose UI の View
タイプが次のものの中で使用される状況では、このデフォルト値が望ましくない場合があります。
フラグメント。Composition は、Compose UI の
View
タイプに対するフラグメントのビュー ライフサイクルに従って、状態を保存する必要があります。遷移。Compose UI の
View
が遷移の中で使用されると、遷移の終了時ではなく遷移の開始時にウィンドウからデタッチされるため、コンポーザブルはまだ画面に表示されているときに状態を廃棄します。独自のライフサイクルで管理されるカスタム
View
。
いくつかの状況では、AbstractComposeView.disposeComposition
を手動で呼び出さない限り、アプリで Composition インスタンスからのメモリリークが徐々に発生することもあります。
Composition が不要になったときに自動的に破棄するには、別の戦略を設定するか、setViewCompositionStrategy
メソッドを呼び出して独自の戦略を作成します。たとえば、DisposeOnLifecycleDestroyed
戦略は、lifecycle
が破棄されたときに Composition を破棄します。この戦略は、既知の LifecycleOwner
と 1 対 1 の関係を共有する Compose UI の View
タイプに適しています。LifecycleOwner
が不明な場合は、DisposeOnViewTreeLifecycleDestroyed
を使用できます。
この API の実際の動作については、フラグメント内の ComposeView をご覧ください。
フラグメント内の ComposeView
Compose UI コンテンツをフラグメントまたは既存の View レイアウトに組み込む場合は、ComposeView
を使用し、その setContent()
メソッドを呼び出します。ComposeView
は Android View
です。
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!" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
Kotlin ソースコードでは、XML で定義されたレイアウト リソースからレイアウトをインフレートします。次に、XML ID を使用して ComposeView
を取得し、ホスト View
に最適な Composition 戦略を設定して、Compose を使用するために setContent()
を呼び出します。
class ExampleFragment : Fragment() { private var _binding: FragmentExampleBinding? = null // This property is only valid between onCreateView and onDestroyView. private val binding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { _binding = FragmentExampleBinding.inflate(inflater, container, false) val view = binding.root binding.composeView.apply { // Dispose of the Composition when the view's LifecycleOwner // is destroyed setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { // In Compose world MaterialTheme { Text("Hello Compose!") } } } return view } override fun onDestroyView() { super.onDestroyView() _binding = null } }
図 1. View UI 階層に Compose 要素を追加するコードの出力を示しています。「Hello Android!」のテキストは TextView
ウィジェットで表示されます。「Hello Compose!」のテキストは Compose テキスト要素で表示されます。
また、全画面が Compose で作成されている場合、ComposeView
をフラグメントに直接含めることもできます。これにより、XML レイアウト ファイル全体の使用を避けることができます。
class ExampleFragmentNoXml : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return ComposeView(requireContext()).apply { // Dispose of the Composition when the view's LifecycleOwner // is destroyed setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { MaterialTheme { // In Compose world Text("Hello Compose!") } } } } }
同じレイアウト内の複数の ComposeView
同じレイアウトに複数の ComposeView
要素がある場合、savedInstanceState
が機能するためには、各要素に一意の ID が必要です。
class ExampleFragmentMultipleComposeView : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View = LinearLayout(requireContext()).apply { addView( ComposeView(requireContext()).apply { setViewCompositionStrategy( ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed ) id = R.id.compose_view_x // ... } ) addView(TextView(requireContext())) addView( ComposeView(requireContext()).apply { setViewCompositionStrategy( ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed ) 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 内のビュー
Compose UI に Android View 階層を含めることができます。このアプローチは特に、Compose でまだ利用できない UI 要素(AdView
など)を使用する場合に便利です。このアプローチでは、設計したカスタムビューを再利用することもできます。
ビュー要素または階層を含めるには、AndroidView
コンポーザブルを使用します。AndroidView
には、View
を返すラムダが渡されます。AndroidView
には、ビューがインフレートされるときに呼び出される update
コールバックも用意されています。AndroidView
は、コールバック内で読み込まれた State
が変更されるたびに、再コンポーズを行います。AndroidView
は、他の多くの組み込みコンポーザブルと同様に、Modifier
パラメータを受け取ります。これは親コンポーザブルでの位置を設定したりするために使用できます。
@Composable fun CustomView() { var selectedItem by remember { mutableStateOf(0) } // Adds view to Compose AndroidView( modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree factory = { context -> // Creates view MyView(context).apply { // Sets up listeners for View -> Compose communication setOnClickListener { selectedItem = 1 } } }, update = { view -> // View's been inflated or state read in this block has been updated // Add logic here if necessary // As selectedItem is read here, AndroidView will recompose // whenever the state changes // Example of Compose -> View communication view.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 のフラグメント
AndroidViewBinding
コンポーザブルを使用して、Fragment
を Compose に追加します。AndroidViewBinding
には、コンポーザブルがコンポジションから離れたときにフラグメントを削除するなど、フラグメント固有の処理があります。
そのためには、FragmentContainerView
を含む XML を Fragment
のホルダーとしてインフレートします。
たとえば、my_fragment_layout.xml
が定義されている場合、android:name
XML 属性を Fragment
のクラス名に置き換えて次のようなコードを使用できます。
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container_view"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:name="com.example.MyFragment" />
Compose で、このフラグメントを次のようにインフレートします。
@Composable fun FragmentInComposeExample() { AndroidViewBinding(MyFragmentLayoutBinding::inflate) { val myFragment = fragmentContainerView.getFragment<MyFragment>() // ... } }
同じレイアウトで複数のフラグメントを使用する必要がある場合は、FragmentContainerView
ごとに一意の ID を定義していることを確認してください。
Compose から Android フレームワークを呼び出す
Compose は、Android フレームワーク クラス内で動作します。たとえば、Activity
や Fragment
などの Android View クラスでホストされているため、Context
、システム リソース、Service
、BroadcastReceiver
などの Android フレームワーク クラスを使用することが必要な場合もあります。
システム リソースについて詳しくは、Compose のリソースのドキュメントをご覧ください。
コンポジション ローカル
CompositionLocal
クラスを使用すると、コンポーズ可能な関数を通じて暗黙的にデータを渡すことができます。通常、UI ツリーの特定のノードに値が設定されます。その値は、コンポーズ可能な関数のパラメータとして CompositionLocal
を宣言しなくても、コンポーズ可能な子孫で使用できます。
CompositionLocal
は、Compose の Android フレームワーク タイプ(Context
、Configuration
または Compose コードがホストされている View
など)の値を対応する LocalContext
、LocalConfiguration
、LocalView
に伝播するために使用されます。
IDE のオートコンプリートで検出しやすいように CompositionLocal
クラスの先頭に Local
が付いています。
CompositionLocal
の現在の値にアクセスするには、current
プロパティを使用します。たとえば、下記のコードは Toast.makeToast
メソッドに LocalContext.current
を指定してトースト メッセージを表示します。
@Composable fun ToastGreetingButton(greeting: String) { val context = LocalContext.current Button(onClick = { Toast.makeText(context, greeting, Toast.LENGTH_SHORT).show() }) { Text("Greet") } }
より詳しい例については、このドキュメントの最後にある事例紹介: BroadcastReceivers をご覧ください。
その他の操作
必要な操作のためのユーティリティが定義されていない場合は、一般的な Compose ガイドラインに従い、データは下に流れ、イベントは上に流れるようにすることをおすすめします(詳しくは Compose における考え方をご覧ください)。たとえば、このコンポーザブルは別のアクティビティを起動します。
class OtherInteractionsActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // get data from savedInstanceState setContent { MaterialTheme { ExampleComposable(data, onButtonClick = { startActivity(Intent(this, MyActivity::class.java)) }) } } } } @Composable fun ExampleComposable(data: DataExample, onButtonClick: () -> Unit) { Button(onClick = onButtonClick) { Text(data.title) } }
事例紹介: BroadcastReceiver
Compose で移行または実装する機能の現実的な例として、CompositionLocal
と副作用を紹介します。ここでは、BroadcastReceiver
をコンポーズ可能な関数から登録する必要があります。
このソリューションでは、LocalContext
(現在のコンテキストを使用するため)、rememberUpdatedState
および DisposableEffect
の副作用を利用します。
@Composable fun SystemBroadcastReceiver( systemAction: String, onSystemEvent: (intent: Intent?) -> Unit ) { // Grab the current context in this part of the UI tree val context = LocalContext.current // Safely use the latest onSystemEvent lambda passed to the function val currentOnSystemEvent by rememberUpdatedState(onSystemEvent) // If either context or systemAction changes, unregister and register again DisposableEffect(context, systemAction) { val intentFilter = IntentFilter(systemAction) val broadcast = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { currentOnSystemEvent(intent) } } context.registerReceiver(broadcast, intentFilter) // When the effect leaves the Composition, remove the callback onDispose { context.unregisterReceiver(broadcast) } } } @Composable fun HomeScreen() { SystemBroadcastReceiver(Intent.ACTION_BATTERY_CHANGED) { batteryStatus -> val isCharging = /* Get from batteryStatus ... */ true /* Do something if the device is charging */ } /* Rest of the HomeScreen */ }