跳到主内容

面向 SwiftUI 开发者的 Flutter

学习如何将 SwiftUI 开发知识应用于构建 Flutter 应用。

想要使用 Flutter 编写移动应用的 SwiftUI 开发者应该阅读本指南。它解释了如何将现有的 SwiftUI 知识应用到 Flutter 中。

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

你在 SwiftUI 方面的知识和经验在构建 Flutter 应用时非常有价值。

Flutter 在 iOS 和 macOS 上运行时也对应用行为进行了一些调整。要了解具体方式,请参阅 平台适配

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

概述

#

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

在新标签页中观看 YouTube 视频:“面向 iOS 开发者的 Flutter”

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

视图 (Views) vs. 组件 (Widgets)

#

SwiftUI 将 UI 组件表示为视图 (views)。你可以使用修饰符 (modifiers) 来配置视图。

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

Flutter 将 UI 组件表示为组件 (widgets)

视图和组件都只存在于需要改变之前。这些语言将此属性称为不可变性 (immutability)。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 组件
  • 社区构建的组件
  • 你自己的自定义组件
  • Cupertino 组件(遵循 Apple 的《人机界面指南》)

在新标签页中观看 YouTube 视频:“面向 iOS 开发者的 Flutter Cupertino 库”

如果你正在寻找一个采用自定义设计系统的优秀参考应用,请查看 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 代码,只是将 Row 替换为 Column

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 组件有一个 builder 方法。这类似于 SwiftUI 的 List 结构体中的 ForEach

  • ListViewitemCount 参数设置 ListView 显示多少项。

  • itemBuilder 有一个 index 参数,其范围在 0 到 itemCount 减 1 之间。

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

显示网格

#

SwiftUI 中构建非条件网格时,你使用带有 GridRowGrid

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 = <Widget>[
  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 采用相似的动画方法。在这两个框架中,你都需要指定 duration(持续时间)和 curve(曲线)等参数。

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(需要一个 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;
    }
    
#

本节解释如何在应用页面之间导航、推送 (push) 和弹出 (pop) 机制等。

#

开发者使用不同的页面构建 iOS 和 macOS 应用,这些页面称为导航路由 (navigation routes)

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 the button is tapped.
}
.font(.system(size: 30, weight: .bold))
.background(Color.yellow)
.foregroundColor(Color.blue)

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

在以下示例中:

  • CupertinoButtoncolor 属性设置其颜色。
  • 子组件 Textcolor 属性设置按钮文本的颜色。
dart
child: CupertinoButton(
  color: CupertinoColors.systemYellow,
  onPressed: () {},
  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 示例