适用于 SwiftUI 开发者的 Flutter

想要使用 Flutter 编写移动应用程序的 SwiftUI 开发者应查看本指南。它解释了如何将现有的 SwiftUI 知识应用于 Flutter。

Flutter 是一个使用 Dart 编程语言构建跨平台应用程序的框架。要了解使用 Dart 编程和使用 Swift 编程之间的一些差异,请参阅 Swift 开发者学习 Dart适用于 Swift 开发者的 Flutter 并发性

在使用 Flutter 构建时,您的 SwiftUI 知识和经验非常宝贵。

Flutter 还对在 iOS 和 macOS 上运行时的应用程序行为进行了许多调整。要了解如何进行调整,请参阅 平台调整

本指南可作为食谱使用,您可以随意浏览,找到与您的需求最相关的疑问。本指南嵌入了示例代码。您可以在 DartPad 上测试完整的示例,或在 GitHub 上查看它们。

概述

作为介绍,请观看以下视频。它概述了 Flutter 在 iOS 上的工作原理以及如何使用 Flutter 构建 iOS 应用程序。

Flutter 和 SwiftUI 代码描述了 UI 的外观和工作方式。开发者将这种类型的代码称为声明性框架

视图与小组件

SwiftUI 将 UI 组件表示为视图。您使用修饰符配置视图。

Text("Hello, World!") // <-- This is a View
  .padding(10)        // <-- This is a modifier of that View

Flutter 将 UI 组件表示为小组件

视图和小组件都只存在于需要更改它们之前。这些语言将此属性称为不可变性。SwiftUI 将 UI 组件属性表示为视图修饰符。相比之下,Flutter 将小组件用于 UI 组件及其属性。

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

为了组合布局,SwiftUI 和 Flutter 都将 UI 组件相互嵌套。SwiftUI 嵌套视图,而 Flutter 嵌套小组件。

布局流程

SwiftUI 使用以下流程布局视图

  1. 父视图向其子视图提议一个大小。
  2. 所有后续子视图
    • 子视图提议一个大小
    • 询问该子视图它想要什么大小
  3. 每个父视图以返回的大小渲染其子视图。

Flutter 的流程略有不同

  1. 父小组件将约束传递给其子组件。约束包括高度和宽度的最小值和最大值。
  2. 子组件尝试确定其大小。它使用其自己的子组件列表重复相同的流程
    • 它通知其子组件子组件的约束。
    • 它询问其子组件它希望是什么大小。
  3. 父组件布局子组件。
    • 如果请求的大小符合约束,则父组件将使用该大小。
    • 如果请求的尺寸不符合约束,父级会限制高度、宽度或两者,以使其符合其约束。

Flutter 与 SwiftUI 不同,因为父组件可以覆盖子组件的期望尺寸。该小组件无法拥有任何它想要的尺寸。它也不能知道或决定其在屏幕上的位置,因为它的父级会做出该决定。

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

SwiftUI 中,视图可能会扩展到可用空间或将其尺寸限制为其内容的尺寸。Flutter 小组件的行为方式类似。

但是,在 Flutter 中,父小组件可以提供无界约束。无界约束将其最大值设置为无穷大。

UnboundedBox(
  child: Container(
      width: double.infinity, height: double.infinity, color: red),
)

如果子组件扩展并且它具有无界约束,Flutter 会返回溢出警告

UnconstrainedBox(
  child: Container(color: red, width: 4000, height: 50),
)

When parents pass unbounded constraints to children, and the children are expanding, then there is an overflow warning

要了解约束在 Flutter 中的工作原理,请参阅 了解约束

设计系统

由于 Flutter 针对多个平台,因此您的应用无需符合任何设计系统。尽管本指南提供了 Material 小组件,但您的 Flutter 应用可以使用许多不同的设计系统

  • 自定义 Material 小组件
  • 社区构建的小组件
  • 您自己的自定义小组件
  • Cupertino 小组件,遵循 Apple 的人机界面指南

如果您正在寻找一个以自定义设计系统为特色的优秀参考应用,请查看 Wonderous

UI 基础知识

本部分介绍了 Flutter 中 UI 开发的基础知识,以及它与 SwiftUI 的比较。这包括如何开始开发您的应用、显示静态文本、创建按钮、响应按压事件、显示列表、网格等。

入门

SwiftUI 中,您使用 App 来启动您的应用。

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            HomePage()
        }
    }
}

另一种常见的 SwiftUI 实践是将应用主体放置在符合 View 协议的 struct 中,如下所示

struct HomePage: View {
  var body: some View {
    Text("Hello, World!")
  }
}

要启动您的 Flutter 应用,请将您的应用实例传递给 runApp 函数。

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

App 是一个小组件。build 方法描述了它所表示的用户界面部分。通常使用 WidgetApp 类(如 CupertinoApp)来开始您的应用。

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

  @override
  Widget build(BuildContext context) {
    // Returns a CupertinoApp that, by default,
    // has the look and feel of an iOS app.
    return const CupertinoApp(
      home: HomePage(),
    );
  }
}

HomePage 中使用的组件可能会从 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 组件。SwiftUI 默认将视图的内容呈现在其中心。Flutter 并非总是如此。 Scaffold 不会将它的 body 组件呈现在屏幕的中心。要将文本居中,请将其包装在 Center 组件中。要了解不同的组件及其默认行为,请查看 组件目录

添加按钮

SwiftUI 中,使用 Button 结构来创建按钮。

Button("Do something") {
  // this closure gets called when your
  // button is tapped
}

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

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

Flutter 为你提供了各种具有预定义样式的按钮。 CupertinoButton 类来自 Cupertino 库。Cupertino 库中的组件使用 Apple 的设计系统。

水平对齐组件

SwiftUI 中,堆栈视图在设计布局中发挥着重要作用。两个单独的结构允许你创建堆栈

  1. HStack 用于水平堆栈视图

  2. VStack 用于垂直堆栈视图

以下 SwiftUI 视图将地球图像和文本添加到水平堆栈视图

HStack {
  Image(systemName: "globe")
  Text("Hello, world!")
}

Flutter 使用 Row 而不是 HStack

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

Row 组件需要 List<Widget>children 参数中。 mainAxisAlignment 属性告诉 Flutter 如何使用额外空间放置子组件。 MainAxisAlignment.center 将子组件置于主轴的中心。对于 Row,主轴是水平轴。

垂直对齐组件

以下示例基于上一节中的示例。

SwiftUI 中,使用 VStack 将组件排列成垂直柱。

VStack {
  Image(systemName: "globe")
  Text("Hello, world!")
}

Flutter 使用与上一示例相同的 Dart 代码,只是将 Column 换成了 Row

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

显示列表视图

SwiftUI 中,使用 List 基本组件来显示项目序列。要显示模型对象序列,请确保用户可以识别模型对象。要使对象可识别,请使用 Identifiable 协议。

struct Person: Identifiable {
  var name: String
}

var persons = [
  Person(name: "Person 1"),
  Person(name: "Person 2"),
  Person(name: "Person 3"),
]

struct ListWithPersons: View {
  let persons: [Person]
  var body: some View {
    List {
      ForEach(persons) { person in
        Text(person.name)
      }
    }
  }
}

这类似于 Flutter 构建其列表小部件的方式。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 对列表有一些注意事项

  • ListView 小部件有一个构建器方法。这类似于 SwiftUI 的 List 结构中的 ForEach

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

  • itemBuilder 有一个索引参数,该参数介于 0 和 itemCount 减 1 之间。

上一个示例为每个项目返回一个 ListTile 小部件。ListTile 小部件包括诸如 heightfont-size 之类的属性。这些属性有助于构建列表。但是,Flutter 允许你返回几乎任何表示数据的窗口小部件。

显示网格

SwiftUI 中构建非条件网格时,使用 GridGridRow

Grid {
  GridRow {
    Text("Row 1")
    Image(systemName: "square.and.arrow.down")
    Image(systemName: "square.and.arrow.up")
  }
  GridRow {
    Text("Row 2")
    Image(systemName: "square.and.arrow.down")
    Image(systemName: "square.and.arrow.up")
  }
}

要在 Flutter 中显示网格,请使用 GridView 小部件。此小部件有各种构造函数。每个构造函数都有类似的目标,但使用不同的输入参数。以下示例使用 .builder() 初始化程序

const widgets = [
  Text('Row 1'),
  Icon(CupertinoIcons.arrow_down_square),
  Icon(CupertinoIcons.arrow_up_square),
  Text('Row 2'),
  Icon(CupertinoIcons.arrow_down_square),
  Icon(CupertinoIcons.arrow_up_square),
];

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,它规定每行显示的项目数。

SwiftUI 的 Grid 和 Flutter 的 GridView 的不同之处在于 Grid 需要 GridRowGridView 使用委托来决定网格如何布局其组件。

创建滚动视图

SwiftUI 中,您使用 ScrollView 创建自定义滚动组件。以下示例以可滚动的方式显示一系列 PersonView 实例。

ScrollView {
  VStack(alignment: .leading) {
    ForEach(persons) { person in
      PersonView(person: person)
    }
  }
}

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

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

响应式和自适应设计

SwiftUI 中,您使用 GeometryReader 创建相对视图大小。

例如,您可以

  • geometry.size.width 乘以某个因子来设置宽度
  • 使用 GeometryReader 作为断点来更改应用的设计。

您还可以使用 horizontalSizeClass 查看大小类是否具有 .regular.compact

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

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

管理状态

SwiftUI 中,您使用 @State 属性包装器来表示 SwiftUI 视图的内部状态。

struct ContentView: View {
  @State private var counter = 0;
  var body: some View {
    VStack{
      Button("+") { counter+=1 }
      Text(String(counter))
    }
  }}

SwiftUI 还包括一些用于更复杂状态管理的选项,例如 ObservableObject 协议。

Flutter 使用 StatefulWidget 管理本地状态。使用以下两个类实现有状态小部件

  • StatefulWidget 的子类
  • State 的子类

State 对象存储小部件的状态。要更改小部件的状态,请从 State 子类调用 setState() 以告知框架重新绘制小部件。

以下示例显示计数器应用程序的一部分

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('+'),
            ),
          ],
        ),
      ),
    );
  }
}

要了解管理状态的更多方法,请查看 状态管理

动画

存在两种主要类型的 UI 动画。

  • 隐式动画:从当前值变为新目标值。
  • 显式动画:在要求时变为动画。

隐式动画

SwiftUI 和 Flutter 采用类似的方法进行动画处理。在这两个框架中,您可以指定 durationcurve 等参数。

SwiftUI 中,您可以使用 animate() 修饰符来处理隐式动画。

Button("Tap me!"){
   angle += 45
}
.rotationEffect(.degrees(angle))
.animation(.easeIn(duration: 1))

Flutter 包含用于隐式动画的小部件。这简化了对常见小部件的动画处理。Flutter 使用以下格式为这些小部件命名:AnimatedFoo

例如:要旋转按钮,请使用 AnimatedRotation 类。这会对 Transform.rotate 小部件进行动画处理。

    AnimatedRotation(
  duration: const Duration(seconds: 1),
  turns: turns,
  curve: Curves.easeIn,
  child: TextButton(
      onPressed: () {
        setState(() {
          turns += .125;
        });
      },
      child: const Text('Tap me!')),
),

Flutter 允许您创建自定义隐式动画。要组合一个新的动画小部件,请使用 TweenAnimationBuilder

显式动画

对于显式动画,SwiftUI 使用 withAnimation() 函数。

Flutter 包含使用格式为 FooTransition 的显式动画小部件。一个示例是 RotationTransition 类。

Flutter 还允许您使用 AnimatedWidgetAnimatedBuilder 创建自定义显式动画。

要详细了解 Flutter 中的动画,请参阅 动画概述

在屏幕上绘制

SwiftUI 中,您可以使用 CoreGraphics 在屏幕上绘制线条和形状。

Flutter 有一个基于 Canvas 类的 API,其中包含两个帮助您进行绘制的类

  1. CustomPaint,需要一个绘图器

        CustomPaint(
       painter: SignaturePainter(_points),
       size: Size.infinite,
     ),
  2. CustomPainter,实现您的算法以绘制画布。

    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;
     }

本部分介绍如何在应用页面之间导航、推送和弹出机制等。

开发者使用称为导航路由的不同页面构建 iOS 和 macOS 应用。

SwiftUI 中,NavigationStack 表示此页面堆栈。

以下示例创建一个显示人员列表的应用。要在新导航链接中显示人员的详细信息,请点击该人员。

NavigationStack(path: $path) {
      List {
        ForEach(persons) { person in
          NavigationLink(
            person.name,
            value: person
          )
        }
      }
      .navigationDestination(for: Person.self) { person in
        PersonView(person: person)
      }
    }

如果您有一个没有复杂链接的小Flutter 应用,请使用带有命名路由的 Navigator。在定义导航路由后,使用其名称调用导航路由。

  1. 在传递给 runApp() 函数的类中命名每个路由。以下示例使用 App

    // Defines the route name as a constant
     // so that it's reusable.
     const detailsPageRouteName = '/details';
    
     class App extends StatelessWidget {
       const App({
         super.key,
       });
    
       @override
       Widget build(BuildContext context) {
         return CupertinoApp(
           home: const HomePage(),
           // The [routes] property defines the available named routes
           // and the widgets to build when navigating to those routes.
           routes: {
             detailsPageRouteName: (context) => const DetailsPage(),
           },
         );
       }
     }

    以下示例使用 mockPersons() 生成人员列表。点击人员会使用 pushNamed() 将人员的详细信息页面推送到 Navigator

        ListView.builder(
       itemCount: mockPersons.length,
       itemBuilder: (context, index) {
         final person = mockPersons.elementAt(index);
         final age = '${person.age} years old';
         return ListTile(
           title: Text(person.name),
           subtitle: Text(age),
           trailing: const Icon(
             Icons.arrow_forward_ios,
           ),
           onTap: () {
             // When a [ListTile] that represents a person is
             // tapped, push the detailsPageRouteName route
             // to the Navigator and pass the person's instance
             // to the route.
             Navigator.of(context).pushNamed(
               detailsPageRouteName,
               arguments: person,
             );
           },
         );
       },
     ),
  2. 定义 DetailsPage 小部件,用于显示每个人的详细信息。在 Flutter 中,您可以在导航到新路由时将参数传递到小部件中。使用 ModalRoute.of() 提取参数

    class DetailsPage extends StatelessWidget {
       const DetailsPage({super.key});
    
       @override
       Widget build(BuildContext context) {
         // Read the person instance from the arguments.
         final Person person = ModalRoute.of(
           context,
         )?.settings.arguments as Person;
         // Extract the age.
         final age = '${person.age} years old';
         return Scaffold(
           // Display name and age.
           body: Column(children: [Text(person.name), Text(age)]),
         );
       }
     }

要创建更高级的导航和路由要求,请使用路由包,例如 go_router

要了解更多信息,请查看 导航和路由

手动返回

SwiftUI中,使用dismiss环境值返回到前一个屏幕。

Button("Pop back") {
        dismiss()
      }

Flutter中,使用Navigator类的pop()函数

TextButton(
  onPressed: () {
    // This code allows the
    // view to pop back to its presenter.
    Navigator.of(context).pop();
  },
  child: const Text('Pop back'),
),

SwiftUI中,使用openURL环境变量打开到另一个应用程序的URL。

@Environment(\.openURL) private var openUrl

// View code goes here

 Button("Open website") {
      openUrl(
        URL(
          string: "https://google.com"
        )!
      )
    }

Flutter中,使用url_launcher插件。

    CupertinoButton(
  onPressed: () async {
    await launchUrl(
      Uri.parse('https://google.com'),
    );
  },
  child: const Text(
    'Open website',
  ),
),

主题、样式和媒体

您可以毫不费力地设置Flutter应用的样式。设置样式包括在浅色和深色主题之间切换、更改文本和UI组件的设计等。本部分介绍如何设置应用的样式。

使用深色模式

SwiftUI中,在View上调用preferredColorScheme()函数以使用深色模式。

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

    CupertinoApp(
  theme: CupertinoThemeData(
    brightness: Brightness.dark,
  ),
  home: HomePage(),
);

设置文本样式

SwiftUI中,使用修饰符函数设置文本样式。例如,要更改Text字符串的字体,请使用font()修饰符

Text("Hello, world!")
  .font(.system(size: 30, weight: .heavy))
  .foregroundColor(.yellow)

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

    Text(
  'Hello, world!',
  style: TextStyle(
    fontSize: 30,
    fontWeight: FontWeight.bold,
    color: CupertinoColors.systemYellow,
  ),
),

设置按钮样式

SwiftUI中,使用修饰符函数设置按钮样式。

Button("Do something") {
    // do something when button is tapped
  }
  .font(.system(size: 30, weight: .bold))
  .background(Color.yellow)
  .foregroundColor(Color.blue)
}

要在Flutter中设置按钮小部件的样式,请设置其子级的样式,或修改按钮本身的属性。

在以下示例中

  • CupertinoButtoncolor属性设置其color
  • Text 小部件的 color 属性设置按钮文本颜色。
child: CupertinoButton(
  color: CupertinoColors.systemYellow,
  onPressed: () {},
  padding: const EdgeInsets.all(16),
  child: const Text(
    'Do something',
    style: TextStyle(
      color: CupertinoColors.systemBlue,
      fontSize: 30,
      fontWeight: FontWeight.bold,
    ),
  ),
),

使用自定义字体

SwiftUI 中,你可以在应用中使用自定义字体,只需两步。首先,将字体文件添加到你的 SwiftUI 项目中。添加文件后,使用 .font() 修饰符将其应用到你的 UI 组件。

Text("Hello")
  .font(
    Font.custom(
      "BungeeSpice-Regular",
      size: 40
    )
  )

Flutter 中,你可以使用名为 pubspec.yaml 的文件来控制你的资源。此文件与平台无关。要将自定义字体添加到你的项目中,请按照以下步骤操作

  1. 在项目的根目录中创建一个名为 fonts 的文件夹。此可选步骤有助于整理你的字体。
  2. 将你的 .ttf.otf.ttc 字体文件添加到 fonts 文件夹中。
  3. 在项目中打开 pubspec.yaml 文件。
  4. 找到 flutter 部分。
  5. fonts 部分下添加你的自定义字体。

     flutter:
       fonts:
         - family: BungeeSpice
           fonts:
             - asset: fonts/BungeeSpice-Regular.ttf
    

将字体添加到你的项目后,你可以像以下示例中那样使用它

        Text(
  'Cupertino',
  style: TextStyle(
    fontSize: 40,
    fontFamily: 'BungeeSpice',
  ),
)

在应用中捆绑图片

SwiftUI 中,你首先将图片文件添加到 Assets.xcassets,然后使用 Image 视图来显示图片。

要在 Flutter 中添加图片,请遵循类似于添加自定义字体的方法。

  1. images 文件夹添加到根目录。
  2. 将此资产添加到 pubspec.yaml 文件中。

     flutter:
       assets:
         - images/Blueberries.jpg
    

添加图片后,使用 Image 小部件的 .asset() 构造函数来显示图片。此构造函数

  1. 使用提供的路径实例化给定的图片。
  2. 从与你的应用捆绑在一起的资产中读取图片。
  3. 在屏幕上显示图片。

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

在应用中捆绑视频

SwiftUI 中,你可以通过两个步骤在你的应用中捆绑一个本地视频文件。首先,你导入 AVKit 框架,然后你实例化一个 VideoPlayer 视图。

Flutter 中,将 video_player 插件添加到你的项目中。此插件允许你从相同的代码库中创建可在 Android、iOS 和网络上运行的视频播放器。

  1. 将插件添加到你的应用中,并将视频文件添加到你的项目中。
  2. 将资源添加到你的 pubspec.yaml 文件中。
  3. 使用 VideoPlayerController 类加载并播放你的视频文件。

要查看完整的演练,请查看 video_player 示例