[Flutter翻译]给SwiftUI开发的Flutter介绍

668 阅读12分钟

本文由 简悦SimpRead 转码,原文地址 www.alwaysrightinstitute.com

在过去的几个月里,Flutter作为一个跨平台的UI框架获得了一些人气。我们将有一个......

在过去的几个月里,Flutter作为一个跨平台的UI框架获得了一些人气。我们将从SwiftUI开发者的角度来看看它,因为它有一个相当类似的方法来构建用户界面,"声明式 "的。

这将是一篇长篇大论的文章,涉及各个方面。请随意跳转。

免责声明:Flutter的知识是在玩了大约一周后才有的。欢迎将更正内容发送到me@helgehess.eu

简介

看到 "XP套件 "来来去去超过25年(是的,包括YellowBox for Windows, XULSwing),我普遍认为,历史证明,使用跨平台的UI 框架几乎是错误的™️方法。 它们往往会在所有的平台上产生不一样的外观和感觉,很少与平台的功能整合,往往是呆板的,而且 不能提供更低的开发成本。

但是,玩玩东西,看看东西是如何结合在一起的,还是很有趣的🤓Flutter对SwiftUI开发者来说相当有趣,因为它非常相似。几乎感觉就像苹果公司雇佣了原来的Flutter开发者来制作一个 "更好 "的版本。

因此,我对这种炒作很感兴趣,并花了一些时间建立了一个小的Flutter应用程序,以了解它是如何进行的,以及它与SwiftUI的对比情况。

哦,需要记住的是。Flutter是一个谷歌项目

作为预览,这是一个简单的Flutter应用程序的样子,我们会在稍后中介绍所有的细节。

import 'package:flutter/material.dart'; // import module

void main() {            // the Dart app entry point
  runApp(const MyApp()); // const is funny, later!
}

class MyApp extends StatelessWidget { // Application
  const MyApp({Key? key}) : super(key: key); // named parameter

  @override // var body: some View
  Widget build(BuildContext context) { // ctx is the Environment
    return MaterialApp( // use the ugly Material styling
      title: 'Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const MyHomePage(title: 'Demo')
    ); // notice all the `;`? required!
  }
}

class MyHomePage extends StatefulWidget { // a View
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title; // non @State View ivar, final is let

  @override  // A `StatefulWidget`, State is extra
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> { // generics
  int _counter = 0; // roughly an ObservableObject ivar

  @override // var view: some View
  Widget build(BuildContext context) {
    return Scaffold( // ~NavigationView
      appBar: AppBar(title: Text(widget.title)), // .navigationTitle...
      body: Center(child: Column( // ~ZStack(VStack(...))
        mainAxisAlignment: MainAxisAlignment.center, // VStack(alignment:)
        children: [
          const Text('Count:'),
          Text('$_counter') // String interpolation
        ]
      )),
      floatingActionButton: FloatingActionButton(
        onPressed: () { // closure
          setState(() { // objectWillChange.send()
            _counter++; // ++!!
          });
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add)
      )
    );
  }
}

这是更多的模板和语法,但非常接近于在SwiftUI中的做法。

设置

使用Homebrew安装Flutter是很简单的(而且似乎不会使系统变得很混乱或做一些意想不到的事情)。

$ brew install flutter

由于我们将使用Visual Studio Code作为 "IDE",这可能是有用的。

$ brew install --cask visual-studio-code

我在这篇文章中看了一下Swift的VSCode,如果需要介绍的话。似乎没有一个漂亮的Mac IDE用于Flutter,也许是Nova?VSCode很难看,但可以忍受。

有一个flutter doctor命令,可以用来检查Flutter的安装是否顺利。

$ flutter doctor
[✓] Flutter (Channel stable, 3.0.1, on macOS 12.4 21F79 darwin-arm, locale en-DE)
[✗] Android toolchain - develop for Android devices
...

要创建一个新的项目,需要使用flutter create

$ flutter create cowtastic
Signing iOS app for device deployment using developer identity: "Apple Development: Helge Heß (ABC12DEFGH)"
Creating project cowtastic...
Running "flutter pub get" in cowtastic...                          918ms
Wrote 127 files.

All done!
In order to run your application, type:

  $ cd cowtastic
  $ flutter run

Your application code is in cowtastic/lib/main.dart.

是的,不开玩笑,这产生了一个不少于65个目录和132个文件的文件夹。唯一让我们感兴趣的是main.dart,住在lib子文件夹中。

这已经可以在命令行上运行,但直接进入Visual Studio Code更有用。有两个扩展似乎很有用,应该安装(使用左边的扩展标签)。FlutterPubspec Assist

这将是它的样子。

image.png

当第一次启动该应用程序时,macOS的签名问题就出现了。如果谷歌(和Homebrew)被信任,请在macOS偏好设置中确认这一点。

image.png

要选择你想在设备上、模拟器上或作为 "Mac应用程序 "运行该应用程序,点击右下方的目标。然后为模拟器选择 "启动iOS模拟器"(或坚持在设备上)。

image.png

这将只是启动模拟器。

F5构建并启动应用程序(并可能带来macOS安全面板,直到事情被授予)。启动应用程序需要一些时间,因为事情正在进行xcodebuild'。

image.png

恭喜你,第一个Flutter应用程序正在运行。

你会注意到,对于一个iOS应用来说,该应用看起来很不对劲。这是因为所有的东西都默认为谷歌的 "Material "主题(大部分的教程都依赖于它)。它可以被改变成更像UIKit的样子,我们会得到

基本概念

乍一看可能很像Java,但Flutter应用程序实际上是用一种自己的编程语言写的,叫做Dart 。它还不错,没有那么严格。Swift看起来更漂亮,有更多的便利性。Dart的编译速度快得多--简直是**就能保存。而且有一个合适的GC,没有ARC。一些语言的例子如下如下

Flutter本身就是一个上面的框架。Flutter的关键概念是Widget,它与SwiftUI的View本质上是一样的(尽管它也用于ViewModifier的,App,一切)。他们的口号是。

Flutter, where Everything is a Widget™

我想知道,在SwiftUI上,人们普遍希望把所有东西都强行纳入Views,是否源于此。

与SwiftUI不同的是,一个Widget本身并没有真正的状态,它是不可改变的。它可以是一个StatefulWidget,但那些仍然没有携带状态_在Widget内部,而是在一个单独的,State<Widget>对象中。与我们的ViewController很相似。 说到对象,Dart中的所有东西都是类和对象(尽管通常是不可变的或const),没有结构。

比较一下HelloWorld应用程序

introduction中,我已经展示了flutter create生成的模板应用程序。这是一个简单的屏幕,有一个文本,显示一个计数器,和一个按钮来增加这个计数器。因为它必须存储当前的计数,所以涉及到一个StatefulWidget

image.png

image.png

请注意,右边的SwiftUI应用程序在默认情况下看起来很正常,而左边的Flutter应用程序看起来像一个Android的东西。我们稍后会看一下那个

这就是同样的应用程序在SwiftUI中的大致情况。

import SwiftUI

@main
struct MyApp: App {
  var body: some Scene {
    WindowGroup {
      MyHomePage(title: "SwiftUI Demo")
    }
  }
}

struct MyHomePage: View {
  
  let title : String
  @State private var counter = 0
  
  var body: some View {
    NavigationView {
      VStack(alignment: .center) {
        Text("You have pushed the button this many times:")
        Text("\(counter)")
          .font(.title)
      }
      .navigationTitle(title)
      .toolbar {
        Button(action: { counter += 1 }) {
          Label("Increment", systemImage: "plus.circle")
        }
      }
    }
  }
}

让我们比较一下不同的部分。

应用程序设置

在两种环境中都有一个主入口点,应用程序本身就是在这里配置的。它在Dart中只是一个main函数,而Swift在2020年得到了神奇的@main注解。

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'Demo',
    theme: ThemeData(primarySwatch: Colors.blue),
    home: const MyHomePage(title: 'Demo')
  );
}

SwiftUI。

import SwiftUI

@main
struct MyApp: App {
  var body: some Scene {
    WindowGroup {
      MyHomePage(title: "Flutter Demo Home Page")
    }
  }
}

Flutter有更多的模板,它有点像你如何在一种你不控制的语言中写一个类似SwiftUI的框架(很奇怪,因为它似乎与Dart紧密耦合)。 Swift得到了一套特定的语言增强功能来支持SwiftUI,包括Property WrappersResult Builders。这些让SwiftUI的源码看起来更漂亮,但不一定更容易跟上。

如前所述,Flutter使用Widget来表示一切,而SwiftUI有更具体的类型,如App,但想法是一样的。

Widget build(BuildContext context) => MaterialApp(...);

在SwiftUI中,build函数是一个计算的`体'属性。

var body: some Scene { ... }

在Flutter中,build函数随身携带一个BuildContext,它与SwiftUI的Environment相当相似。例如,它被用来传递FontStyle的、主题和类似于@EnvironmentObject的东西。

MyHomePage顶层小部件

页面的设置比较有趣(在SwiftUI中,"根视图 "通常被称为 "内容视图",但我选择了 "MyHomePage "以符合Flutter的设置)。

状态

在Flutter中,"状态 "是与Widgets分开处理的,而在SwiftUI中,视图的 "配置 "和它的 "状态 "可以混合在一个实体中。后者使得演示更加紧凑(如图),但一旦应用程序开始增长,通常会导致意大利语代码。当在SwiftUI中干净利落地做事情时,它们最终看起来相当相似。

所以在Flutter中,MyHomePage是一个StatefulWidget

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title; // final is let

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

注意,"StatefulWidget "并不意味着widget本身携带状态,它仍然是不可改变的 "配置"(title参数)。相反,状态被推送到一个单独的State对象中。

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) { ... }
}

非常类似于人们在干净的SwiftUI代码中所做的,并经常称之为 "ViewModels"(实现为ObservableObject的)。 让我觉得有点惊讶的是,State本身也有一个build函数,而不是相关的widget有一个build函数,可以进入State。这与我们的ViewController的做法非常相似。State就是这样一个ViewController,它是一个非常具体的ObservableObject,它有一个View附件,它被控制着。

这就是SwiftUI中的普通版本的样子。

struct MyHomePage: View {
  
  let title : String
  @State private var counter = 0
  
  var body: some View { ... }
}

它携带了title配置以及counter@State。这比Flutter版本的两个对象要紧凑得多,但它也增加了很多神奇的、经常是意想不到的行为。@State本质上是将View的 "结构 "变成一个对象(即它得到一个 "身份")。 在这里,我实际上更喜欢Flutter的设置,即状态有它自己的身份,如前所述,无论如何,这就是你在SwiftUI中的最终结果。

在Widget/View中的状态管理还有一个方面,就是状态如何被修改。

Flutter。

onPressed: () {
  setState(() { 
    _counter++;
  }
}

SwiftUI。

action: { counter += 1 }

这在SwiftUI中要紧凑得多,因为@State属性包装器看到了修改,并自动标记View进行重新评估(与@Published类似)。 在Flutter中,修改必须被包裹在一个setState()的调用中,它告诉系统事情发生了明确的变化(估计它做了快照和差异化作为其中的一部分)。

页面布局

image.png

image.png

build/body的实现方式又很相似。有两个显著的区别。

  • SwiftUI使用Result Builders,本质上是一种不同的语言语法,对源代码的美观和性能都有一些好处(它们保留了View树的全部静态类型)。 Flutter只是在常见的构建器模式中使用常规的函数调用。
  • Flutter通常不使用ViewModifier.padding(虽然我认为那是可能的,只是不习惯),而是在Widgets中包裹Widgets(例如在Container中进行padding)。
Widget build(BuildContext context) => Scaffold(
  appBar: AppBar(title: Text(widget.title)),
  body: Center(child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      const Text('Count:'),
      Text('$_counter')
    ]
  )),
  floatingActionButton: FloatingActionButton(
    onPressed: () => setState(() { _counter++; }),
    tooltip: 'Increment',
    child: const Icon(Icons.add)
  )
);

这只是调用了各自部件的构造器,参数是子部件,有时是特定部分的子部件。没有尾部闭包或结果构建器,因此子视图在widget的 "init调用 "的参数中存在。

使用的小组件。

  • Scaffold 是一个来自 "Material "库的部件。它基本上提供了布局,包括标题栏、浮动按钮和页面内容。有点类似于SwiftUI中的NavigationView
  • AppBar 是一个绘制实际标题栏的小部件。类似于 .navigationTitle 修改器和同伴。
  • Center只是将其单一的Widget置于可用空间的中心,有点像只有一个子的ZStack,尽管它也 "扩展"(占用可用空间)。
  • Column是一个VStack,它的mainAxisAlignment说明了内容在垂直方向上的分布,与.layoutPriority有点关系。crossAxisAlignmentVStackalignment参数相同(使用start而不是leadingend而不是trailing,注意该枚举需要拼写出来,只是.center不够)。
  • Text,这就像Text。虽然插值(Text('$_counter))是由语言完成的,而不是由Text本身完成的。也就是说,它可能不能做SwiftUI能做的本地化事情。
  • 其他的容器是。
    • Row,它是HStack
    • Container,用于添加填充和装饰,如边框、背景颜色。

在SwiftUI中大致相同。

var body: some View {
  NavigationView {
    VStack(alignment: .center) {
      Text("You have pushed the button this many times:")
      Text("\(counter)")
        .font(.title)
    }
    .navigationTitle(title)
    .toolbar {
      Button(action: { counter += 1 }) {
        Label("Increment", systemImage: "plus.circle")
      }
    }
  }
}

我认为SwiftUI肯定看起来更漂亮,但Flutter的东西也很合理。而且不需要任何额外的语言功能。

中场休息。Flutter中很好的东西。

已经说了很多了,我们来个视频吧。Flutter真正伟大的地方是迭代的速度,眼见为实。

zeezide.de/videos/flut…

这个应用程序最初是用F5启动的,这需要一个时间。但你很少这样做。一旦启动,你只需编辑一个源文件(这里我们把主题颜色从蓝色改为绿色),保存文件,💥嘣,应用程序就在模拟器中更新了。 而且它只是工作。就像_总是_一样。它不像Xcode预览那样,除了最简单的情况,它是一个火车头(我倾向于创建自己的Xcode项目只是为了预览,当需要迭代一个视图的一些视觉效果时)。

这一部分是Flutter开发中令人难以置信的优点。Swift在这里有困难,因为它是非常静态的,而且速度惊人的慢。我们将看到WWDC 2022是否带来改进。我有点怀疑。

另一件在Flutter中很好,但在SwiftUI中很糟糕的事情是文档。不要误会,SwiftUI文档非常漂亮。它做得不好的地方是记录了实际的SwiftUI开发(可能是因为它是由从未写过真正的SwiftUI应用的人写的)。 而符号的文档,可用性接近zarro🙈

Flutter文档则相反。有点难看,但非常有帮助,而且总是直指要害。感觉它是由真正使用过Flutter的开发者编写的,知道开发者在寻找什么信息。从入门,到类文档,再到烹饪手册。很好的东西。

我认为这两样东西是Flutter不飘逸而真正飞翔的地方。但让我们回到技术层面。

Dart和Swift

如果你以前用过C或Java语言,那就很简单了。下面是对Dart语法的一个小概述。

你必须使用分号,来自Swift的我经常忘记它们。

onPressed: () {
  setState(() {
    _counter++; // <= semicolon!
  });           // <= another semicolon!
},

也显示了Dart的闭包,上面两个闭包没有参数。

( parameters ) { code }

它不像Swift那样有尾部闭包,例如,你不能这样写。

setState { // 在Dart中是不存在的
  _counter++
}
setState(() { //需要在参数列表中出现
  _counter++;
});

有很多这样的小语法,让Swift看起来更漂亮、更干净,尽管很多时候它实际上让人更难读懂。

你还必须在返回值的块中使用return,例如,这将返回一个新的Widget(读作:View)。

Widget build(BuildContext context) {
  return MaterialApp(...);
}

虽然有一个类似于JavaScript的快捷语法,如果函数只是一个单一的表达式。我发现它经常适用,而且一般都很好看。

Widget build(BuildContext context) => MaterialApp(
  ...
); // <= semicolon needed here!

还显示:函数和参数及变量的返回类型是在名称前拼写的,而不是在名称后。就像在C/Java中一样。也不需要func关键字。例如,上述内容在Swift中会是这样的。

func build(_ context: BuildContext) -> Widget {
  MaterialApp(...)
}

Dart和Swift一样,都有位置参数和命名参数。上面显示的是位置参数,即build是这样调用的。

widget.build(context);

而不是像这样(这在Swift中是默认的,没有_占位符)。

widget.build(context: context); // wrong for positional

命名的参数声明有一个有点奇怪的语法。

key是参数名称,Key?是类型。

这给我们带来了Optionals (Key? - 一个可选的Key)。目前的Dart有nullability和nullability检查的概念,但做得不那么 "当面"。一般来说,像Smalltalk一样,Dart中所有的值都是对象(并允许有null的变量)。这甚至包括像int这样的东西。

final int  a = 42;   // pushing null raises error
final int? b = null;
if (b != null) return b!;
final int  c = b!;   // force unwrap is there, error throws
final d      = c;    // "auto" also available

final和Swift中的let是一样的。

let a : Int  = 42;   // not possible to set to `nil`
let b : Int? = nil;
if let b { return b }
let c : Int  = b!;   // runtime crash
let d = c

注意Dart要求在if条件(和for等)中使用小括号(),但允许不使用大括号{}的单语句块(if (x) return 42;)。

如前所述,Dart没有像Swift那样的Result Builders

VStack {
  Text("Hello")
  Text("World")
}

这是一个聪明但复杂的东西,在其他地方有解释,相反,事情是用常规调用来构建的。

Row(children: [
  Text("Hello"),
  Text("World")
])

从编码的角度来看,这并不是那么可怕的。

我不想过多地讨论泛型,但一般来说,在SwiftUI中你会更经常地与它们打交道(有一些好的理由)。例如,像AnyView这样的东西在Flutter中是不必要的,你只需使用Widget基类。

Dart有stdlib函数,如map,它返回一个Iterable。同样,需要处理的通用模板也比较少。样本。

_saved.map( (pair) => Text(pair.asPascalCase));

Swift的Any类型在Flutter中被称为dynamic。例如,这个Swift:

let json : [ String : Any ]
let json : Dictionary<String, Any> // or this

在Dart中看起来像这样。

final Map<String, dynamic> json;

为了铸造,Dart有isas

List<SPIPackage> _decodeJSON(dynamic json) {
  final jsonDict = json as Map<String, dynamic>;
  final results  = jsonDict["results"] as List<dynamic>;

  for (final result in results) {
    final json = result as Map<String, dynamic>;
    if (json["package"] == null) continue;

处理类似JSON结果的JSONSerialization的方法。如果类型不匹配,就会在运行时出错而失败。"深度转换 "不像在Swift中那样工作。例如,这样做是行不通的。

final typed = json as Map<String, Map<String, List<Map<String, dynamic>>>>

铸造必须一步步完成。

(感谢上帝)Dart中没有Coddable。虽然它也禁用了运行时反射,导致开发者不得不手动操作(我认为这是好的)。

factory SPIPackage.fromJson(Map<String, dynamic> json) => SPIPackage(
  packageId      : json['packageId'],
  repositoryName : json['repositoryName'],
  ...
);

...或者求助于代码生成(我不喜欢这样,代码生成是错误的™️)。在这种情况下,这也是一个隐式的as'转换,到[String`](api.dart.dev/stable/2.17…

class SPIPackage {
  final String packageId;
  final String repositoryName;
  ...
}

final class不起作用。你也不能将一个类嵌套在另一个类中。

async/await,但它是基于Futurecompute函数在一个isolate中开始工作,这似乎有点像Swift的actor。一个例子。

class _SPMListPageState extends State<SPMListPage> { 
  late Future<List<SPIPackage>> _packages; // late is like lazy

  Future<List<SPIPackage>> _querySPI(String query) async { // <= async!
    final response = await http.get( // <= await
      Uri(scheme: "https",  host: "swiftpackageindex.com",
          path: "/api/search", 
          queryParameters : { "query": query })
    );

    if (response.statusCode == 200) {
      return _decodeJSON(jsonDecode(response.body));
    }
    else {
      throw Exception("Failed to run query! ${response.statusCode}");
    }
  }
  ...
}

这如何整合到Flutter Widgets?一切都是widget! FutureBuilder:

Widget _buildResultView(BuildContext context) =>
  FutureBuilder<List<SPIPackage>>(
    future: _packages,
    builder: ( ctx, snapshot ) {
      if (snapshot.hasData) {
        return _buildResultList(context, snapshot.data as List<SPIPackage>);
      }
      else if (snapshot.hasError) {...}
      else { PlatformCircularProgressIndicator()); }
    }
  );

Swift的typealias在Dart中也是可用的,称为typedef,来自UXKit.dart

typedef UXApp        = PlatformApp;
typedef UXScaffold   = PlatformScaffold;
typedef UXAppBar     = PlatformAppBar;
typedef UXIconButton = PlatformIconButton;

另一个有趣的事情,细心的读者可能已经注意到了,到处都有_的前缀,比如。

late Future<List<SPIPackage>> _packages;
Widget _buildResultView(BuildContext context)

这正是你所想的:_前缀标志着事物是内部的/私有的。没有的符号是公共的。例如,"StatefulWidget "的 "State "通常是私有的,以_开头。

我认为这不是最差的方法,因为它消除了杂乱无章的现象,而且很明显。

一些数据类型。

final answer         = 42;            // an int
final isIt           = "1337";        // a String
final setOfWordPairs = <WordPair>{};  // a Set
final wordPairs      = <WordPair>[];  // a List, Array in Swift
final answers = { 42: "The Answer" }; // a Map,  Dictionary in Swift

final answer = answers[42];

字符串有插值功能,但不是Swift的可定制子系统

final singleVar = Text('$answer');
final exprPath  = Text("${answers[42]}");

完成Dart的概述,它有一个有趣的const结构,你经常看到。

Column(children: [
  const Text('Count:'),
  Text('$_counter')
]);

它有点像一个callite singleton,确保 "常量对象 "只有一个实例存在。例如,在这种情况下,没有变量引用的Text是一个编译时的常量,每次都不需要。这种 "常量 "可以通过层次结构传播,就像这样。

const Column(children: [
  Text('Hello'),
  Text('World')
]);

VSCode告诉你是否应该添加一个const

软件包

🐄 是时候添加一些牛了! 🐮

Dart自带一个包系统,并且有一个官方的包注册表。pub.dev。Swift有SPM和(非官方但很好的)Swift Package Index

Package.swift是Swift的,pubspec.yaml是Dart的。

name: cowtastic
description: A new Flutter project.
version: 1.0.0+1

environment:
  sdk: ">=2.17.1 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2

dev_dependencies:
  flutter_test:
    sdk: flutter
flutter:
  uses-material-design: true

应该是不言自明的。

可以手动添加软件包,或者如果安装了Pubspec Assist VSCode扩展。 使用它(⌘⇧ P, then "Pubspec", "Add/update deps"):

image.png

Dart似乎没有像Swift(或JavaScript)那样有一个伟大的cows包,但至少有cowsay。这被添加到pubspec.yaml中。

dependencies:
  cowsay: ^1.0.0
  cupertino_icons: ^1.0.2
  flutter:
    sdk: flutter

一般来说,有一个声明性的包格式似乎比SPM用Package.swift做的要好得多(以及由此带来的所有痛苦)。

要使用cowsay,它需要被导入到main.dart文件中。在VSCode中,包的查找有(工作!)自动完成。

import 'package:flutter/material.dart';
import 'package:cowsay/cowsay.dart';

请注意,一个特定的.dart文件被导入,而不是真正的包。而package是一个URL方案,使编译器在特定的文件结构中查找该文件。

然后Cowsay可以在State的build函数(又称body)中使用。

body: Center(
  child: Column(
    mainAxisAlignment: MainAxisAlignment.start,
    children: [
      Text(Cowsay.getString('Hello!')), // <==
      const Text(
        'You have pushed the button this many times:',
      ),

等到voilà(再次,只需⌘s保存、编译_和_现场部署!)。

image.png

这并没有使用单行字体,实际上我们花了不少时间来弄清楚如何获得这种字体。在SwiftUI中,这将是一个简单的Font.monospaced

Text(cows.vaca())
  .font(.body.monospaced())


在Flutter中,我不得不将其与特定的字体绑定。

Text(cowsay.getString('Hello!'), 
     style: TextStyle(fontFamily: "Menlo"))

可能有一个更好的方法。

你的包里有多个文件

要在Swift中使用多个文件,你只需在同一个目标文件夹中创建它们,彼此相邻。不需要明确地导入它们,这很好,很干净。但据推测,这对编译速度没有什么帮助。

在Dart中,额外的文件也可以被添加到lib目录中,与main.dart一起。但要使用它们,需要导入它们。例如,如果你把MyApp类移到自己的my_app.dart文件中(是的,snakecase是Dart中文件名的做法)。

import 'my_app.dart';
import 'uxkit.dart';

当我第一次玩Flutter时,我对我们得到Android Material的外观感到有点惊讶。好吧,并不完全是惊讶。但是没有一个简单的开关来告诉Flutter像iOS应用一样渲染。而且看起来真的没有!

取而代之的是Flutter自带的一套 "Material "widgets,这也是模板应用导入的内容。

import 'package:flutter/material.dart';

和一个完全不同的"Cupertino "部件集

import 'package:flutter/cupertino.dart';

是的,这些是不同的widget,有不同的参数和布局。还有Material,像ListTile这样的部件不能作为CupertinoPageScaffold的子部件使用。

为了获得iOS的外观,必须编写一个不同的页面。

Widget build(BuildContext context) => CupertinoApp( // <==
  title: "Flutter Demo Home Page",
  home: const MyHomePage(title: 'Flutter Demo Home Page')
);
Widget build(BuildContext context) =>
  CupertinoPageScaffold( // <==
    navigationBar: 
      CupertinoNavigationBar(middle: Text(widget.title)),
    child: ListView(children: [
      Column(children: [
        Text(Cowsay.getString('Hello!'),
        style: const TextStyle(fontFamily: "Menlo")),
        const Text(
          'You have pushed the button this many times:',
        ),
        Text(
          '$_counter',
          style: Theme.of(context).textTheme.headline4,
        )
      ])
    ])
  );

这有点疯狂,不是吗?好吧,也许不是,😎它实际上遵循了SwiftUI的 "一学就会,随处适用 "的口号。无论如何,不同的平台需要不同的配置才能看起来合适。

image.png

也请记住,Flutter实际上并没有使用UIKit(就像SwiftUI在引擎盖下的许多东西一样)!

Material和Cupertino套装都是在Flutter内部进行所有的渲染,也就是说,它们是UI框架的重新实现。正因为如此,它们有不同的外观和感觉,当主操作系统向前发展时就会过时,而且在所有平台上都感觉 "不对劲"。

好吧,那么我们是否必须为不同的平台编写单独的Flutter应用程序?不一定。有一些软件包将这两个部件集抽象成一个共同的跨平台的部件集。

例如,Flutter Platform Widgets,它可以被添加为一个依赖项,然后被导入。

import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';

这包含了切换到Material或Cupertino的小部件,取决于平台。

Widget build(BuildContext context) => PlatformApp(
  title: "Flutter Demo Home Page",
  home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
Widget build(BuildContext context) =>
  PlatformScaffold(
    appBar: PlatformAppBar(title: Text(widget.title)),
    body: ListView(children: [
      Column(children: [
        Text(Cowsay.getString('Hello!'))
...               

它还有一个PlatformWidget,允许在每个上有不同的小工具,像这样。

PlatformWidget(
  cupertino : (_, __) => Text(cow)
  material  : (_, __) => ListTile(title: cow)
)

我真的不知道该怎么想。这不是默认的,这有点令人惊讶。特别是因为Cupertino的widget集实际上并没有使用UIKit。据推测,所有的东西一开始都是Material,然后人们实际上试图在iOS上使用Flutter。

因为这些widget的名字可能会很长,我自己创建了一个小的UXKit.dart

import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';

typedef UXApp        = PlatformApp;
typedef UXScaffold   = PlatformScaffold;
typedef UXAppBar     = PlatformAppBar;
typedef UXIcons      = PlatformIcons;
typedef UXIconButton = PlatformIconButton;

(注意,仅仅是Scaffold之类的东西往往会与Material set发生冲突,因为那些东西有不合格的全局名称,所以应该使用前缀。)

还值得注意的是,SwiftUI似乎走的是将(苹果)平台特定的东西,如navigationBarTitle(iOS特定的概念)抽象成一个更通用的navigationTitle。所以,也许我们确实在SwiftUI中得到了write-once 🧐。

高级状态管理

State management 是一个有趣的话题,因为它有点像SwiftUI在这里做的事情。没有什么🤓,而是支持多种方法。状态管理方法列表 (但是文档,同样::chefskiss:这里!)。

正如我们所看到的,Flutter本身将StateStatefulWidget解耦,使设置更加结构化。状态对象有点像SwiftUI人使用ObservableObject "ViewModels "或ViewController的方式。

Flutter也有一个更普遍的可观察对象的概念,以及@EnvironmentObjectObservableObject的在Flutter中被称为ChangeNotifier的,看起来像这样。

class CartModel extends ChangeNotifier {

  final List<Item> _items = [];
  int get totalPrice => _items.length * 42; // computed property!

  void add(Item item) {
    _items.add(item);
    notifyListeners();
  }
}

在Swift中,这将看起来像。

class CartModel: ObservableObject {
  
  @Published private var items = [ Item ]()
  var totalPrice : Int { items.count * 42 } // yes,yes

  func add(_ item: Item) { items.append(item) }
}

当数组被修改时,@Published属性封装器会进行修改通知(它将调用objectWillChange.send())。

在Dart中,notifyListeners方法需要明确地被调用,***在变化发生之后。(这很有趣,因为SwiftUI最初是用didChange,但在第一个版本之前改用objectWillChange

ChangeNotifier是Dart/Flutter本身的一部分。但是,尽管Flutter带着BuildContext,这有点像SwiftUI环境,它本身并没有@EnvironmentObject。这可以使用Provider包来添加。

要创建和注入一个环境对象,需要使用ChangeNotifierProvider

class Root extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    ChangeNotifierProvider(
      create: ( ctx ) => CartModel(),
      child: const HomePage()
    );
  }
}

这有点类似于。

struct Root: View {

  @StateObject var cart = CartModel() // autoclosure, also on-demand!

  var body: some View {
    HomePage()
      .environmentObject(cart)
  }
}

为了访问模型,使用了Consumer小组件。

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Consumer<CartModel>(
    builder: ( ctx, cart, child ) {
      return Text("Total: ${cart.total}");
    }
  );
}

如果模型发生变化,这将刷新子窗口部件。

在SwiftUI中,同样的情况会看起来像。

struct HomePage: View {
  
  @EnvironmentObject var cart : CartModel
  
  var body: some View {
    Text("Total: \(cart.total)")
  }
}

同样,SwiftUI因为看起来很好而得到了一些额外的分数,但在Flutter的设置中,发生的事情可以说是更容易理解。

BTW:不是100%确定,但我认为Flutter也有一个区别,那就是Flutter_总是在变化时重建所有的子小部件。这很复杂,但通常在SwiftUI中,子视图需要明确地订阅各自的环境/可观察对象以获得刷新(要么使用@EnvironmentObject@ObservedObject)。

总的来说,SwiftUI和Flutter中也有类似的概念。另外,两者都参考了其他包来进行更复杂的设置。我推荐ViewController 😬

导航

2022-06-05: SwiftUI 4(iOS 16+)有一个新的导航API,修复了很多东西。了解它在这里

SwiftUI中的导航是一个相当多的错误的混乱。很少有主题能得到如此多的抱怨。

在SwiftUI中,导航被绑定在状态上,这对这样一个框架来说有点意义。一个小例子。

struct MainPage: View {

  @State var showDetail = false

  var body: some View {
    NavigationView {
      VStack {
        NavigationLink("Goto detail", isActive: $showDetail) {
          DetailPage()
        }
      }
    }
  }
}
struct DetailPage: View {
  ...
}

要以编程方式触发导航,你 "只需 "改变showDetail的状态。

func gotoDetail() {
  showDetail = true
}

而且会神奇地发生。直到它不再发生。这真的是一个调度-主-同步-后的混乱。

在Flutter中,应用程序的导航状态并不是从状态中派生出来的,而是明确持有并推送给一个Navigator对象。

UXIconButton(
  icon: Icon(UXIcons(context).book),
  onPressed: () {
    Navigator.of(context)
      .push(platformPageRoute(context: context, 
        builder: (ctx) => DetailPage()
      ));
  }
)

这与我在ViewController中的做法很相似。

class HomePage: ViewController {
  var view: some View {
    Button("Goto Detail") {
      show(DetailPage())
    }
  }
}

人们提到,目前的Flutter Navigator可能过于复杂了。我不能说太多,但至少它似乎能可靠地工作,不管是什么情况。

小型网络服务应用

像我一样,我最后为Swift Package Index写了一个小的Flutter前台。

image.png

它有一个TextField,做HTTP和JSON,并在ListView中显示搜索结果(我有没有提到:Flutter文件😚)。

重要的是: 不要在你自己的应用程序中使用这个API,如果你想以编程方式访问SPI,请与Sven或Dave联系。

所有的文件都有小注解。

main.dart:

import 'package:flutter/widgets.dart';
import 'my_app.dart';

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

将应用程序部件移到一个自己的文件中,该文件被导入,my_app.dart

import 'package:flutter/material.dart';
import 'package:hello_flutter/spm_list_page.dart';
import 'uxkit.dart';

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

  @override
  Widget build(BuildContext context) => const UXApp(
    title : 'Welcome to Flutter',
    home  : SPMListPage()
  );
}

没有太多的魔法,SPMListPage是 "内容视图",spm_list_page.dart

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';    // for jsonDecode
import 'uxkit.dart';
import 'spipackage.dart'; // the 'model'

class SPMListPage extends StatefulWidget {
  const SPMListPage({Key? key}) : super(key: key);

  @override
  State<SPMListPage> createState() => _SPMListPageState();
}

class _SPMListPageState extends State<SPMListPage> { 

  late Future<List<SPIPackage>> _packages; // Swift "lazy"
  final _biggerFont  = const TextStyle(fontSize: 28);
  final _searchField = TextEditingController(text: "Dart");
  
  @override
  void initState() { // like onAppear
    super.initState();
    _fetch();
  }
  @override
  void dispose() {
    _searchField.dispose();
    super.dispose();
  }
  
  List<SPIPackage> _decodeJSON(dynamic json) {
    var packages = <SPIPackage>[];
    final jsonDict = json as Map<String, dynamic>;
    final results  = jsonDict["results"] as List<dynamic>;
    
    for (final result in results) {
      final json = result as Map<String, dynamic>;
      
      if (json["package"] == null) continue;
      final nest = json["package"] as Map<String, dynamic>;
      if (nest["_0"] == null) continue; // Coddable, lolz
      final packageJSON = nest["_0"] as Map<String, dynamic>;
      final package = SPIPackage.fromJson(packageJSON);
      packages.add(package);
    }
    return packages;
  }
  
  Future<List<SPIPackage>> _querySPI(String query) async {
    final response = await http.get(
      Uri(scheme: "https",  host: "swiftpackageindex.com",
          path: "/api/search", 
          queryParameters : { "query": query })
    );
    
    if (response.statusCode == 200) {
      return _decodeJSON(jsonDecode(response.body));
    }
    else {// is this attached to the future?
      throw Exception("Failed to run query! ${response.statusCode}");
    }
  }
  
  void _fetch() {
    setState(() {
      _packages = _querySPI(_searchField.text);      
    });
  }

  // MARK: - UI

  Widget _buildCell(BuildContext context, SPIPackage item) {
    return Container(
      margin     : const EdgeInsets.fromLTRB(16, 16, 16, 0),
      padding    : const EdgeInsets.all(8),
      decoration : BoxDecoration(
        border: Border.all(width: 4, color: Colors.black38),
        borderRadius: const BorderRadius.all(Radius.circular(8)),
      ),

      child: Column( // VStack
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(item.packageName ?? item.repositoryName, style: _biggerFont),
          Text("Stars: ${item.stars}")
        ]
      )
    );
  }

  Widget _buildResultList(BuildContext context, List<SPIPackage> packages) {
    return ListView.builder(
      itemCount: packages.length,
      padding: const EdgeInsets.all(0.0),
      itemBuilder: ( ctx, index ) => _buildCell(context, packages[index])
    );
  }

  Widget _buildResultView(BuildContext context) {
    return FutureBuilder<List<SPIPackage>>(
      future: _packages,
      builder: ( ctx, snapshot ) {
        if (snapshot.hasData) {
          return _buildResultList(context, snapshot.data as List<SPIPackage>);
        }
        else if (snapshot.hasError) {
          return Center(child: Text("ERROR: ${snapshot.error}"));
        }
        else {
          return Center(child: PlatformCircularProgressIndicator());
        }
      }
    );
  }

  Widget _buildSearchForm(BuildContext context) {
    return Row(children: [
      Expanded(child: PlatformTextField(controller: _searchField)),
      UXIconButton(icon: Icon(UXIcons(context).search), 
                   onPressed: () => _fetch())
    ]);
  }

  @override
  Widget build(BuildContext context) => UXScaffold(
    appBar            : UXAppBar(title: const Text("SPI")),
    body              : Column(children: [ 
      Padding(padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), child:
        _buildSearchForm(context)
      ),
      Expanded(child: _buildResultView(context)) // expanded is required
    ]),

    iosContentPadding : true
  );
}

我承认,这不是一个美丽的东西,但是,嘿,它是相当不错的。

Flutter似乎没有@Binding的,所以有了TextEditingController对象(一个ChangeNotifier!)。我在这里实际上没有监听变化事件,而只是在用户按下按钮时才获取。

需要注意的一点是,使用本地方法来构建用户界面的一部分是很常见的。人们在SwiftUI中也经常这么做。

SPIPackage.dart的(不可变的,所有最终的)"模型"。

class SPIPackage {

  final String  packageId;
  final String  repositoryName;
  final String? packageName;
  final String  packageURL;
  final int     stars;
  final String  summary;

  const SPIPackage({
    required this.packageId,
    required this.repositoryName,
    required this.packageName,
    required this.packageURL,
    required this.stars,
    required this.summary
  });

  factory SPIPackage.fromJson(Map<String, dynamic> json) {
    return SPIPackage(
      packageId      : json['packageId'],
      repositoryName : json['repositoryName'],
      packageName    : json['packageName'],
      packageURL     : json['packageURL'],
      stars          : json['stars'],
      summary        : json['summary']
    );
  }
}

注意这些选项。不知道为什么 "工厂 "是一个独立于构造函数的概念。json动态值不需要明确投递。

就这样吧!

结束语

首先,非常感谢@pfriedrich_帮助我们开始使用Flutter!

从开发者的角度来看,在Flutter中开发似乎很愉快。热重载,伟大的文档,实际工作中的工具都是非常好的。

我一般不喜欢的是 "外观"。VSCode既实用又丑陋。Flutter产生的应用程序是丑陋的(@kiliankoe总是要求提供一个在iPhone上不感觉糟糕的单一Flutter应用程序)。如果你使用实际的Flutter惯例(到处都是尾部逗号,在闭合的大括号上注入注释,......),源代码就很难看。

但是,API/语言是好的。如果我被迫(💰💰💰💰💰)制作一个Android应用,Flutter似乎是一个不错的选择(尽管Kotlin/Jetpack Compose听起来也很有趣)。 SwiftUI的开发者会发现有很多东西值得喜欢(但也有可能更讨厌Xcode)。

虽然在没有进行像样的测试的情况下,总是很难谈论性能,但感觉SwiftUI有可能大大加快(10倍?) 考虑到所有的静态类型、堆栈分配、每个视图的无效性。但谁知道呢,也许Dart编译器可以弥补所有这些🙅♀️。

像往常一样,我们欢迎所有的反馈。@helje5me@helgehess.eu

链接

热烈欢迎反馈。@helje5, @helge@mastodon.social, me@helgehess.eu. GitHub

想支持我们的工作?购买一个应用程序。Past for iChat, SVG Shaper, Shrugs, HMScriptEditor。你不需要使用它!😀


www.deepl.com 翻译