Flutter 是一个使用 Dart 编程语言构建跨平台应用的框架。

在使用 Flutter 进行构建时,你的 Jetpack Compose 知识和经验非常有价值。

本文档可以作为参考,你可以跳到任何部分,找到与你需求最相关的问题。本指南嵌入了示例代码。通过使用悬停或聚焦时出现的“在 DartPad 中打开”按钮,你可以在 DartPad 上打开并运行某些示例。

概述

#

Flutter 和 Jetpack Compose 代码描述了 UI 的外观和工作方式。开发者将此类代码称为声明式框架

尽管在与旧版 Android 代码交互方面存在关键差异,但这两种框架之间有许多共通之处。

可组合项与 widget

#

Jetpack Compose 将 UI 组件表示为可组合函数,在本文档中后续称为可组合项。可组合项可以通过使用 Modifier 对象进行修改或装饰。

kotlin
Text("Hello, World!", 
   modifier: Modifier.padding(10.dp)
)
Text("Hello, World!",
    modifier = Modifier.padding(10.dp))

Flutter 将 UI 组件表示为widget

可组合项和 widget 都只在需要改变时才存在。这些语言称此属性为不变性。Jetpack Compose 使用由 Modifier 对象支持的可选 modifier 属性来修改 UI 组件属性。相比之下,Flutter 将 widget 用于 UI 组件及其属性。

dart
Padding(                         // <-- This is a Widget
  padding: EdgeInsets.all(10.0), // <-- So is this
  child: Text("Hello, World!"),  // <-- This, too
)));

为了组合布局,Jetpack Compose 和 Flutter 都将 UI 组件相互嵌套。Jetpack Compose 嵌套 Composables,而 Flutter 嵌套 Widgets

布局过程

#

Jetpack Compose 和 Flutter 以类似的方式处理布局。它们都以单次遍历的方式布局 UI,并且父元素将其布局约束传递给子元素。更具体地说,

  1. 父级递归测量自身及其子级,并提供所有父级到子级的约束。
  2. 子级尝试使用上述方法调整自身大小,并为其自己的子级提供其约束以及可能从其祖先节点继承的任何约束。
  3. 当遇到叶节点(没有子级的节点)时,将根据提供的约束确定大小和属性,并将该元素放置在 UI 中。
  4. 在所有子级都已调整大小和定位后,根节点可以确定它们的测量、大小和放置。

在 Jetpack Compose 和 Flutter 中,父组件都可以覆盖或约束子组件的期望大小。widget 不能拥有它想要的任何大小。它通常也无法知道或决定其在屏幕上的位置,因为其父级会做出该决定。

要强制子 widget 以特定大小渲染,父级必须设置严格约束。当约束的最小尺寸值等于其最大尺寸值时,该约束变为严格约束。

要了解 Flutter 中约束的工作原理,请访问 理解约束

设计系统

#

因为 Flutter 针对多个平台,所以你的应用不需要遵循任何设计系统。虽然本指南介绍了 Material widget,但你的 Flutter 应用可以使用许多不同的设计系统

  • 自定义 Material widget
  • 社区构建的 widget
  • 你自己的自定义 widget

如果你正在寻找一个具有自定义设计系统的优秀参考应用,请查阅 Wonderous

UI 基础

#

本节涵盖 Flutter 中 UI 开发的基础知识,以及它与 Jetpack Compose 的比较。这包括如何开始开发你的应用、显示静态文本、创建按钮、响应按压事件、显示列表、网格等。

入门

#

对于 Compose 应用,你的主要入口点将是 Activity 或其后代之一,通常是 ComponentActivity

kotlin
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 函数。

dart
void main() {
  runApp(const MyApp());
}

App 是一个 widget。它的 build 方法描述了它所代表的用户界面部分。你的应用通常会以 WidgetApp 类(例如 MaterialApp)开始。

dart
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: HomePage(),
    );
  }
}

HomePage 中使用的 widget 可能以 Scaffold 类开头。Scaffold 为应用实现了一个基本的布局结构。

dart
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 目录

添加按钮

#

Compose 中,你使用 Button 可组合项或其变体之一来创建按钮。在使用 Material 主题时,ButtonFilledTonalButton 的别名。

kotlin
Button(onClick = {}) {
    Text("Do something")
}

要在 Flutter 中实现相同的结果,请使用 FilledButton

dart
FilledButton(
  onPressed: () {
    // This closure is called when your button is tapped.
  },
  const Text('Do something'),
),

Flutter 提供了各种具有预定义样式的按钮。

水平或垂直对齐组件

#

Jetpack Compose 和 Flutter 以类似的方式处理水平和垂直项目集合。

以下 Compose 代码片段在 RowColumn 容器中添加了一个地球图像和文本,并使项目居中

kotlin
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 也使用 RowColumn,但在指定子 widget 和对齐方式方面存在一些细微差别。以下示例与 Compose 示例等效。

dart
Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    Icon(Icons.public),
    Text('Hello, world!'),
  ],
),

Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    Icon(MaterialIcons.globe),
    Text('Hello, world!'),
  ],
)

RowColumnchildren 参数中需要一个 List<Widget>mainAxisAlignment 属性告诉 Flutter 如何在额外空间中定位子元素。MainAxisAlignment.center 将子元素放置在主轴的中心。对于 Row,主轴是水平轴;对于 Column,主轴是垂直轴。

显示列表视图

#

Compose 中,你可以根据需要显示的列表大小,通过几种方式创建列表。对于少量可以一次性显示的所有项目,你可以在 ColumnRow 内部遍历集合。

对于包含大量项目的列表,LazyList 具有更好的性能。它只布局将可见的组件,而不是所有组件。

kotlin
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 中延迟构建列表,....

dart
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 有一些列表约定

  • ListView widget 有一个构建器方法。这类似于 Compose LazyList 内部的 item 闭包。

  • ListViewitemCount 参数设置了 ListView 显示的项目数量。

  • itemBuilder 有一个索引参数,其值将在零到 itemCount 减一之间。

上一个示例为每个项目返回了一个 ListTile widget。ListTile widget 包含 heightfont-size 等属性。这些属性有助于构建列表。然而,Flutter 允许你返回几乎任何代表你数据的 widget。

显示网格

#

Compose 中构建网格类似于 LazyList(LazyColumnLazyRow)。你可以使用相同的 items 闭包。每种网格类型都有属性来指定如何排列项目,以及是否使用自适应或固定布局等。

kotlin
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() 初始化器

dart
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 的 LazyHorizontalGridLazyVerticalGrid 和 Flutter 的 GridView 有些相似。GridView 使用委托来决定网格如何布局其组件。LazyHorizontalGrid \ LazyVerticalGrid 上的 rowscolumns 和其他相关属性具有相同的目的。

创建滚动视图

#

Jetpack Compose 中的 LazyColumnLazyRow 内置了滚动支持。

要创建可滚动视图,Flutter 使用 SingleChildScrollView。在以下示例中,函数 mockPerson 模拟 Person 类的实例以创建自定义 PersonView widget。

dart
SingleChildScrollView(
  child: Column(
    children: mockPersons
        .map(
          (person) => PersonView(
            person: person,
          ),
        )
        .toList(),
  ),
),

响应式和自适应设计

#

Compose 中的自适应设计是一个复杂的话题,有许多可行的解决方案

  • 使用自定义布局
  • 单独使用 WindowSizeClass
  • 使用 BoxWithConstraints 根据可用空间控制显示内容
  • 使用 Material 3 自适应库,该库结合 WindowSizeClass 和专门的可组合布局来处理常见布局

因此,建议你直接研究 Flutter 选项,看看什么符合你的要求,而不是试图找到一对一的翻译。

要在 Flutter 中创建相对视图,你可以使用以下两种选项之一

  • LayoutBuilder 类中获取 BoxConstraints 对象。
  • 在你的构建函数中使用 MediaQuery.of() 来获取当前应用的大小和方向。

要了解更多信息,请查阅 创建响应式和自适应应用

状态管理

#

Compose 使用 remember API 和 MutableState 接口的后代来存储状态。

kotlin
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。

以下示例展示了计数器应用的一部分

dart
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 可组合项在屏幕上绘制形状、图像和文本。

Flutter 有一个基于 Canvas 类的 API,其中有两个类可以帮助你进行绘制

  1. CustomPaint,它需要一个画笔

    dart
    CustomPaint(
      painter: SignaturePainter(_points),
      size: Size.infinite,
    ),
  2. CustomPainter,它实现了你在画布上绘制的算法。

    dart
    class 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 可组合项中来在任何任意级别控制浅色和深色模式。

Flutter 中,你可以在应用级别控制浅色和深色模式。要控制亮度模式,请使用 App 类的 theme 属性

dart
const MaterialApp(
  theme: ThemeData(
    brightness: Brightness.dark,
  ),
  home: HomePage(),
);

文本样式

#

Compose 中,你可以使用 Text 上的属性来设置一两个属性,或者构造一个 TextStyle 对象来一次性设置许多属性。

kotlin
Text("Hello, world!", color = Color.Green,
        fontWeight = FontWeight.Bold, fontSize = 30.sp)
kotlin
Text("Hello, world!", 
   style = TextStyle(
      color = Color.Green, 
      fontSize = 30.sp, 
      fontWeight = FontWeight.Bold
   ),
)

要在 Flutter 中设置文本样式,请将 TextStyle widget 添加为 Text widget 的 style 参数的值。

dart
Text(
  'Hello, world!',
  style: TextStyle(
    fontSize: 30,
    fontWeight: FontWeight.bold,
    color: Colors.blue,
  ),
),

按钮样式

#

Compose 中,你使用 colors 属性修改按钮的颜色。如果未修改,它们将使用当前主题的默认值。

kotlin
Button(onClick = {},
   colors = ButtonDefaults.buttonColors().copy(
      containerColor = Color.Yellow, contentColor = Color.Blue,
       )) {
    Text("Do something", fontSize = 30.sp, fontWeight = FontWeight.Bold)
}

要在 Flutter 中为按钮 widget 设置样式,你同样可以设置其子级的样式,或修改按钮本身的属性。

dart
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 应用不同,原生 Android 应用期望在 /res/<qualifier>/ 下有一个固定的目录结构,其中限定符可以指示文件类型、特定方向或 Android 版本,只要引用文件在 pubspec.yaml 文件中列出,Flutter 就不需要特定的位置。下面是 pubspec.yaml 中引用了多个图片和字体文件的摘录。

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 目录中的字体文件。

kotlin
// Font files bunded with app
val firaSansFamily = FontFamily(
   Font(R.font.firasans_regular, FontWeight.Normal),
   // ...
)

// Usage
Text(text = "Compose", fontFamily = firaSansFamily, fontWeight = FontWeight.Normal)
dart
Text(
  'Flutter',
  style: TextStyle(
    fontSize: 40,
    fontFamily: 'FiraSans',
  ),
),

使用字体提供商(Google Fonts)

#

一个不同点是使用来自 Google Fonts 等字体提供商的字体。在 Compose 中,实例化是内联完成的,使用与引用本地文件大致相同的代码。

在实例化一个引用字体服务特殊字符串的提供商后,你将使用相同的 FontFamily 声明。

kotlin
// Font files bunded 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 插件通过字体名称提供。

dart
import 'package:google_fonts/google_fonts.dart';
//...
Text(
  'Flutter',
  style: GoogleFonts.firaSans(),
  // or 
  //style: GoogleFonts.getFont('FiraSans')
),

使用图片

#

Compose 中,图像文件通常放在资源中的 drawable 目录 /res/drawable,并使用 Image 可组合项显示图像。资产通过使用资源定位器(格式为 R.drawable.<文件名>,不带文件扩展名)进行引用。

Flutter 中,资源位置在 pubspec.yaml 中列出,如下面代码片段所示。

yaml
    flutter:
      assets:
        - images/Blueberries.jpg

添加图片后,你可以使用 Image widget 的 .asset() 构造函数显示它。这个构造函数

要查看完整的示例,请查阅 Image 文档。