其他注意事项

虽然从 View 迁移到 Compose 纯粹是与界面相关的,但为了安全和增量迁移,还有许多事项需要考虑。本页面包含将基于 View 的应用迁移到 Compose 时的一些注意事项。

迁移应用主题

Material Design 是为 Android 应用设置主题的推荐设计系统。

对于基于 View 的应用,有三个版本的 Material 可用

  • 使用 AppCompat 库的 Material Design 1(即 Theme.AppCompat.*
  • 使用 MDC-Android 库的 Material Design 2(即 Theme.MaterialComponents.*
  • 使用 MDC-Android 库的 Material Design 3(即 Theme.Material3.*

对于 Compose 应用,有两个版本的 Material 可用

  • 使用 Compose Material 库的 Material Design 2(即 androidx.compose.material.MaterialTheme
  • 使用 Compose Material 3 库的 Material Design 3(即 androidx.compose.material3.MaterialTheme

如果您的应用设计系统允许,我们建议使用最新版本(Material 3)。Views 和 Compose 都有可用的迁移指南

在 Compose 中创建新屏幕时,无论您使用哪个版本的 Material Design,请确保在任何从 Compose Material 库发出界面的可组合项之前应用 MaterialTheme。Material 组件(ButtonText 等)依赖于 MaterialTheme 的存在,否则它们的行为是未定义的。

所有 Jetpack Compose 示例都使用了在 MaterialTheme 之上构建的自定义 Compose 主题。

请参阅Compose 中的设计系统将 XML 主题迁移到 Compose 以了解更多信息。

如果您在应用中使用 Navigation 组件,请参阅通过 Compose 导航 - 互操作性将 Jetpack Navigation 迁移到 Navigation Compose 以了解更多信息。

测试您的混合 Compose/View 界面

将应用部分内容迁移到 Compose 后,测试至关重要,以确保没有破坏任何功能。

当 activity 或 fragment 使用 Compose 时,您需要使用 createAndroidComposeRule,而不是使用 ActivityScenarioRulecreateAndroidComposeRuleActivityScenarioRuleComposeTestRule 集成,让您可以同时测试 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 布局以了解有关测试的更多信息。有关与界面测试框架的互操作性,请参阅与 Espresso 的互操作性与 UiAutomator 的互操作性

将 Compose 与现有应用架构集成

单向数据流 (UDF) 架构模式与 Compose 无缝协作。如果应用使用其他类型的架构模式(例如 Model View Presenter (MVP)),我们建议您在采用 Compose 之前或同时将该部分的界面迁移到 UDF。

在 Compose 中使用 ViewModel

如果您使用 架构组件 ViewModel 库,可以通过调用 viewModel() 函数从任何可组合项访问 ViewModel,如Compose 和其他库中所述。

采用 Compose 时,请注意不要在不同的可组合项中使用相同的 ViewModel 类型,因为 ViewModel 元素遵循 View 生命周期作用域。如果使用 Navigation 库,作用域将是宿主 activity、fragment 或导航图。

例如,如果可组合项托管在 activity 中,viewModel() 始终返回相同的实例,该实例只在 activity 结束时清除。在以下示例中,相同的用户(“user1”)被问候两次,因为在宿主 activity 下的所有可组合项中都重用了相同的 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 的作用域限定为目标的生命周期,并在目标从返回堆栈中移除时清除。在以下示例中,当用户导航到 Profile 屏幕时,会创建一个新的 GreetingViewModel 实例。

@Composable
fun MyApp() {
    NavHost(rememberNavController(), startDestination = "profile/{userId}") {
        /* ... */
        composable("profile/{userId}") { backStackEntry ->
            GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "")
        }
    }
}

状态单一可信来源

当您在界面的某个部分采用 Compose 时,Compose 和 View 系统代码可能需要共享数据。如果可能,我们建议您将该共享状态封装在另一个遵循两个平台都使用的 UDF 最佳实践的类中;例如,在 ViewModel 中,该 ViewModel 公开共享数据流以发出数据更新。

然而,如果共享数据是可变的或与界面元素紧密绑定,则并非总是可行。在这种情况下,一个系统必须是单一可信来源,并且该系统需要将任何数据更新共享给另一个系统。通常,单一可信来源应由离界面层次结构根部更近的元素拥有。

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 可组合项的 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
    }
}

迁移共享界面

如果您正在逐步迁移到 Compose,您可能需要在 Compose 和 View 系统中都使用共享界面元素。例如,如果您的应用有一个自定义的 CallToActionButton 组件,您可能需要在 Compose 和基于 View 的屏幕中都使用它。

在 Compose 中,共享界面元素成为可组合项,可以在整个应用中重复使用,无论该元素是使用 XML 样式化还是自定义 View。例如,您将为自定义号召性用语 Button 组件创建一个 CallToActionButton 可组合项。

要在基于 View 的屏幕中使用可组合项,请创建一个从 AbstractComposeView 扩展的自定义 View 封装器。在其 overridden 的 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)
        }
    }
}

请注意,可组合项参数成为自定义 View 中的可变变量。这使得自定义 CallToActionViewButton View 能够像传统 View 一样被膨胀和使用。请参阅下面通过 View Binding 实现的示例

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 中,要么不在。

通过清晰地分离状态管理和呈现逻辑,您可以更自由地更改内容显示方式,将状态转换为界面。在需要时能够提升状态也使得可组合项更具可重用性,因为状态所有权更加灵活。

推广封装和可重用组件

View 元素通常对其所处位置有所了解:在 ActivityDialogFragment 内部或另一个 View 层次结构中的某个位置。由于它们通常从静态布局文件膨胀而来,因此 View 的整体结构往往非常僵化。这导致更紧密的耦合,并使得 View 难以更改或重用。

例如,自定义 View 可能会假定它具有某种类型且具有特定 ID 的子 View,并直接响应某个操作来更改其属性。这使得这些 View 元素紧密耦合:如果找不到子 View,自定义 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 为构建自适应界面提供的技术。

View 中的嵌套滚动

有关如何在可滚动 View 元素和可滚动可组合项之间启用嵌套滚动互操作性的更多信息,请阅读嵌套滚动互操作性

RecyclerView 中的 Compose

RecyclerView 1.3.0-alpha02 版本起,RecyclerView 中的可组合项性能良好。请确保您的 RecyclerView 版本至少为 1.3.0-alpha02,才能看到这些优势。

WindowInsets 与 View 的互操作性

当您的屏幕在同一层次结构中同时包含 View 和 Compose 代码时,您可能需要覆盖默认的边衬区。在这种情况下,您需要明确指出哪个应该消耗边衬区,哪个应该忽略它们。

例如,如果您的最外层布局是 Android View 布局,您应该在 View 系统中消耗边衬区,并让 Compose 忽略它们。或者,如果您的最外层布局是可组合项,您应该在 Compose 中消耗边衬区,并相应地填充 AndroidView 可组合项。

默认情况下,每个 ComposeView 都会在 WindowInsetsCompat 的消耗级别消耗所有边衬区。要更改此默认行为,请将 ComposeView.consumeWindowInsets 设置为 false

如需了解更多信息,请阅读Compose 中的 WindowInsets 文档。