如果你正在用 Jetpack Compose 做安卓开发,大概率会遇到这样一个场景:界面层级越来越深,Column套Row,Row再套Box,最后嵌套了五六层,改一个间距要翻半天代码,预览也卡得不行。这时候很多人会想,要是 Compose 里也有类似传统 View 系统里ConstraintLayout那样的约束布局就好了。
事实上,Compose 官方确实提供了ConstraintLayout,而且它不是简单把老布局搬运过来,而是用一套声明式 DSL 重新实现了约束逻辑。这篇文章会从“什么时候真的需要它”讲起,带你搞懂它的核心概念、写一个完整示例,再把最容易踩的坑和使用建议一并梳理清楚。
先说结论:在 Compose 中,ConstraintLayout 不是默认首选,但在复杂页面、自定义层级、动态对齐和性能敏感场景下,它依然是不可替代的布局工具。
1. 这篇文章真正要解决的问题
很多人刚学 Jetpack Compose 时,容易被“声明式 UI 不用再写布局文件”这句话误导,觉得布局问题已经被Column、Row、Box三个组件解决了。实际写项目后才发现,界面一旦复杂起来,线性布局嵌套的代价非常高。
先说性能。Compose 的布局阶段需要测量和放置每个子组件。嵌套层级越深,测量次数就越多。尤其是当你用Column包Row,再用Row包多个Box去模拟“相对定位”时,Compose 需要一层层递归测量,才能计算出最终位置。在列表项这种高频 recomposition 的场景里,这种写法很容易造成掉帧。
再说代码可维护性。嵌套布局的缩进会让人崩溃,尤其是你发现自己为了把一个Text放到父容器底部,不得不在外面再包一层Box的时候。这里真正容易踩坑的地方是:很多新手以为 Compose 里没有 ConstraintLayout,于是拼命用Box的Modifier.align来模拟约束,结果代码读起来非常痛苦,别人接手时根本不知道你想表达什么布局意图。
ConstraintLayout 解决的就是这三件事:
- 用同级约束替代多层嵌套,降低测量深度。
- 用明确的
constrainAs描述组件相对位置,代码即布局图。 - 提供 guideline、barrier、chain 等高级能力,适配复杂设计稿。
这篇文章适合以下读者:
- 已经掌握
Column、Row、Box基础用法,想进阶布局能力的安卓开发者。 - 正在从传统 View 体系迁移到 Compose,但被嵌套布局困扰的开发者。
- 想优化列表项布局性能,或者想让自己写的 Compose 代码更具可读性的开发者。
读完你会得到:一套完整可运行的 ConstraintLayout 示例、对核心 API 的理解、以及实际项目中如何选型布局方案的建议。
2. 约束布局的核心概念与适用场景
2.1 什么是约束布局
在传统 View 系统中,ConstraintLayout是一个允许你用“约束”来定义子视图之间相对位置的布局容器。所谓约束,可以理解为一条“规则”:A 的左边对齐 B 的右边,C 的底部对齐父容器的底部,D 的宽度跟随 A 和 B 的边界。
Compose 中的ConstraintLayout设计目标一致,但实现方式完全不同。传统 View 通过 XML 属性来描述约束,Compose 则通过 Kotlin DSL 在代码中创建约束。
一个最简单的约束关系是这样的:
// 伪代码,演示约束关系 ConstraintLayout { val title = createRef() val content = createRef() Text( text = "标题", modifier = Modifier.constrainAs(title) { top.linkTo(parent.top, margin = 16.dp) start.linkTo(parent.start, margin = 16.dp) } ) Text( text = "内容区域", modifier = Modifier.constrainAs(content) { top.linkTo(title.bottom, margin = 8.dp) start.linkTo(parent.start, margin = 16.dp) } ) }这里有几个需要解释的概念:
createRef():为每个子组件创建一个“约束引用”,它并不代表组件本身,只是一个用于描述位置关系的锚点。constrainAs:把某个 Composable 与一个约束引用绑定,并在花括号内声明它的位置规则。linkTo:指定这个组件的某条边(top、bottom、start、end)与另一个约束目标的对应边建立关联。
2.2 为什么 Compose 还需要 ConstraintLayout
既然Column、Row、Box已经能组合出任何布局,为什么还要引入一套新 API?
答案就在“组合”这两个字上。Column和Row天生适合线性排列,Box适合堆叠,但你若要表达“A 在 B 和 C 中间,且宽度由两者的位置决定”这种复杂关系,用线性布局就需要额外嵌套和测量辅助。
ConstraintLayout 的核心价值不是“做布局”,而是“降低布局复杂度”。它把多个组件放在同一个层级上描述关系,而不是通过物理嵌套实现关系。
一个典型的场景是底部操作栏:左侧是取消按钮,右侧是确认按钮,中间是标题,标题要居中但又不完全基于父容器中心点,而是要基于左右按钮之间的空间中心。这种需求在 XML 时代用ConstraintLayout非常自然,在 Compose 中用Row+weight也能做,但如果左右按钮宽度不固定,Row方案就会变得棘手。ConstraintLayout 可以用两组约束精确表达。
2.3 核心 API 一览
从官方 API 来看,Compose 的ConstraintLayout核心包括:
| 名称 | 作用 | 对应传统 View 中的概念 |
|---|---|---|
createRef() | 创建一个组件的引用锚点 | @+id/xxx |
constrainAs() | 为组件声明约束规则 | layout_constraintXxx |
createGuidelineFrom...() | 创建百分比或固定偏移的辅助线 | Guideline |
createBarrier() | 创建屏障,使多个组件的边界对齐 | Barrier |
Chain | 将多个组件连成一条链,实现分布和权重 | layout_constraintChainStyle |
本篇文章会重点讲createRef()、constrainAs()和createGuidelineFrom...(),因为这是日常开发中使用频率最高的三个。
2.4 和传统 View 系统约束布局的关键区别
很多人从 View 切到 Compose 时,会下意识寻找 XML 中“属性”的对应物。但 Compose 的 ConstraintLayout 有两个显著不同:
第一,约束是内联代码,不是独立 XML 文件。这意味着约束逻辑与 UI 代码在同一个文件,看到组件就能看到它的位置规则,不需要在两个文件之间跳转。
第二,约束不依赖 id 字符串。传统 View 里要通过@+id/xxx来引用组件,拼写错误只有在运行或构建时才能发现。Compose 通过createRef()创建的引用是类型安全的,如果引用了不存在的引用,编译期就会报错。
这两点决定了 Compose 的 ConstraintLayout 更适合现代安卓开发工作流,但也要求开发者有更强的代码组织能力。
3. 环境准备与前置条件
在开始写代码之前,先确认你的工程环境。执行ConstraintLayout示例至少需要满足以下条件:
- Android Studio 版本建议使用当前稳定版本,本文示例以较新的 Android Studio 环境为准。
- 项目使用 Kotlin DSL 或 Groovy DSL 均可,不影响本文示例。
- 已启用 Jetpack Compose 支持。
在build.gradle.kts(模块级别)中,需要添加 Compose ConstraintLayout 依赖:
dependencies { implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1") }如果你使用的是build.gradle(Groovy 语法),对应写法是:
dependencies { implementation 'androidx.constraintlayout:constraintlayout-compose:1.0.1' }这里要特别提醒:constraintlayout-compose和传统 View 时代的androidx.constraintlayout:constraintlayout是两个完全不同的库。前者专门为 Compose 编写,包名是androidx.constraintlayout.compose;后者是面向 View 系统的,不能在 Compose 中直接使用。很多从老项目迁移过来的同学第一次踩坑就在这。
添加依赖后,需要同步 Gradle。然后可以在一个 Composable 函数中使用:
import androidx.constraintlayout.compose.ConstraintLayout环境准备就绪后,我们来拆解核心流程。
4. 约束布局的核心流程拆解
这一节我会用一个常见的“卡片式布局”来演示 ConstraintLayout 的完整使用过程。假设我们要实现这样一个界面:左上角是用户头像,头像右侧是用户名,用户名下方是发布时间,卡片右下角有一个“关注”按钮,头像左下角有一个在线状态绿点。
在传统 View 时代,这个布局要么用RelativeLayout,要么用ConstraintLayout,层级大概 2 到 3 层。如果纯用 Compose 基础组件,可能需要Row套Column,再嵌套Box,大概 4 层以上。我们用 ConstraintLayout 来写,只需要一层。
4.1 第一步:创建约束引用
在 Composable 的ConstraintLayout块内,首先为每个需要定位的组件创建引用:
ConstraintLayout( modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { val avatarRef = createRef() val nameRef = createRef() val timeRef = createRef() val statusDotRef = createRef() val followButtonRef = createRef() }这一步的作用是声明“布局中有哪些需要约束的元素”。注意,createRef()必须在ConstraintLayout的作用域内调用。
4.2 第二步:为组件绑定约束
接下来,在每个子组件上通过Modifier.constrainAs来声明约束关系:
Image( bitmap = avatarBitmap, contentDescription = "用户头像", modifier = Modifier .size(48.dp) .constrainAs(avatarRef) { top.linkTo(parent.top) start.linkTo(parent.start) } ) Text( text = "Android 开发技术栈", modifier = Modifier.constrainAs(nameRef) { top.linkTo(avatarRef.top) start.linkTo(avatarRef.end, margin = 12.dp) end.linkTo(followButtonRef.start, margin = 8.dp) width = Dimension.wrapContent } )这段代码的含义是:
- 头像位于父容器左上角。
- 用户名顶部与头像顶部对齐,左侧在头像右侧 12dp 处。
- 用户名的右侧要保持在关注按钮左侧 8dp 之前,避免文字过长顶到按钮。
这里需要解释Dimension.wrapContent。在 ConstraintLayout 中,组件的宽度默认遵循它的内容尺寸,但当你同时约束了start和end时,组件会被拉伸。如果你不想让它被拉伸,就显式指定width = Dimension.wrapContent。
这就是新手最常见的误区:给一个Text同时设置了start.linkTo和end.linkTo,结果发现文字被拉长了,左右都贴到边界。原因就是没有手动设置Dimension.wrapContent。
4.3 第三步:处理相对定位和辅助线
继续完善卡片布局。发布时间在用户名下方,关注按钮需要垂直居中于头像位置,在线状态绿点要贴在头像右下角边缘。
Text( text = "5 分钟前发布", style = MaterialTheme.typography.bodySmall, color = Color.Gray, modifier = Modifier.constrainAs(timeRef) { top.linkTo(nameRef.bottom, margin = 4.dp) start.linkTo(nameRef.start) } ) Button( onClick = { /* 关注逻辑 */ }, modifier = Modifier.constrainAs(followButtonRef) { centerVerticallyTo(avatarRef) end.linkTo(parent.end) } ) { Text(text = "关注") } Box( modifier = Modifier .size(12.dp) .background(Color(0xFF4CAF50), CircleShape) .constrainAs(statusDotRef) { top.linkTo(avatarRef.top, margin = 34.dp) start.linkTo(avatarRef.start, margin = 34.dp) } )这里用到了几个新 API:
centerVerticallyTo(avatarRef):让当前组件在垂直方向上与另一个引用居中。margin参数:在linkTo中传入,表示边距。
在线状态绿点的处理方式比较粗暴,直接用固定 margin 把它挪到右下角。实际项目中还可以用createRef()的bottom和end链来实现,这里不赘述。
5. 完整示例代码实现
为了让读者能直接运行,我写一个完整的 Composable 示例。这段代码可以在一个空白的 Compose 工程中直接运行,展示一个包含头像、标题、描述、进度条和操作按钮的复杂卡片布局。
示例场景:一个视频课程卡片。卡片顶部是课程封面缩略图,左侧是课程标题和讲师名,右侧是收藏按钮,底部有一行小字显示学习进度,进度条位于卡片左下角到右下角。
package com.example.composeconstraintlayout import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme { VideoCardExample() } } } } @Composable fun VideoCardExample() { ConstraintLayout( modifier = Modifier .fillMaxSize() .padding(16.dp) ) { val coverRef = createRef() val titleRef = createRef() val teacherRef = createRef() val favoriteRef = createRef() val progressRef = createRef() val progressTextRef = createRef() val statusDotRef = createRef() // 封面占位块 Box( modifier = Modifier .size(width = 80.dp, height = 100.dp) .clip(RoundedCornerShape(8.dp)) .background(Color(0xFFB39DDB)) .constrainAs(coverRef) { top.linkTo(parent.top) start.linkTo(parent.start) } ) // 课程标题 Text( text = "Jetpack Compose 声明式 UI 开发实战", style = MaterialTheme.typography.titleMedium, modifier = Modifier.constrainAs(titleRef) { top.linkTo(coverRef.top) start.linkTo(coverRef.end, margin = 12.dp) end.linkTo(favoriteRef.start, margin = 8.dp) width = Dimension.wrapContent } ) // 讲师名 Text( text = "讲师:陈老师", style = MaterialTheme.typography.bodyMedium, color = Color.Gray, modifier = Modifier.constrainAs(teacherRef) { top.linkTo(titleRef.bottom, margin = 6.dp) start.linkTo(titleRef.start) } ) // 收藏按钮 Button( onClick = { /* 收藏逻辑 */ }, modifier = Modifier.constrainAs(favoriteRef) { centerVerticallyTo(coverRef) end.linkTo(parent.end) } ) { Text(text = "收藏") } // 在线状态绿点 Box( modifier = Modifier .size(14.dp) .background(Color(0xFF4CAF50), CircleShape) .constrainAs(statusDotRef) { top.linkTo(coverRef.top, margin = 78.dp) start.linkTo(coverRef.start, margin = 68.dp) } ) // 进度条 LinearProgressIndicator( progress = { 0.6f }, modifier = Modifier.constrainAs(progressRef) { top.linkTo(coverRef.bottom, margin = 16.dp) start.linkTo(parent.start) end.linkTo(parent.end) width = Dimension.fillToConstraints } ) // 进度文字 Text( text = "60%", style = MaterialTheme.typography.bodySmall, color = Color.Gray, modifier = Modifier.constrainAs(progressTextRef) { top.linkTo(progressRef.bottom, margin = 4.dp) end.linkTo(parent.end) } ) } }这段代码演示了几个关键点:
start.linkTo(coverRef.end, margin = 12.dp)可以代替嵌套Row实现水平排列。centerVerticallyTo(coverRef)让按钮相对封面垂直居中。width = Dimension.fillToConstraints让进度条从父容器起点延伸到终点。这在Column里需要用fillMaxWidth,但在约束布局里直接用约束规则完成。- 在线状态绿点使用固定 margin 的方式定位到封面右下角,这是一种快捷写法,虽然不优雅但直接有效。
5.1 如何运行和验证
在 Android Studio 中新建一个空 Compose 工程,将依赖添加完毕后,把上述代码放入MainActivity.kt,运行到模拟器或真机。预期看到的效果是:
- 封面块显示紫色背景,位于卡片左侧。
- 标题和讲师名位于封面右侧,标题不会被按钮挤压变形。
- 收藏按钮位于卡片右上角,垂直方向和封面中心对齐。
- 进度条横跨整个卡片宽度,进度文字在右下角。
如果界面与预期不符,优先检查每个组件是否绑定了正确的constrainAs引用。
6. 运行结果与效果验证
上面示例的运行结果在视觉上非常直观,但怎么从代码层面判断你的约束设置是否正确?我提供一个验证清单。
6.1 预期输出
运行后,界面应该呈现:
- 紫色封面块位于屏幕左上角。
- 标题文字在封面右侧,并且顶部对齐。
- 讲师名在标题下方。
- 收藏按钮位于屏幕右侧,垂直方向与封面中心对齐。
- 绿色小圆点覆盖在封面右下角。
- 进度条位于封面下方,宽度充满可用空间。
- 60% 文本显示在进度条下方右侧。
6.2 判断是否成功的方法
- 如果所有组件都在预期位置,说明约束关系正确。
- 如果某个组件没有出现,检查它是否缺少
constrainAs。 - 如果出现了但位置错乱,优先检查
linkTo的起始点是否拼错。 - 如果布局正常但组件被拉伸,查看是否设置了
Dimension.wrapContent。
6.3 运行失败第一步看哪里
如果启动崩溃,先在 Logcat 中搜索ConstraintLayout相关异常。最常见的崩溃原因是使用了一个引用来约束它自己,例如:
// 错误示例:引用自身 val boxRef = createRef() Box(modifier = Modifier.constrainAs(boxRef) { start.linkTo(boxRef.start) })这种自己关联自己的约束会直接抛出异常。构建时不会报错,运行时才会。搜索Circular constraint可以快速定位。
7. 约束布局常见问题与排查方法
在实际开发中,下面几个问题出现的频率非常高。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 组件没有出现在预期位置 | 没有绑定constrainAs | 检查组件修饰符中是否有constrainAs | 为组件绑定对应引用并声明约束 |
| 文字被拉长变形 | 同时约束了start和end | 检查宽度默认行为 | 显式添加width = Dimension.wrapContent |
| 运行崩溃:循环约束 | 引用linkTo自身 | 查看 Logcat 中的 Circular constraint | 检查引用变量是否错误指向自身 |
| 组件重叠 | 约束冲突或使用了固定 margin | 确认两组约束是否指向同一位置 | 使用辅助线或 barrier 明确位置规则 |
| 预览显示空白 | 依赖未正确添加或未同步 | 查看 Gradle 同步状态 | 重新同步项目并确认依赖存在 |
| 引用不存在的对象 | 约束变量写错名 | 编译器会提示,运行前修复 | 使用createRef()创建的变量名 |
| 使用传统 ConstraintLayout 依赖导致编译失败 | 导入了 View 系统的类 | 检查 import 语句 | 使用androidx.constraintlayout.compose.ConstraintLayout |
7.1 最容易忽略的细节
在 Compose 中,约束布局的很多 API 设计是“宽进严出”。写起来简单,但如果你不明白默认行为,很容易写出反直觉的布局。
我建议至少记住这三条经验:
constrainAs不是Modifier的普通扩展,它在ConstraintLayoutScope中才有意义,不要在其他作用域中使用。createRef()每个组件需要独立调用,不能在循环中复用同一个引用,否则只有最后一个组件生效。- 约束是可以叠加的,组件可以同时拥有
top、bottom、start、end四条边的约束,但越复杂越难维护,能用简单方式解决就不要上复杂规则。
8. 最佳实践与工程建议
8.1 不要为了用而用
ConstraintLayout 虽然强大,但它并不是 Compose 布局的银弹。一个页面如果只有三四个元素、线性排列清晰,直接用Column或Row反而更简洁。
我的建议是:
- 组件少于 5 个,且关系简单,优先使用
Column、Row、Box。 - 需要多个组件互相参照位置,且有交叉对齐关系,使用 ConstraintLayout。
- 单个列表项的布局复杂,使用 ConstraintLayout 提升测量性能。
- 有动态位置变化的需求,例如某个元素要根据另一个元素是否显示来决定位置,使用 ConstraintLayout 更自然。
8.2 保持约束粒度适中
不要在同一个ConstraintLayout里塞几十个组件。约束系统擅长处理关系,但关系过多时理解成本同样很高。如果页面很复杂,可以拆分成多个子布局,每个子布局内部使用约束,再通过基础组件组合。
经验法则是:一个ConstraintLayout的约束引用数量控制在 10 个以内。超过这个数量,代码的可读性会明显下降。
8.3 善用指南线和屏障
createGuidelineFromTop()和createGuidelineFromStart()可以创建不依赖具体组件的辅助线。比如你想让某个组件位于屏幕高度的 1/3 处,直接使用辅助线比计算固定偏移更稳妥。
拦截屏障createBarrier()则适合处理“多个动态高度组件”的对齐需求。比如一个Text在后面一个Text的下面,但前面的Text可能换行,用top.linkTo(text2.bottom)是不准确的。你应该:
val barrier = createEndBarrier(titleRef, teacherRef) Text( text = "描述信息", modifier = Modifier.constrainAs(descRef) { top.linkTo(barrier) } )这样,无论titleRef和teacherRef哪个更高,描述信息都会跟在较矮元素的边界之后。
8.4 在大型项目中使用 ConstraintLayout 的建议
在团队协作场景中,建议为 ConstraintLayout 的约束规则写简短注释。虽然代码即布局,但多个linkTo堆在一起时,读者很难一眼看出设计意图。输出一段类似下面的注释,能显著提高可维护性:
// 封面在左上角,标题在右,按钮在最右侧,进度条横跨底部8.5 性能考量
Compose 官方文档指出,在测量阶段,ConstraintLayout 的测量性能优于同级别嵌套的线性布局。因为子组件可以并行测量,且不需要多层递归。但这不意味着你可以在一个页面中滥用它。
实际项目中,列表项的item布局是最值得优化的场景。一个常见做法是:在LazyColumn的 item 中,如果布局关系复杂,使用 ConstraintLayout 而不是多层嵌套。
最后提醒一点:ConstraintLayout 的Modifier.constrainAs内部会记录约束关系,这些对象在重组时会被复用。你不需要为每个引用创建状态,保持引用赋值在ConstraintLayout块内即可。
9. 常见问题再补三个实战细节
9.1 如何让组件在父容器中水平居中
如果组件需要水平居中且垂直位置跟随另一个组件,可以这样写:
Modifier.constrainAs(ref) { start.linkTo(parent.start) end.linkTo(parent.end) width = Dimension.wrapContent top.linkTo(otherRef.bottom, margin = 8.dp) }核心是同时约束start和end,并把宽度设为wrapContent。如果不设置宽度,组件会被拉满整个宽度,视觉上变成全宽,而不是居中。
9.2Dimension.fillToConstraints和Dimension.wrapContent的区别
这两个属性是初学者最容易搞混的。
Dimension.wrapContent:组件尺寸按内容决定。适合文本、按钮。Dimension.fillToConstraints:组件尺寸根据约束边界拉伸。适合进度条、分割线、需要撑满空间的块。Dimension.preferredWrapContent:优先按内容尺寸,但如果有约束限制,可以压缩。适用于文本过长时自动换行并限制在约束内。
9.3constrainAs和Modifier顺序的影响
Modifier是有顺序语义的。如果你的代码是:
Modifier .padding(8.dp) .constrainAs(ref) { ... }和:
Modifier .constrainAs(ref) { ... } .padding(8.dp)效果完全不同。前者是先让组件内部有 8dp 内边距,然后执行约束布局;后者是先确定约束位置,再给组件加 8dp 内边距,视觉上会影响 constraining 的位置计算。
实际开发中建议把constrainAs放在Modifier链的最前面,这样约束规则优先确定位置,后续修饰符基于位置再添加边距、背景等效果。
10. 结语:什么时候真的该上约束布局
回到开头的问题:ConstraintLayout 在 Compose 中到底是不是必需品?
如果你只写简单页面,Column、Row、Box足够解决 90% 的问题。但当你开始接触真实项目,遇到列表项多样化、布局关系交叉、或者需要精准对齐的设计稿时,ConstraintLayout 会成为你工具箱里最有力的那一把扳手。
它的价值体现在三点:
- 用约束表达关系,而不是用嵌套模拟关系。
- 减少无效测量层级,提升复杂布局性能。
- 代码结构更贴近设计稿,后期改间距、调位置更方便。
如果你正在学习 Jetpack Compose,建议你动手把上面的示例代码运行一遍,然后尝试改一改约束:比如把头像挪到右边、把进度条宽度改成 80%、把收藏按钮改成图标按钮。改完你就能理解约束系统的边界和优势了。
下一步可以继续研究createBarrier()和Chain,这两块在处理复杂列表项时非常实用。也可以看看官方文档中的MotionLayout支持,它是基于约束系统实现动画的高级方案。先掌握本文的 API,再逐步往高级功能走,这条路是最稳妥的。