希望使用 Flutter 编写移动应用的 SwiftUI 开发者应查阅本指南。本指南解释了如何将现有的 SwiftUI 知识应用于 Flutter。

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

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

Flutter 还在 iOS 和 macOS 上运行时对应用行为进行了多项调整。要了解具体方法,请参阅平台适配

本文档可以用作一本食谱,你可以跳到最符合你需求的问题。本指南嵌入了示例代码。通过使用悬停或聚焦时出现的“在 DartPad 中打开”按钮,你可以在 DartPad 上打开并运行一些示例。

概述

#

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

在新标签页中观看 YouTube 视频:“Flutter for iOS developers”

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

视图 vs. 小部件

#

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

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

**Flutter** 将 UI 组件表示为*小部件*。

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

dart
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 中,父小部件可以提供无边界约束。无边界约束将其最大值设置为无穷大。

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

如果子小部件扩展且具有无边界约束,Flutter 将返回溢出警告

dart
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 小部件
  • 社区构建的小部件
  • 你自己的自定义小部件
  • 遵循 Apple 人机界面指南的Cupertino 小部件

在新标签页中观看 YouTube 视频:“Flutter's cupertino library for iOS developers”

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

UI 基础

#

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

入门

#

在 **SwiftUI** 中,你使用 App 来启动你的应用。

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

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

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

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

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

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

dart
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 为应用实现了一个基本的布局结构。

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 小部件。SwiftUI 默认将其视图内容渲染在其中心。Flutter 则并非总是如此。Scaffold 不会在屏幕中心渲染其 body 小部件。要使文本居中,请将其包装在 Center 小部件中。要了解不同小部件及其默认行为,请查看小部件目录

添加按钮

#

在 **SwiftUI** 中,你使用 Button 结构体来创建按钮。

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

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

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

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

水平对齐组件

#

在 **SwiftUI** 中,堆栈视图在设计布局中扮演着重要角色。两个独立的结构允许你创建堆栈

  1. HStack 用于水平堆栈视图

  2. VStack 用于垂直堆栈视图

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

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

**Flutter** 使用 Row 而不是 HStack

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

Row 小部件在 children 参数中需要一个 List<Widget>mainAxisAlignment 属性告诉 Flutter 如何在额外空间中定位子小部件。MainAxisAlignment.center 将子小部件定位在主轴的中心。对于 Row,主轴是水平轴。

垂直对齐组件

#

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

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

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

**Flutter** 使用与上一个示例相同的 Dart 代码,只是它将 Column 替换为 Row

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

显示列表视图

#

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

swift
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 不需要列表项是可识别的。你设置要显示的项目数量,然后为每个项目构建一个小部件。

dart
class Person {
  String name;
  Person(this.name);
}

final List<Person> 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 有一个索引参数,该参数将在零到 itemCount 减一之间。

前面的示例为每个项目返回了一个 ListTile 小部件。ListTile 小部件包括 heightfont-size 等属性。这些属性有助于构建列表。但是,Flutter 允许你返回几乎任何代表你数据的小部件。

显示网格

#

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

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

dart
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 实例。

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

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

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

响应式和自适应设计

#

在 **SwiftUI** 中,你使用 GeometryReader 来创建相对视图大小。

例如,你可以

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

你还可以使用 horizontalSizeClass 查看大小类是否为 .regular.compact

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

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

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

状态管理

#

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

swift
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() 以告知框架重新绘制小部件。

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

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

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

动画

#

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

  • 隐式动画:从当前值动画到新目标。
  • 显式动画:在被要求时进行动画。

隐式动画

#

SwiftUI 和 Flutter 采用相似的动画方法。在这两个框架中,你都指定了诸如 durationcurve 之类的参数。

在 **SwiftUI** 中,你使用 animate() 修饰符来处理隐式动画。

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

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

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

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

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

显式动画

#

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

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

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

要了解有关 Flutter 中动画的更多信息,请参阅动画概述

在屏幕上绘图

#

在 **SwiftUI** 中,你使用 CoreGraphics 在屏幕上绘制线条和形状。

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

本节解释了如何在应用页面之间导航、推入和弹出机制等等。

#

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

在 **SwiftUI** 中,NavigationStack 代表了这种页面堆栈。

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

swift
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

    dart
    // 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

    dart
    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() 提取参数

    dart
    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 环境值来返回到上一个屏幕。

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

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

dart
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 到另一个应用程序。

swift
@Environment(\.openURL) private var openUrl

// View code goes here

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

在 **Flutter** 中,使用 url_launcher 插件。

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

主题、样式和媒体

#

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

使用深色模式

#

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

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

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

文本样式

#

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

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

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

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

按钮样式

#

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

swift
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 属性设置按钮文本颜色。
dart
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 组件。

swift
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 部分下添加你的自定义字体。

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

将字体添加到你的项目后,你可以按照以下示例使用它

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

在应用中捆绑图片

#

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

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

  1. 在根目录中添加一个 images 文件夹。

  2. 将此资产添加到 pubspec.yaml 文件。

    yaml
    flutter:
      assets:
        - images/Blueberries.jpg

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

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

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

在应用中捆绑视频

#

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

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

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

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