likes
comments
collection
share

SwiftUI 工程师的 Flutter 指南

作者站长头像
站长
· 阅读数 6

本文翻译自 Flutter for SwiftUI Developers | Flutter

介绍

  • 本文为SwiftUI工程师入门 Flutter专用。介绍了如何将SwiftUI 经验应用于 Flutter 中。
  • Flutter 是一个用于构建跨平台App的框架,它使用Dart语言。

概述

Flutter 和 SwiftUI 使用纯代码描述 UI 的外观和工作方式。这种代码被称为声明式UI

View 与 Widget

SwiftUI 将 UI 组件表示为View

Text("Hello, World!") // 这是一个 View
  .padding(10)        // View 的属性

Flutter 将 UI 组件表示为Widget

Padding(                         // 这是一个 Widget
  padding: EdgeInsets.all(10.0), // Widget 的属性
  child: Text("Hello, World!"),  // 子 Widget
)));

SwiftUI 嵌套视图,而 Flutter 嵌套 Widget, 层层嵌套形成树形结构。

布局过程

SwiftUI 布局过程

  • 第一阶段 —— 讨价还价
  1. 父View为子View提供建议尺寸
  2. 子view根据自身的特性,返回一个size
  3. 父view根据子view返回的size为其进行布局
  • 第二阶段 —— 布局到屏幕上

父View根据布局系统提供的屏幕区域为子视图设置渲染的位置和尺寸。此时,视图树上的每个View都将与屏幕上的具体位置联系起来。

Flutter 布局过程

  1. 父节点向子节点传递约束信息,限制子节点的最大和最小宽高
  2. 子节点根据自己的约束信息来确定自己的大小(Szie)。
  3. 父节点根据特定的规则(不同的组件会有不同的布局算法)确定每一个子节点在父节点空间中的位置,用偏移 offset表示。
  4. 递归整个过程,确定每一个节点的位置和大小。

可以看到,组件的大小是由自身来决定的,而组件的位置是由父组件来决定的

UI基础知识

下面介绍 UI 开发的基础知识,并将Flutter和SwiftUI进行对比。

SwiftUI启动App

@main
struct MyApp: App { // App 对象
    var body: some Scene {
        WindowGroup {
            HomePage()
        }
    }
}
struct HomePage: View { // 显示首页
  var body: some View {
    Text("Hello, World!")
  }
}

Flutter启动App

void main() {
  runApp(const MyApp()); // App对象
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    // CupertinoApp,使用iOS风格的控件
    return const CupertinoApp(
      home: HomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
        child: Text(
          'Hello, World!',
        ),
      ),
    );
  }
}

默认情况下,SwiftUI 的View默认居中。 Flutter使用Center组件包装让文本居中。

添加按钮

SwiftUI使用Button创建按钮。

Button("Do something") {
  // 按钮点击回调
}

为了在 Flutter 中达到相同的结果, 使用类:CupertinoButton

CupertinoButton(
  onPressed: () {
    // 按钮点击回调
  },
  child: const Text('Do something'),
)

水平对齐组件

SwiftUI

  • HStack创建水平Stack视图
  • VStack创建垂直Stack视图
HStack { // 添加图像和文本到HStack中
  Image(systemName: "globe")
  Text("Hello, world!")
}

Flutter

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

垂直对齐组件

SwiftUI

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

Flutter

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

列表视图(List View)

SwiftUI

struct Person: Identifiable { // Identifiable 唯一标识model
  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 {              // List 表示一组项目
      ForEach(persons) { person in
        Text(person.name)
      }
    }
  }
}

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( // build函数构造ListView
        itemCount: items.length, // 子项目的数量
        itemBuilder: (context, index) { // 每一项的内容
          return ListTile(
            title: Text(items[index].name),
          );
        },
      ),
    );
  }
}

Grid 视图

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

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( // GridView构造器
        // 设置GridView参数
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3, // 每行显示数量
          mainAxisExtent: 40.0,  // item高度
        ),
        itemCount: widgets.length, // item总数量
        itemBuilder: (context, index) => widgets[index],
      ),
    );
  }
}

创建 Scroll View

SwiftUI

ScrollView { // 定义滚动视图
  VStack(alignment: .leading) {
    ForEach(persons) { person in // 子视图
      PersonView(person: person)
    }
  }
}

Flutter 使用 SingleChildScrollView。

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

管理状态

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

struct ContentView: View {
  @State private var counter = 0;
  var body: some View {
    VStack{
      Button("+") { counter+=1 } // @State属性改变, Text会自动刷新
      Text(String(counter))
    }
  }}

Flutter 使用 StatefulWidgetState管理状态。

State存储Widget的状态。 要更改Widget的状态,使用setState()告诉Flutter重绘Widget

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

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});
  @override
  State<MyHomePage> createState() => _MyHomePageState(); // 创建State
}

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(() { // setState通知Text刷新
                _counter++;
              }),
              child: const Text('+'),
            ),
          ],
        ),
      ),
    );
  }
}

动画

存在如下两种类型的 UI 动画。

  • 隐式动画

SwiftUI

Button(“Tap me!”){
   angle += 45
}
.rotationEffect(.degrees(angle))
.animation(.easeIn(duration: 1)) // 使用animation函数处理隐式动画。

Flutter 有专门处理隐式动画的Widget(Animated+XXX)。

AnimatedRotation( // 旋转动画
  duration: const Duration(seconds: 1),
  turns: turns,
  curve: Curves.easeIn,
  child: TextButton(
      onPressed: () {
        setState(() {
          turns += .125;
        });
      },
      child: const Text('Tap me!')),
),
  • 显式动画

    • SwiftUI 使用withAnimation()函数
    • Flutter 使用专门的Widget如RotationTransition(XXX+Transition)

在屏幕上绘图

  • SwiftUI使用CoreGraphics框架绘制线条和形状
  • Flutter使用 CustomPaintCustomPainter进行绘制
CustomPaint(
   painter: SignaturePainter(_points),
   size: Size.infinite,
 ),

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

Navigation(导航)

本部分介绍如何在App的页面之间navigate(导航)pushpop。 开发人员使用称为navigation routes在不同页面跳转

在 SwiftUI 中,使用NavigationStack表示页面栈

下面的示例创建一个显示人员列表的应用。点击那个人在新的NavigationLink中显示人员的详细信息。

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

Flutter的命名路由

// 定义 route name 为常量,方便复用
 const detailsPageRouteName = '/details';

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

   @override
   Widget build(BuildContext context) {
     return CupertinoApp(
       home: const HomePage(),
       // routes属性定义可用的命名路由和跳转的目标Widget
       routes: {
         detailsPageRouteName: (context) => const DetailsPage(),
       },
     );
   }
 }

下面的示例: 点击某人会push到此人的详细信息页面, 使用 Navigator pushNamed()

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: () { // 为ListTitle添加点击事件
         // 将detailsPageRouteName命名路由push给Navigator,并将参数person传递给route。
         Navigator.of(context).pushNamed(
           detailsPageRouteName,
           arguments: person,
         );
       },
     );
   },
 ),
class DetailsPage extends StatelessWidget {
   const DetailsPage({super.key});

   @override
   Widget build(BuildContext context) {
     final Person person = ModalRoute.of( // ModalRoute.of 读取参数
       context,
     )?.settings.arguments as Person;
     // 读取person的年龄属性
     final age = '${person.age} years old';
     return Scaffold(
        // 显示名字和年龄
       body: Column(children: [Text(person.name), Text(age)]),
     );
   }
 }

要创建更高级的导航和路由要求, 使用route package,例如go_router。

手动pop返回

在 SwiftUI 中,使用dismiss方法返回上一个界面

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

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

TextButton(
  onPressed: () {
    // pop back到它的presenter
    Navigator.of(context).pop();
  },
  child: const Text('Pop back'),
),

导航到其他App

在 SwiftUI 中,使用环境变量@Environment打开一个指向其他App的URL。

@Environment(\.openURL) private var openUrl

 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 App的样式。

样式包括:

  • 在浅色和深色主题之间切换,
  • 更改文本和 UI 组件的设计。

使用深色模式

  • 在 SwiftUI 中,在View上调用函数preferredColorScheme()以使用深色模式
  • 在 Flutter 中,在App级别控制明暗模式。
    CupertinoApp(
      theme: CupertinoThemeData(
        brightness: Brightness.dark, // 深色模式
      ),
      home: HomePage(),
    );

设置文本样式

在 SwiftUI 中, 使用font()函数更改Text的字体

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

在 Flutter 中,使用TextStyle设置文本样式

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 中,设置child的样式,或修改按钮本身的属性。

child: CupertinoButton(
  color: CupertinoColors.systemYellow, // 设置按钮的背景色
  onPressed: () {},
  padding: const EdgeInsets.all(16),
  child: const Text( // 设置它的child节点Text的样式来修改按钮的文本样式
    'Do something',
    style: TextStyle(
      color: CupertinoColors.systemBlue,
      fontSize: 30,
      fontWeight: FontWeight.bold,
    ),
  ),
),

使用自定义字体

SwiftUI

Text("Hello")
  .font(
    Font.custom( // 使用自定义字体"BungeeSpice-Regular"
      "BungeeSpice-Regular",
      size: 40
    )
  )

在 Flutter 中,使用pubspec.yaml文件定义App资源, 此文件是跨平台的。添加字体包含如下步骤:

  1. 创建fonts文件夹来组织字体。
  2. 添加.ttf .otf .ttc样式文件到文件夹。
  3. 打开pubspec.yaml
  4. 在flutter的fonts结构下添加自定义字体
 flutter:
   fonts:
     - family: BungeeSpice
       fonts:
         - asset: fonts/BungeeSpice-Regular.ttf
Text(
  'Cupertino',
  style: TextStyle(
    fontSize: 40,
    fontFamily: 'BungeeSpice', // 使用刚添加的BungeeSpice字体
  ),
)

App显示图像

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

  • 在 Flutter 中添加图像,和添加自定义字体类似。

    1. 添加images文件夹到根目录。
    2. 添加asset到pubspec.yaml中
 flutter:
   assets:
     - images/Blueberries.jpg

添加图像后,使用Image.asset()显示它。

在App播放视频

  • SwiftUI: 使用 AVKitVideoPlayer
  • Flutter: 使用 video_player插件