面向 Jetpack Compose 开发者的 Flutter
学习在构建 Flutter 应用时运用 Jetpack Compose 开发经验。
Flutter 是使用 Dart 编程语言构建跨平台应用的框架。
你的 Jetpack Compose 知识与经验在构建 Flutter 时非常宝贵。
本文档可跳转查阅,找到最符合你需求的问题。本指南嵌入示例代码;通过悬停或聚焦时出现的「Open in DartPad」按钮,可在 DartPad 中打开并运行部分示例。
概览
#Flutter 与 Jetpack Compose 代码描述 UI 的外观与行为,开发者称此类代码为 declarative framework(声明式框架)。
两者尤其在与传统 Android 代码交互方面存在关键差异,但框架之间也有许多共同点。
Composable 与 Widget
#Jetpack Compose 将 UI 组件表示为 composable functions(composable 函数),本文档中简称 composables。可通过 Modifier 对象修改或装饰 Composable。
Text("Hello, World!",
modifier: Modifier.padding(10.dp)
)
Text("Hello, World!",
modifier = Modifier.padding(10.dp))
Flutter 将 UI 组件表示为 widget。
Composable 与 widget 仅在需要变更前存在,这种特性称为 immutability(不可变性)。
Jetpack Compose modifies UI component properties using an optional
modifier property backed by a Modifier object.
By contrast, Flutter widgets configure their properties directly
through constructor parameters.
Jetpack Compose 通过由 Modifier 对象支持的 modifier 属性修改 UI 组件属性;Flutter widget 则通过构造函数参数直接配置属性。
Padding( // <-- This is a Widget
padding: EdgeInsets.all(10.0), // <-- a parameter to Padding
child: Text("Hello, World!"), // <-- This is also a Widget
);
组合布局时,Jetpack Compose 与 Flutter 都将 UI 组件相互嵌套:Jetpack Compose 嵌套 Composable,Flutter 嵌套 Widget。
布局过程
#Jetpack Compose 与 Flutter 布局方式相似:单次传递布局 UI,父元素向子元素提供布局约束。更具体地说:
-
The parent measures itself and its children recursively providing any constraints from the parent to the child.
父级递归测量自身与子级,将来自父级的约束传给子级。
-
The children try to size themselves using the above methods and provide their own children both their constraints and any that might apply from their ancestor nodes.
子级尝试按上述方法确定尺寸,并向自己的子级提供约束及可能来自祖先节点的约束。
-
Upon encountering a leaf node (a node with no children), the size and properties are determined based on the provided constraints and the element is placed in the UI.
遇到叶节点(无子节点)时,根据约束确定尺寸与属性并放置到 UI。
-
With all the children sized and placed, the root nodes can determine their measurement, size, and placement.
所有子级确定尺寸并放置后,根节点可确定自身的测量、尺寸与位置。
在 Jetpack Compose 与 Flutter 中,父组件可覆盖或约束子组件期望的尺寸;widget 不能任意尺寸,也 通常 无法知晓或决定屏幕位置,由父组件决定。
要强制子 widget 以特定尺寸渲染,父级须设置 tight constraints(紧约束);当最小尺寸等于最大尺寸时约束为紧约束。
要了解 Flutter 约束机制,请参阅 Understanding constraints(理解约束)。
设计系统
#Flutter 面向多平台,应用不必遵循特定设计系统。本指南使用 Material widget,但 Flutter 应用可采用多种设计系统:
Custom Material widgets
自定义 Material widget
Community built widgets
社区构建的 widget
Your own custom widgets
你自己的自定义 widget
若要参考采用自定义设计系统的优秀应用,请参阅 Wonderous。
UI 基础
#本节涵盖 Flutter UI 开发基础及其与 Jetpack Compose 的对比,包括如何开始开发、显示静态文本、创建按钮、响应点击、显示列表与网格等。
入门
#Compose 应用的主入口通常是 Activity 或其子类,一般为 ComponentActivity。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
SampleTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
启动 Flutter 应用时,将应用实例传给 runApp 函数。
void main() {
runApp(const MyApp());
}
App 是 widget,其 build 方法描述所代表的用户界面部分。通常以 WidgetApp
类(如 MaterialApp)开始应用。
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: HomePage(),
);
}
}
HomePage 中使用的 widget 可能以 Scaffold 类开始,Scaffold 实现应用的基本布局结构。
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text(
'Hello, World!',
),
),
);
}
}
注意 Flutter 使用 Center
widget。
Compose 继承 Android Views 的若干默认行为:除非另有说明,多数组件按内容「包裹」尺寸,即仅占用渲染所需空间。Flutter 并非总是如此。
要居中文本,请用 Center widget 包裹。要了解不同 widget 及其默认行为,请参阅 Widget catalog(Widget 目录)。
添加按钮
#
在 Compose 中,使用 Button composable 或其变体创建按钮;使用 Material 主题时 Button
是 FilledTonalButton 的别名。
Button(onClick = {}) {
Text("Do something")
}
在 Flutter 中,使用 FilledButton 类可达到相同效果:
FilledButton(
onPressed: () {
// This closure is called when your button is tapped.
},
const Text('Do something'),
),
Flutter 提供多种预定义样式的按钮。
水平或垂直对齐组件
#Jetpack Compose 与 Flutter 以相似方式处理水平与垂直排列的项。
以下 Compose 片段在 Row 与 Column 容器中添加地球图标与文本并居中:
Row(horizontalArrangement = Arrangement.Center) {
Image(Icons.Default.Public, contentDescription = "")
Text("Hello, world!")
}
Column(verticalArrangement = Arrangement.Center) {
Image(Icons.Default.Public, contentDescription = "")
Text("Hello, world!")
}
Flutter 也使用 Row
与 Column,但在指定子 widget 与对齐方面略有不同。以下与 Compose 示例等价:
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.public),
Text('Hello, world!'),
],
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(MaterialIcons.globe),
Text('Hello, world!'),
],
)
Row 与 Column 的 children 参数需要 List<Widget>。mainAxisAlignment
告诉 Flutter 如何在额外空间中定位子项;MainAxisAlignment.center 将子项放在主轴中心。Row 的主轴为水平轴,Column
则为垂直轴。
::: note
Flutter 的 Row 与 Column 用 MainAxisAlignment 与
CrossAxisAlignment 控制项的位置;Jetpack Compose 用以下垂直与水平属性之一:verticalArrangement、verticalAlignment、horizontalAlignment、horizontalArrangement。判断
MainAxis 的技巧是找以 arrangement 结尾的属性;CrossAxis 则以 alignment
结尾的属性为准。
:::
显示列表视图
#
在 Compose 中,可根据列表规模用几种方式创建列表:少量可一次显示的项可在 Column 或 Row 内遍历集合。
大量项的列表用 LazyList 性能更好,仅布局可见组件而非全部。
data class Person(val name: String)
val people = arrayOf(
Person(name = "Person 1"),
Person(name = "Person 2"),
Person(name = "Person 3")
)
@Composable
fun ListDemo(people: List<Person>) {
Column {
people.forEach {
Text(it.name)
}
}
}
@Composable
fun ListDemo2(people: List<Person>) {
LazyColumn {
items(people) { person ->
Text(person.name)
}
}
}
在 Flutter 中惰性构建列表……
class Person {
String name;
Person(this.name);
}
var items = [
Person('Person 1'),
Person('Person 2'),
Person('Person 3'),
];
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(items[index].name),
);
},
),
);
}
}
Flutter 列表有一些约定:
-
The
ListViewwidget has a builder method. This works like theitemclosure inside a ComposeLazyList. -
ListViewwidget 有 builder 方法,类似 ComposeLazyList内的item闭包。 -
The
itemCountparameter of theListViewsets how many items theListViewdisplays. ListView的itemCount参数设置显示项数。-
The
itemBuilderhas an index parameter that will be between zero and one less than itemCount. itemBuilder的 index 参数介于 0 与 itemCount 减 1 之间。
上一示例为每项返回 ListTile
widget,其包含 height、font-size 等属性有助于构建列表;但 Flutter 允许返回几乎任何表示数据的 widget。
显示网格
#
在 Compose 中构建网格类似 LazyList(LazyColumn 或 LazyRow),可使用相同 items
闭包;各网格类型有属性指定项排列方式、自适应或固定布局等。
val widgets = arrayOf(
"Row 1",
Icons.Filled.ArrowDownward,
Icons.Filled.ArrowUpward,
"Row 2",
Icons.Filled.ArrowDownward,
Icons.Filled.ArrowUpward
)
LazyVerticalGrid (
columns = GridCells.Fixed(3),
contentPadding = PaddingValues(8.dp)
) {
items(widgets) { i ->
if (i is String) {
Text(i)
} else {
Image(i as ImageVector, "")
}
}
}
在 Flutter 中用 GridView
widget 显示网格;该 widget 有多种构造函数,目标相似但参数不同。以下示例使用 .builder() 初始化:
const widgets = [
Text('Row 1'),
Icon(Icons.arrow_downward),
Icon(Icons.arrow_upward),
Text('Row 2'),
Icon(Icons.arrow_downward),
Icon(Icons.arrow_upward),
];
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisExtent: 40,
),
itemCount: widgets.length,
itemBuilder: (context, index) => widgets[index],
),
);
}
}
SliverGridDelegateWithFixedCrossAxisCount delegate 决定网格布局组件的多种参数,包括每行项数的 crossAxisCount。
Jetpack Compose 的 LazyHorizontalGrid、LazyVerticalGrid 与 Flutter 的 GridView
有些相似。GridView 用 delegate 决定布局;LazyHorizontalGrid/LazyVerticalGrid
的 rows、columns 等属性作用相同。
创建滚动视图
#Jetpack Compose 的 LazyColumn 与 LazyRow 内置滚动支持。
Flutter 用 SingleChildScrollView
创建滚动视图。以下示例中 mockPerson 模拟 Person 实例以创建自定义 PersonView widget。
SingleChildScrollView(
child: Column(
children: mockPersons
.map(
(person) => PersonView(
person: person,
),
)
.toList(),
),
),
响应式与自适应设计
#Compose 中的自适应设计是复杂主题,有多种可行方案:
Using a custom layout
使用自定义布局
Using
WindowSizeClassalone单独使用
WindowSizeClass-
Using
BoxWithConstraintsto control what is shown based on available space 使用
BoxWithConstraints根据可用空间控制显示内容-
Using the Material 3 adaptive library that uses
WindowSizeClassalong with specialized composable layouts for common layouts 使用 Material 3 自适应库,结合
WindowSizeClass与常见布局的专用 composable 布局
因此建议你直接了解 Flutter 选项,看何者符合需求,而非强求一一对应翻译。
在 Flutter 中创建相对视图有两种方式:
-
Get the
BoxConstraintsobject in theLayoutBuilderclass. -
在
LayoutBuilder类中获取BoxConstraints对象。 -
Use the
MediaQuery.of()in your build functions to get the size and orientation of your current app. -
在 build 函数中使用
MediaQuery.of()获取当前应用的尺寸与方向。
了解更多请参阅 Creating responsive and adaptive apps(创建响应式与自适应应用)。
管理状态
#Compose 用 remember API 与 MutableState 接口的后代存储状态。
Scaffold(
content = { padding ->
var _counter = remember { mutableIntStateOf(0) }
Column(horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize().padding(padding)) {
Text(_counter.value.toString())
Spacer(modifier = Modifier.height(16.dp))
FilledIconButton (onClick = { -> _counter.intValue += 1 }) {
Text("+")
}
}
}
)
Flutter 用 StatefulWidget
管理本地状态,需以下两个类实现:
a subclass of
StatefulWidgetStatefulWidget的子类a subclass of
StateState的子类
State 对象存储 widget 状态;要变更状态,在 State 子类中调用 setState() 通知框架重绘 widget。
以下示例展示计数器应用的一部分:
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('$_counter'),
TextButton(
onPressed: () => setState(() {
_counter++;
}),
child: const Text('+'),
),
],
),
),
);
}
}
更多状态管理方式请参阅 State management(状态管理)。
在屏幕上绘制
#在 Compose 中,用 Canvas composable 在屏幕上绘制形状、图像与文本。
Flutter 基于 Canvas 类提供 API,有两个类辅助绘制:
-
CustomPaintthat requires a painter:dartCustomPaint( painter: SignaturePainter(_points), size: Size.infinite, ), -
CustomPainterthat implements your algorithm to draw to the canvas.dartclass SignaturePainter extends CustomPainter { SignaturePainter(this.points); final List<Offset?> points; @override void paint(Canvas canvas, Size size) { final Paint paint = Paint() ..color = Colors.black ..strokeCap = StrokeCap.round ..strokeWidth = 5; for (int i = 0; i < points.length - 1; i++) { if (points[i] != null && points[i + 1] != null) { canvas.drawLine(points[i]!, points[i + 1]!, paint); } } } @override bool shouldRepaint(SignaturePainter oldDelegate) => oldDelegate.points != points; }
主题、样式与媒体
#可轻松为 Flutter 应用设置样式,包括在浅色与深色主题间切换、更改文本与 UI 组件设计等。本节介绍如何设置样式。
使用深色模式
#在 Compose 中,可用 Theme composable 包裹组件,在任意层级控制浅色与深色。
在 Flutter 中,可在应用级控制浅色与深色模式,通过 App 类的 theme 属性控制亮度模式:
const MaterialApp(
theme: ThemeData(
brightness: Brightness.dark,
),
home: HomePage(),
);
设置文本样式
#在 Compose 中,用 Text 上的属性设置一两个属性,或构建 TextStyle 一次设置多个。
Text("Hello, world!", color = Color.Green,
fontWeight = FontWeight.Bold, fontSize = 30.sp)
Text("Hello, world!",
style = TextStyle(
color = Color.Green,
fontSize = 30.sp,
fontWeight = FontWeight.Bold
),
)
在 Flutter 中,将 TextStyle 作为 Text widget 的 style
参数值以设置文本样式。
Text(
'Hello, world!',
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
设置按钮样式
#在 Compose 中,用 colors 属性修改按钮颜色;未修改则使用当前主题默认色。
Button(onClick = {},
colors = ButtonDefaults.buttonColors().copy(
containerColor = Color.Yellow, contentColor = Color.Blue,
)) {
Text("Do something", fontSize = 30.sp, fontWeight = FontWeight.Bold)
}
在 Flutter 中,可类似地设置子项样式或修改按钮自身属性。
FilledButton(
onPressed: (){},
style: FilledButton.styleFrom(backgroundColor: Colors.amberAccent),
child: const Text(
'Do something',
style: TextStyle(
color: Colors.blue,
fontSize: 30,
fontWeight: FontWeight.bold,
)
)
)
为 Flutter 打包资源
#应用常需打包资源:动画、矢量图、图像、字体或其他文件。
与原生 Android 在 /res/<qualifier>/ 下要求特定目录结构不同,Flutter 只要资源列在 pubspec.yaml
中即可,无需固定位置。以下是引用若干图像与字体的 pubspec.yaml 摘录。
flutter:
assets:
- assets/my_icon.png
- assets/background.png
fonts:
- family: FiraSans
fonts:
- asset: fonts/FiraSans-Regular.ttf
使用字体
#在 Compose 中,使用字体有两种方式:通过运行时服务获取 Google Fonts,或打包在资源文件中。
Flutter 有类似字体用法,下面一并说明。
使用打包字体
#以下 Compose 与 Flutter 代码大致等价,用于使用上文所列 /res/ 或 fonts 目录中的字体文件。
// Font files bundled with app
val firaSansFamily = FontFamily(
Font(R.font.firasans_regular, FontWeight.Normal),
// ...
)
// Usage
Text(text = "Compose", fontFamily = firaSansFamily, fontWeight = FontWeight.Normal)
Text(
'Flutter',
style: TextStyle(
fontSize: 40,
fontFamily: 'FiraSans',
),
),
使用字体提供方(Google Fonts)
#差异之一是从 Google Fonts 等提供方使用字体:在 Compose 中,实例化方式与引用本地文件大致相同。
实例化引用字体服务特殊字符串的 provider 后,使用相同 FontFamily 声明。
// Font files bundled with app
val provider = GoogleFont.Provider(
providerAuthority = "com.google.android.gms.fonts",
providerPackage = "com.google.android.gms",
certificates = R.array.com_google_android_gms_fonts_certs
)
val firaSansFamily = FontFamily(
Font(
googleFont = GoogleFont("FiraSans"),
fontProvider = provider,
)
)
// Usage
Text(text = "Compose", fontFamily = firaSansFamily, fontWeight = FontWeight.Light)
Flutter 由 google_fonts 插件按字体名称提供。
import 'package:google_fonts/google_fonts.dart';
//...
Text(
'Flutter',
style: GoogleFonts.firaSans(),
// or
//style: GoogleFonts.getFont('FiraSans')
),
使用图片
#
在 Compose 中,图像通常放在 /res/drawable,用 Image composable 显示,通过
R.drawable.<文件名>(无扩展名)引用。
在 Flutter 中,资源位置列在 pubspec.yaml 中,如下片段所示。
flutter:
assets:
- images/Blueberries.jpg
添加图像后,可用 Image widget 的 .asset() 构造函数显示。该构造函数:
完整示例请参阅 Image 文档。
除非另有说明,本文档之所提及适用于 Flutter 3.44.0 版本。本页面最后更新时间:2026-06-04。查看文档源码 或者 为本页面内容提出建议。