将 Compose 与现有界面集成

如果您的应用界面是基于 View 系统,您可能不想一次全部重写整个界面。本页将帮助您向现有界面中添加新的 Compose 元素。

迁移共享界面

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

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

为了在基于 View 的屏幕中使用可组合项,您需要创建一个从 AbstractComposeView 扩展的自定义视图封装容器。在该容器被替换的 Content 可组合项中,将您创建的可组合项封装在 Compose 主题中,如下例所示:

@Composable
fun CallToActionButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            backgroundColor = MaterialTheme.colors.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<String>("")
    var onClick by mutableStateOf<() -> Unit>({})

    @Composable
    override fun Content() {
        YourAppTheme {
            CallToActionButton(text, onClick)
        }
    }
}

请注意,可组合项参数在自定义视图中会成为可变变量。这会使自定义 CallToActionViewButton 视图在使用视图绑定等功能时变得可膨胀且可以使用,像传统视图一样。请参见下面的示例:

class ExampleActivity : Activity() {

    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.something)
            onClick = { /* Do something */ }
        }
    }
}

如果自定义组件包含可变状态,请参阅状态可信来源部分。

主题

建议您按照 Material Design 的准则,采用适用于 Android 的 Material Design 组件(MDC) 库来设计 Android 应用的主题。如 Compose 主题文档中所述,Compose 使用 MaterialTheme 可组合项实现这些概念。

在 Compose 中创建新屏幕时,您应确保在使用任何从 Material 组件库中发出界面的可组合项之前使用 MaterialTheme。Material 组件(ButtonText 等)依赖于现有的 MaterialTheme,如果没有 MaterialTheme,这些组件的行为将处于未定义状态。

所有 Jetpack Compose 示例都使用基于 MaterialTheme 构建的自定义 Compose 主题。

多个可信来源

现有应用可能包含大量视图主题和样式。在现有应用中引入 Compose 时,您需要迁移主题才能对任意 Compose 屏幕使用 MaterialTheme。这意味着应用的主题将会有 2 个可信来源:基于 View 的主题,以及 Compose 主题。样式上的任何更改都需要在多处实施。

如果您计划将应用完全迁移到 Compose,最终还是要针对现有主题创建 Compose 版本。问题在于,在开发过程中创建 Compose 主题的时间越早,开发中需要进行的维护就越多。

MDC Compose 主题适配器

如果您在 Android 应用中使用 MDC 库,则可借助 MDC Compose 主题适配器库,在可组合项中轻松地重复使用基于 View 的现有主题的颜色排版形状主题:

import com.google.android.material.composethemeadapter.MdcTheme

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            MdcTheme {
                // Colors, typography, and shape have been read from the
                // View-based theme used in this Activity
                ExampleComposable(/*...*/)
            }
        }
    }
}

如需了解详情,请参阅 MDC 库文档

AppCompat Compose 主题适配器

借助 AppCompat Compose 主题适配器库,您可以在 Jetpack Compose 中轻松地重复使用 AppCompat XML 主题。它会使用上下文主题的颜色排版值创建 MaterialTheme

import com.google.accompanist.appcompattheme.AppCompatTheme

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            AppCompatTheme {
                // Colors, typography, and shape have been read from the
                // View-based theme used in this Activity
                ExampleComposable(/*...*/)
            }
        }
    }
}

默认组件样式

MDC 和 AppCompat Compose 主题适配器库不会读取任何由主题定义的默认微件样式。这是因为 Compose 没有默认可组合项的概念。

如需详细了解组件样式自定义设计系统,请参阅主题文档

Compose 中的主题叠加层

将基于 View 的屏幕迁移到 Compose 时,请注意 android:theme 属性的用法。您可能需要在 Compose 界面树的相应部分添加新的 MaterialTheme

如需了解详情,请参阅主题指南

WindowInsets 和 IME 动画

您可以使用 accompanist-insets 库处理 WindowInsets,该库提供用于在布局中处理它们的可组合项和修饰符,以及对 IME 动画的支持。

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            MaterialTheme {
                ProvideWindowInsets {
                    MyScreen()
                }
            }
        }
    }
}

@Composable
fun MyScreen() {
    Box {
        FloatingActionButton(
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(16.dp) // normal 16dp of padding for FABs
                .navigationBarsPadding(), // Move it out from under the nav bar
            onClick = { }
        ) {
            Icon( /* ... */)
        }
    }
}

展示界面元素上下滚动以便为键盘腾出空间的动画

图 2. 采用 accompanist-insets 库的 IME 动画。

如需了解详情,请参阅 accompanists-insets 库文档

处理屏幕尺寸的变化

若应用根据屏幕尺寸使用不同的 XML 布局,迁移应用时请使用 BoxWithConstraints 可组合项了解可组合项能占用的最小尺寸和最大尺寸。

@Composable
fun MyComposable() {
    BoxWithConstraints {
        if (minWidth < 480.dp) {
            /* Show grid with 4 columns */
        } else if (minWidth < 720.dp) {
            /* Show grid with 8 columns */
        } else {
            /* Show grid with 12 columns */
        }
    }
}

使用 View 实现嵌套滚动

遗憾的是,View 系统和 Jetpack Compose 之间的嵌套滚动尚不可用。您可以在此问题跟踪器 bug 中查看进度。

Compose 在 RecyclerView 中的运用

Jetpack Compose 将 DisposeOnDetachedFromWindow 用作默认 ViewCompositionStrategy。这意味着,每当视图与窗口分离时,系统都会处置组合

ComposeView 用作 RecyclerView ViewHolder 的一部分时,默认策略的效率并不高,因为在 RecyclerView 与窗口分离之前,底层组合实例将一直保留在内存中。最好在 RecyclerView 不再需要 ComposeView 时处置底层组合。

借助 disposeComposition 函数,您可以手动处置 ComposeView 的底层组合。您可以在相应视图被回收时调用此函数,如下所示:

import androidx.compose.ui.platform.ComposeView

class MyComposeAdapter : RecyclerView.Adapter<MyComposeViewHolder>() {

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int,
    ): MyComposeViewHolder {
        return MyComposeViewHolder(ComposeView(parent.context))
    }

    override fun onViewRecycled(holder: MyComposeViewHolder) {
        // Dispose of the underlying Composition of the ComposeView
        // when RecyclerView has recycled this ViewHolder
        holder.composeView.disposeComposition()
    }

    /* Other methods */
}

class MyComposeViewHolder(
    val composeView: ComposeView
) : RecyclerView.ViewHolder(composeView) {
    /* ... */
}

正如 Interoperability API 指南中的“ComposeView 的 ViewCompositionStrategy”部分所述,为了使 Compose ViewHolder 能够适用于所有场景,有必要使用 DisposeOnViewTreeLifecycleDestroyed 策略。

import androidx.compose.ui.platform.ViewCompositionStrategy

class MyComposeViewHolder(
    val composeView: ComposeView
) : RecyclerView.ViewHolder(composeView) {

    init {
        composeView.setViewCompositionStrategy(
            ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
        )
    }

    fun bind(input: String) {
        composeView.setContent {
            MdcTheme {
                Text(input)
            }
        }
    }
}

如需了解 RecyclerView 中所用的 ComposeView 的实际运用,请查看 Sunflower 应用的 compose_recyclerview 分支