雖然從 View 遷移至 Compose 純粹與 UI 相關,但為了執行安全及漸進式的遷移作業,需要考量許多事項。本頁面包含將以 View 為基礎的應用程式遷移至 Compose 的部分注意事項。
遷移應用程式主題
Material Design 是為 Android 應用程式設定主題的推薦設計系統。
以 View 為基礎的應用程式有三種 Material 版本:
- Material Design 1 使用 AppCompat 程式庫 (即
Theme.AppCompat.*
) - Material Design 2 使用 MDC-Android 程式庫 (即
Theme.MaterialComponents.*
) - Material Design 3 使用 MDC-Android 程式庫 (即
Theme.Material3.*
)
Compose 應用程式有兩種可用的 Material 版本:
- Material Design 2 使用 Compose Material 程式庫 (即
androidx.compose.material.MaterialTheme
) - Material Design 3 使用 Compose Material 3 程式庫 (即
androidx.compose.material3.MaterialTheme
)
如果能適用應用程式的設計系統,建議您使用最新版 Material 3。我們為 View 和 Compose 都提供了遷移指南:
- 在 View 中從 Material 1 遷移至 Material 2
- 在 View 中從 Material 2 遷移至 Material 3
- 在 Compose 中從 Material 2 遷移至 Material 3
無論使用的 Material Design 版本為何,在 Compose 中建立新畫面時,請務必先套用 MaterialTheme
,再讓任何可組合項從 Compose Material 程式庫發出 UI。Material 元件 (Button
、Text
等) 需要有 MaterialTheme
,如果沒有,這些元件的行為將處於未定義狀態。
所有 Jetpack Compose 範例都會使用以 MaterialTheme
為基礎建構的自訂 Compose 主題。
詳情請參閱「Compose 中的設計系統」和「將 XML 主題遷移至 Compose」。
導覽
如果您在應用程式中使用導覽元件,請參閱「使用 Compose 進行導覽 - 互通性」和「將 Jetpack Navigation 遷移至 Navigation Compose」瞭解詳情。
測試混合的 Compose/View 使用者介面
將應用程式的部分內容遷移至 Compose 後,請務必進行測試,以確保沒有任何內容遭到毀損。
當活動或片段使用 Compose 時,您需要使用 createAndroidComposeRule
,而非 ActivityScenarioRule
。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 與現有的應用程式架構
單向資料流 (UDF) 架構模式可與 Compose 完美搭配運作。如果應用程式改用 Model View Presenter (MVP) 等其他類型的架構模式,建議您在採用 Compose 之前或期間,將該部分 UI 遷移至 UDF。
在 Compose 中使用 ViewModel
如果您使用架構元件 ViewModel
程式庫,可以呼叫 viewModel()
函式,從任何可組合項中存取 ViewModel
,如「Compose 和其他程式庫」一文中所述。
如果採用 Compose,在不同的可組合函式中使用相同的 ViewModel
類型時請務必謹慎,因為 ViewModel
元件會追蹤檢視畫面生命週期的範圍。如果使用導覽資料庫,範圍會限定為代管活動、片段或導覽圖。
舉例來說,如果可組合項在活動中代管,viewModel()
一律會傳回只在活動完成時清除的相同例項。在以下範例中,系統會歡迎同一位使用者 ("user1") 兩次,原因在於代管活動下的所有可組合項重複使用了相同的 GreetingViewModel
例項。第一個建立的 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
,以及內有 TextField
可組合函式的 ComposeView
。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
本身有關。
相較之下,在使用 Kotlin 中有條件的邏輯時,Compose 能輕鬆顯示完全不同的可組合元件:
@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 } ) } }
在上例中,三個部分的狀態都是封裝更嚴密,耦合性更少:
ImageWithEnabledOverlay
只需要知道isEnabled
的目前狀態,不需要知道ControlPanelWithToggle
的存在或控制方式。ControlPanelWithToggle
不知道ImageWithEnabledOverlay
的存在。可能無法顯示isEnabled
,也可能有一個或多個方法將其顯示出來,而ControlPanelWithToggle
則無需變更。對父項而言,巢狀
ImageWithEnabledOverlay
或ControlPanelWithToggle
的深度並不重要。這些子項可以是動畫改變、調換內容,或是將內容傳遞給其他子項。
此模式被稱為 反向控制,詳情請參閱 CompositionLocal
說明文件。
處理螢幕大小變更
建立回應式 View
版面配置的主要方式之一就是為不同大小的視窗提供不同的資源。雖然符合條件的資源仍然適用於確定螢幕級別的版面配置,但使用 Compose 時,在程式碼中使用一般條件邏輯即可更加輕鬆地完全變更版面配置。詳情請參閱「使用視窗大小類別」。
此外,請參閱「支援不同的螢幕大小」,瞭解 Compose 提供的建構自動調整式 UI 的相關技巧。
View 巢狀結構捲動
如果想進一步瞭解如何啟用可在捲動式檢視畫面與捲動式可組合函式之間同時使用的巢狀捲動互通性,讓兩個方向皆使用巢狀結構,請閱讀「巢狀捲動互通性」一節。
RecyclerView
中的 Compose
RecyclerView
中的可組合項自 RecyclerView
1.3.0-alpha02 版起,便一直表現良好。請務必使用 RecyclerView
1.3.0-alpha02 以上版本,才能享有這些好處。
WindowInsets
與 View 的互通性
如果畫面在同一個階層中同時含有 View 和 Compose 程式碼,您可能需要覆寫預設內嵌項目。在這種情況下,您必須明確指出在何者能使用插邊,哪些應忽略插邊。
舉例來說,如果最外層版面配置是 Android View 版面配置,您應在 View 系統中使用內嵌,並忽略 Compose 的內嵌。或者,如果最外層的版面配置是可組合函式,您應在 Compose 中使用內嵌,並據此為 AndroidView
可組合函式填充。
根據預設,每個 ComposeView
都會在 WindowInsetsCompat
消費層級使用所有內嵌項目。如要變更這個預設行為,請將 ComposeView.consumeWindowInsets
設為 false
。
詳情請參閱 Compose 中的 WindowInsets
說明文件。
為您推薦
- 注意:系統會在 JavaScript 關閉時顯示連結文字
- 顯示表情符號
- Compose 中的 Material Design 2
- Compose 中的視窗插邊