面向 Jetpack Compose 开发者的 Flutter
学习如何在构建 Flutter 应用时应用 Jetpack Compose 开发经验。
Flutter 是一个使用 Dart 编程语言构建跨平台应用程序的框架。
您在 Jetpack Compose 方面的知识和经验在构建 Flutter 应用时非常有价值。
本文档可用作参考指南,您可以根据需要跳转查看最相关的问题。本指南嵌入了示例代码。通过悬停或聚焦时显示的“在 DartPad 中打开”按钮,您可以在 DartPad 中查看并运行部分示例。
概述
#Flutter 和 Jetpack Compose 的代码都描述了 UI 的外观和工作方式。开发者将这种类型的代码称为声明式框架。
尽管两者存在主要差异(尤其是在与传统的 Android 代码交互时),但这两个框架之间有许多共同点。
Composable 函数与 Widget
#Jetpack Compose 将 UI 组件表示为可组合函数 (composable functions),在本文档中简称为 composables。Composables 可以通过使用 Modifier 对象进行更改或修饰。
Text("Hello, World!",
modifier: Modifier.padding(10.dp)
)
Text("Hello, World!",
modifier = Modifier.padding(10.dp))
Flutter 将 UI 组件表示为 widgets。
Composables 和 Widgets 在需要更改之前一直保持存在。这些语言将这种特性称为不可变性。Jetpack Compose 使用由 Modifier 对象支持的可选 modifier 属性来修改 UI 组件属性。相比之下,Flutter widgets 则直接通过构造函数参数来配置其属性。
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 嵌套 Composables,而 Flutter 嵌套 Widgets。
布局流程
#Jetpack Compose 和 Flutter 处理布局的方式非常相似。两者都通过单次遍历来布局 UI,并且父元素会将布局约束向下传递给子元素。具体来说:
- 父级递归地测量自身及其子级,并将来自父级的任何约束提供给子级。
- 子级尝试使用上述方法确定自身大小,并为其子级提供它们自己的约束以及可能适用的任何来自祖先节点的约束。
- 当遇到叶节点(没有子节点的节点)时,会根据提供的约束确定大小和属性,并将该元素放置在 UI 中。
- 在所有子级完成测量和放置后,根节点可以确定其测量值、大小和位置。
在 Jetpack Compose 和 Flutter 中,父组件都可以覆盖或限制子组件的期望大小。Widget 不能随意设置大小。它也通常无法知晓或决定其在屏幕上的位置,因为该决定由其父级做出。
要强制子 widget 以特定大小呈现,父级必须设置紧凑约束 (tight constraints)。当约束的最小尺寸值等于其最大尺寸值时,该约束即变为紧凑。
要了解 Flutter 中的约束是如何工作的,请访问理解约束。
设计系统
#由于 Flutter 面向多个平台,您的应用不需要遵循任何特定的设计系统。虽然本指南以 Material widgets 为特色,但您的 Flutter 应用可以使用许多不同的设计系统。
- 自定义 Material widgets
- 社区构建的 widgets
- 您自己的自定义 widgets
如果您正在寻找一个采用自定义设计系统的优秀参考应用,请查看 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 View 祖先那里继承了许多默认设置。除非另有说明,否则大多数组件会根据内容“包裹”其大小,这意味着它们在渲染时仅占用所需的空间。但在 Flutter 中情况并非总是如此。
要使文本居中,请将其包裹在 Center widget 中。要了解有关不同 widget 及其默认行为的信息,请查看组件目录 (Widget catalog)。
添加按钮
#在 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 对列表有一些约定。
-
ListViewwidget 有一个构建器方法。其工作方式类似于 ComposeLazyList中的item闭包。 -
ListView的itemCount参数设置ListView显示多少项。 -
itemBuilder具有一个索引参数,该索引将介于 0 到 itemCount 减 1 之间。
前面的示例为每个项返回了一个 ListTile widget。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 委托决定了网格用于布局其组件的各种参数。这包括决定每行显示项数的 crossAxisCount。
Jetpack Compose 的 LazyHorizontalGrid、LazyVerticalGrid 与 Flutter 的 GridView 在某种程度上是相似的。GridView 使用委托来决定网格应如何布局其组件。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 中的自适应设计是一个复杂的话题,有许多可行的解决方案:
- 使用自定义布局
- 仅使用
WindowSizeClass - 使用
BoxWithConstraints根据可用空间控制显示内容 - 使用 Material 3 自适应库,该库结合使用
WindowSizeClass和用于常见布局的专用 composable 布局
因此,建议您直接查看 Flutter 的选项,看看什么符合您的要求,而不是试图寻找一一对应的转换。
要在 Flutter 中创建相对视图,可以使用以下两种选项之一:
- 在
LayoutBuilder类中获取BoxConstraints对象。 - 在您的构建函数中使用
MediaQuery.of()来获取当前应用的大小和方向。
要了解更多信息,请查看创建响应式和自适应应用。
状态管理
#
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 管理本地状态。通过以下两个类实现有状态的 widget:
StatefulWidget的子类State的子类
State 对象存储 widget 的状态。要更改 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('+'),
),
],
),
),
);
}
}
要了解更多管理状态的方法,请查看状态管理。
在屏幕上绘制
#在 Compose 中,您使用 Canvas composable 在屏幕上绘制形状、图像和文本。
Flutter 拥有一个基于 Canvas 类的 API,并提供了两个帮助您绘制的类:
-
CustomPaint,需要一个 painterdartCustomPaint( painter: SignaturePainter(_points), size: Size.infinite, ), -
CustomPainter,实现了您绘制到画布的算法。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 widget 添加为 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 中的按钮 widget 设置样式,您同样可以设置其子级的样式,或者直接修改按钮本身的属性。
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>/ 下有特定的目录结构,其中限定符可能指示文件类型、特定方向或 Android 版本)不同,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 也有类似的方法来使用字体,我们将在下面讨论这两种方法。
使用绑定字体
#以下是用于在 /res/ 或 fonts 目录中使用字体文件的 Compose 和 Flutter 代码,基本等效。
// 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 中,实例化是内联完成的,使用的代码与引用本地文件的代码大致相同。
在实例化引用字体服务特殊字符串的提供程序后,您将使用相同的 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 中,图像文件通常放在资源目录的 drawable 文件夹 /res/drawable 中,并使用 Image composable 显示这些图像。资源通过使用资源定位器(格式为 R.drawable.<file name>,不带文件扩展名)来引用。
在 Flutter 中,资源位置列在 pubspec.yaml 中,如下面的代码片段所示。
flutter:
assets:
- images/Blueberries.jpg
添加图像后,您可以使用 Image widget 的 .asset() 构造函数显示它。此构造函数
要查看完整示例,请参阅 Image 文档。