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

在构建 Flutter 应用程序时,您的 Jetpack Compose 知识和经验非常有价值。

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

概述

#

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

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

可组合项与 Widgets

#

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

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

Flutter 将 UI 组件表示为widgets

可组合项和 widgets 都只存在到它们需要更改为止。这些语言将此属性称为不变性。Jetpack Compose 使用由 Modifier 对象支持的可选修饰符属性修改 UI 组件属性。相比之下,Flutter 将 widgets 用于 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 widgets,但您的 Flutter 应用程序可以使用许多不同的设计系统

  • 自定义 Material widgets
  • 社区构建的 widgets
  • 您自己的自定义 widgets

如果您正在寻找一个具有自定义设计系统的出色参考应用程序,请查看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 Views 继承了许多默认值。除非另有说明,否则大多数组件都会将其大小“包裹”到内容,这意味着它们在渲染时只占用所需的空间。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!'),
  ],
)

RowColumn 需要在 children 参数中包含一个 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 有一个 builder 方法。这与 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,它需要一个 painter

    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 中打包资产

#

通常需要将资源打包以供应用程序使用。它们可以是动画、矢量图形、图像、字体或其他通用文件。

与期望在 /res/<qualifier>/ 下设置目录结构的本机 Android 应用程序不同(其中 qualifier 可以指示文件类型、特定方向或 Android 版本),Flutter 不需要特定位置,只要引用的文件在 pubspec.yaml 文件中列出即可。下面是引用了多个图像和字体文件的 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 bundled 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 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 插件使用字体名称提供。

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

使用图片

#

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

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

yaml
    flutter:
      assets:
        - images/Blueberries.jpg

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

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