本文翻译自
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插件