VIew から Compose への移行は純粋に UI に関連しています。ただし、安全かつ段階的な移行を行うためにはさまざまな考慮事項があります。このページでは、View ベースのアプリを Compose に移行する際の考慮事項について説明します。
アプリのテーマを移行する
マテリアル デザインは、Android アプリのテーマ設定に推奨されるデザイン システムです。
View ベースのアプリの場合、マテリアルには次の 3 つのバージョンが用意されています。
- AppCompat ライブラリを使用したマテリアル デザイン 1(
Theme.AppCompat.*
) - MDC-Android ライブラリを使用したマテリアル デザイン 2(
Theme.MaterialComponents.*
) - MDC-Android ライブラリを使用したマテリアル デザイン 3(
Theme.Material3.*
)
Compose アプリの場合、マテリアルには次の 2 つのバージョンが用意されています。
- Compose マテリアル ライブラリを使用したマテリアル デザイン 2(
androidx.compose.material.MaterialTheme
) - Compose マテリアル 3 ライブラリを使用したマテリアル デザイン 3(
androidx.compose.material3.MaterialTheme
)
アプリのデザイン システムで最新バージョン(マテリアル 3)を使用する必要がある場合は、そうすることをおすすめします。View と Compose の両方の移行ガイドが用意されています。
Compose で新しい画面を作成するときは、使用しているマテリアル デザインのバージョンに関係なく、Compose Material ライブラリから UI を出力するコンポーザブルの前に MaterialTheme
を適用してください。マテリアル コンポーネント(Button
、Text
など)は MaterialTheme
が設定されているかどうかに左右され、設定されていない場合の動作は未定義になります。
Jetpack Compose サンプルはすべて、MaterialTheme
上に構築されたカスタムの Compose テーマを使用しています。
詳細については、Compose でのデザイン システムと XML テーマを Compose に移行するをご覧ください。
ナビゲーション
アプリで Navigation コンポーネントを使用している場合は、Compose を使用したナビゲーション - 相互運用性と Jetpack Navigation を Navigation Compose に移行するをご覧ください。
Compose と View が混在する UI をテストする
アプリの一部を Compose に移行した後は、何も破損していないことをテストで必ず確認する必要があります。
アクティビティまたはフラグメントで Compose が使用される場合、ActivityScenarioRule
を使用する代わりに createAndroidComposeRule
を使用する必要があります。createAndroidComposeRule
は ActivityScenarioRule
を ComposeTestRule
と統合します。これにより、Compose コードと View コードを同時にテストできます。
class MyActivityTest { @Rule @JvmField val composeTestRule = createAndroidComposeRule<MyActivity>() @Test fun testGreeting() { val greeting = InstrumentationRegistry.getInstrumentation() .targetContext.resources.getString(R.string.greeting) composeTestRule.onNodeWithText(greeting).assertIsDisplayed() } }
テストの詳細については、Compose レイアウトのテストをご覧ください。UI テスト フレームワークとの相互運用については、Espresso との相互運用と UiAutomator との相互運用をご覧ください。
Compose を既存のアプリ アーキテクチャと統合する
Unidirectional Data Flow(UDF)アーキテクチャ パターンは、Compose とシームレスに連携します。アプリで Model View Presenter(MVP)のような他のアーキテクチャ パターンを使用している場合は、Compose を導入する前、または導入中に UI の該当部分を UDF に移行することをおすすめします。
Compose で ViewModel
を使用する
Architecture ComponentsViewModel
ライブラリを使用している場合、Compose とその他のライブラリで説明されているように、viewModel()
関数を呼び出すことで、任意のコンポーザブルから ViewModel
にアクセスできます。
Compose を導入する場合、ViewModel
要素は View のライフサイクル スコープに従うため、異なるコンポーザブルで同じ ViewModel
タイプを使用する際は注意が必要です。スコープは、Navigation ライブラリが使用されている場合は、ホスト アクティビティ、フラグメント、ナビゲーション グラフのいずれかになります。
たとえば、コンポーザブルがアクティビティでホストされている場合、viewModel()
は常に同じインスタンスを返しますが、このインスタンスはアクティビティ終了時にのみクリアされます。次の例では、同じ GreetingViewModel
インスタンスがホスト アクティビティ下のすべてのコンポーザブルで再利用されるため、同じユーザー(「user1」)にメッセージが 2 回表示されます。最初に作成された ViewModel
インスタンスは他のコンポーザブルで再利用されます。
class GreetingActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme { Column { GreetingScreen("user1") GreetingScreen("user2") } } } } } @Composable fun GreetingScreen( userId: String, viewModel: GreetingViewModel = viewModel( factory = GreetingViewModelFactory(userId) ) ) { val messageUser by viewModel.message.observeAsState("") Text(messageUser) } class GreetingViewModel(private val userId: String) : ViewModel() { private val _message = MutableLiveData("Hi $userId") val message: LiveData<String> = _message } class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T { return GreetingViewModel(userId) as T } }
また、ナビゲーション グラフは ViewModel
要素のスコープを設定するため、ナビゲーション グラフ内のデスティネーションであるコンポーザブルには ViewModel
の異なるインスタンスがあります。この場合、ViewModel
のスコープはデスティネーションのライフサイクルに設定され、デスティネーションがバックスタックから削除されるとクリアされます。次の例では、ユーザーがプロフィール画面に移動すると、GreetingViewModel
の新しいインスタンスが作成されます。
@Composable fun MyApp() { NavHost(rememberNavController(), startDestination = "profile/{userId}") { /* ... */ composable("profile/{userId}") { backStackEntry -> GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "") } } }
状態の信頼できる情報源
UI の一部に Compose を導入する場合、Compose と View システムコードでデータを共有する必要が出てくる可能性があります。可能であれば、両方のプラットフォームで使用される UDF ベスト プラクティスに即した別のクラスに、共有状態をカプセル化することをおすすめします。たとえば、データ更新を出力するために共有データのストリームを公開する ViewModel
にカプセル化します。
ただし、共有するデータが可変型であったり、UI 要素と密接に結びついていたりすると、そうはいかない場合もあります。その場合、一方のシステムが信頼できる情報源であり、もう一方のシステムとデータの更新を共有する必要があります。一般的な経験則として、信頼できる情報源は、UI 階層のルートに近い方の要素が所有する必要があります。
信頼できる情報源としての Compose
SideEffect
コンポーザブルを使用して、Compose 以外のコードに Compose 状態をパブリッシュします。この場合、信頼できる情報源は、状態の更新を送信するコンポーザブルに保持されます。
たとえば、分析ライブラリを使用すると、カスタム メタデータ(この例では「ユーザー プロパティ」)を後続のすべての分析イベントにアタッチすることで、ユーザー全体をセグメント化できます。現在のユーザーのユーザータイプを分析ライブラリに伝えるには、SideEffect
を使用してその値を更新します。
@Composable fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics { val analytics: FirebaseAnalytics = remember { FirebaseAnalytics() } // On every successful composition, update FirebaseAnalytics with // the userType from the current User, ensuring that future analytics // events have this metadata attached SideEffect { analytics.setUserProperty("userType", user.userType) } return analytics }
詳しくは、Compose における副作用をご覧ください。
信頼できる情報源としての View システム
View システムが状態を所有し、それを Compose と共有する場合は、状態を mutableStateOf
オブジェクトでラップして、Compose がスレッドセーフになるようにすることをおすすめします。このアプローチを使用すると、信頼できる情報源がないため、コンポーズ可能な関数は簡略化されますが、View システムは、可変状態と、その状態を使用する View を更新する必要があります。
次の例では、CustomViewGroup
に TextView
と ComposeView
(内部に TextField
コンポーザブルがある)が含まれています。TextView
は、ユーザーが TextField
に入力した内容を表示する必要があります。
class CustomViewGroup @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : LinearLayout(context, attrs, defStyle) { // Source of truth in the View system as mutableStateOf // to make it thread-safe for Compose private var text by mutableStateOf("") private val textView: TextView init { orientation = VERTICAL textView = TextView(context) val composeView = ComposeView(context).apply { setContent { MaterialTheme { TextField(value = text, onValueChange = { updateState(it) }) } } } addView(textView) addView(composeView) } // Update both the source of truth and the TextView private fun updateState(newValue: String) { text = newValue textView.text = newValue } }
共有 UI の移行
Compose に徐々に移行する場合は、Compose と View システムの両方で共有 UI 要素を使用しなければならないことがあります。たとえば、アプリにカスタム CallToActionButton
コンポーネントがある場合、Compose と View ベースの両方の画面で使用する必要があるかもしれません。
Compose の場合、共有 UI 要素はコンポーザブルとなり、XML を使用してスタイル設定されている要素やカスタムビューの要素であるかに関係なく、アプリ全体で再利用できます。たとえば、カスタム外部リンクの Button
コンポーネントに CallToActionButton
コンポーザブルを作成するとします。
View ベースの画面でこのコンポーザブルを使用するには、AbstractComposeView
から拡張するカスタム ビューラッパーを作成します。そのオーバーライドした Content
コンポーザブルに、以下の例のように Compose テーマでラップして作成したコンポーザブルを配置します。
@Composable fun CallToActionButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, ) { Button( colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.secondary ), onClick = onClick, modifier = modifier, ) { Text(text) } } class CallToActionViewButton @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : AbstractComposeView(context, attrs, defStyle) { var text by mutableStateOf("") var onClick by mutableStateOf({}) @Composable override fun Content() { YourAppTheme { CallToActionButton(text, onClick) } } }
コンポーザブル パラメータは、カスタムビュー内で可変変数になることに注意してください。これにより、カスタムの CallToActionViewButton
ビューは、従来のビューのようにインフレータブルかつ使用可能になります。以下に、ビュー バインディングを使用した例を示します。
class ViewBindingActivity : ComponentActivity() { private lateinit var binding: ActivityExampleBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityExampleBinding.inflate(layoutInflater) setContentView(binding.root) binding.callToAction.apply { text = getString(R.string.greeting) onClick = { /* Do something */ } } } }
カスタム コンポーネントに可変状態が含まれている場合は、状態に関する信頼できる情報源をご覧ください。
プレゼンテーションからの状態の分離を優先する
伝統的に View
はステートフルです。View
は、表示する方法に加えて、表示する内容を記述するフィールドを管理します。View
を Compose に変換する際は、状態ホイスティングで詳しく説明しているように、レンダリングされるデータを分離して単方向データフローを実現するよう努めてください。
たとえば、View
には、「表示される」、「表示されない」、「消失した」のいずれかの状態を記述する visibility
プロパティがあります。これは View
の固有のプロパティです。View
の可視性は他のコードによって変更されることがありますが、現在の可視性の状態を実際に認識するのは View
自身のみです。View
が確実に表示されるようにするロジックはエラーが発生しやすく、多くの場合 View
自身に関連付けられます。
一方、Compose では、Kotlin の条件付きロジックを使用して、まったく異なるコンポーザブルを簡単に表示できます。
@Composable fun MyComposable(showCautionIcon: Boolean) { if (showCautionIcon) { CautionIcon(/* ... */) } }
設計上、CautionIcon
は自身が表示されている理由を知る必要も考慮する必要もなく、visibility
の概念はありません。それは Composition 内にあるかないかのいずれかです。
状態管理とプレゼンテーション ロジックを明確に分離することで、コンテンツを状態の UI への変換として表示する方法をより自由に変更できます。また、必要なときに状態をホイスティングできるので、コンポーザブルがより再利用しやすくなります。これは、状態の所有権の柔軟性がより高いためです。
カプセル化された再利用可能なコンポーネントを活用する
多くの場合、View
要素にはそれが存在する場所(Activity
内、Dialog
内、Fragment
内、または別の View
階層内のどこか)の情報が含まれています。それらはしばしば静的レイアウト ファイルからインフレートされるので、View
の全体的な構造は非常に固定的なものになりがちです。その結果、結合が密になり、View
の変更または再利用が困難になります。
たとえば、カスタム View
は、特定の ID を持つ特定の型の子ビューを持っていると想定し、なんらかのアクションに応じてそのプロパティを直接変更します。これにより、それらの View
要素は互いに密結合されます。カスタム View
は、子を見つけられない場合、クラッシュしたり破損したりすることがあります。また、子はカスタム View
の親がないと再利用できないことがあります。
再利用可能なコンポーザブルを使用する Compose では、こうしたことはそれほど問題になりません。親は状態とコールバックを簡単に指定できるので、再利用可能なコンポーザブルは、それが使用される場所が正確にわからなくても記述できます。
@Composable fun AScreen() { var isEnabled by rememberSaveable { mutableStateOf(false) } Column { ImageWithEnabledOverlay(isEnabled) ControlPanelWithToggle( isEnabled = isEnabled, onEnabledChanged = { isEnabled = it } ) } }
上記の例では、3 つの部分すべてがより高度にカプセル化され、結合が疎になっています。
ImageWithEnabledOverlay
が知る必要があるのは、現在のisEnabled
の状態だけです。ControlPanelWithToggle
が存在するかどうか、またそれが制御可能かどうかを知る必要はありません。ControlPanelWithToggle
はImageWithEnabledOverlay
の存在を認識しません。isEnabled
の表示方法は無指定にすることも、1 つまたは複数指定することもできます。ControlPanelWithToggle
は変更の必要がありません。親にとって、
ImageWithEnabledOverlay
またはControlPanelWithToggle
のネストの深さは問題になりません。このような子には、アニメーションの変更、コンテンツのスワップアウト、他の子へのコンテンツの引き渡しなどがあります。
このパターンは「制御の反転」と呼ばれます。詳しくは、CompositionLocal
のドキュメントをご覧ください。
画面サイズの変更処理
レスポンシブな View
レイアウトの主な作成方法の一つは、異なるウィンドウ サイズごとに異なるリソースを用意することです。従来のように、修飾されたリソースを使用して画面レベルのレイアウトを決定することもできますが、Compose では、通常の条件付きロジックを使用してもっと簡単にコード内でレイアウトを完全に変更できます。詳細については、ウィンドウ サイズクラスを使用するをご覧ください。
さらに、アダプティブ UI の作成用に Compose が提供する手法については、各種の画面サイズをサポートするをご覧ください。
View でのネストされたスクロール
(両方向でネストされている)スクロール可能な View 要素とスクロール可能なコンポーザブルの間でネストされたスクロールの相互運用を実現する方法について詳しくは、ネストされたスクロールの相互運用をご覧ください。
RecyclerView
の Compose
RecyclerView
バージョン 1.3.0-alpha02 以降、RecyclerView
のコンポーザブルのパフォーマンスが向上しています。そのメリットを得るために、RecyclerView
はバージョン 1.3.0-alpha02 以降を使用してください。
WindowInsets
と View の相互運用
同じ階層内にビューと Compose コードの両方が画面に含まれている場合は、デフォルトのインセットをオーバーライドする必要が生じることがあります。この場合、どちらがインセットを使用するか、どちらがインセットを無視するかを明示的に指定する必要があります。
たとえば、最外側のレイアウトが Android View レイアウトの場合は、View システムでインセットを使用し、Compose では無視する必要があります。または、最も外側のレイアウトがコンポーザブルの場合は、Compose でインセットを使用し、それに応じて AndroidView
コンポーザブルをパディングする必要があります。
デフォルトでは、各 ComposeView
は WindowInsetsCompat
レベルの使用量ですべてのインセットを使用します。このデフォルトの動作を変更するには、ComposeView.consumeWindowInsets
を false
に設定します。
詳細については、Compose の WindowInsets
のドキュメントをご覧ください。
あなたへのおすすめ
- 注: JavaScript がオフになっている場合はリンクテキストが表示されます
- 絵文字を表示する
- Compose のマテリアル デザイン 2
- Compose のウィンドウ インセット