其他考量

雖然從 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) 可以進行相關操作這兩個檢視表都有遷移指南 以及 Compose:

無論 Material 版本為何,在 Compose 中建立新畫面時 採用的設計方式,務必先套用 MaterialTheme 會從 Compose Material 程式庫輸出 UI 的可組合函式。材質 元件 (ButtonText 等) 需要使用 MaterialTheme 沒有的話,使用者的行為將處於未定義狀態。

所有語言 Jetpack Compose 範例 使用以 MaterialTheme 為基礎建構的自訂 Compose 主題。

詳情請參閱 Compose 中的設計系統將 XML 主題遷移至 Compose

如果您在應用程式中使用 Navigation 元件,請參閱 請參閱 Compose 導覽 - 互通性和 詳情請參閱將 Jetpack Navigation 遷移至 Navigation Compose

測試混合的 Compose/View 使用者介面

將應用程式的各個部分遷移至 Compose 後,請務必進行測試來確保 你沒有任何破壞的狀況

當活動或片段使用 Compose 時,您需要使用 createAndroidComposeRule,而不是 ActivityScenarioRule。「createAndroidComposeRule」整合項目 使用 ComposeTestRule 搭配 ActivityScenarioRule,可讓您測試 Compose 同時查看程式碼。

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 程式碼。在此情況下, 可靠資料來源會保留在傳送狀態更新的可組合項中。

舉例來說,數據分析資料庫可能讓您區隔使用者 附加自訂中繼資料來填入人口 (此範例為使用者屬性) 套用至所有後續 Analytics 事件如要將目前使用者的使用者類型連接到數據分析程式庫,請使用 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 可組合函式的 ComposeViewTextView 必須顯示 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 元素通常對於自己所在位置有一定瞭解:例如在 ActivityDialogFragment 中或在另一個 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 則無需變更。

  • 對父項而言,巢狀 ImageWithEnabledOverlayControlPanelWithToggle 的深度並不重要。這些子項可以是動畫改變、調換內容,或是將內容傳遞給其他子項。

此模式被稱為 反向控制,詳情請參閱 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 說明文件。