用 Go 实现 Flutter
我最近发现了 Flutter —— Google的一个新的移动开发框架,我甚至曾经将 Flutter 基础知识教给没有程式设计经验的人。Flutter 是用 Dart 编写的,这是一种诞生于 Chrome 浏览器的程式语言,后来改用到了控制台。这不禁让我想到“Flutter 也许可以很轻易地用 Go 来实现”!为什么不用 Go 实现呢?Go 和 Dart 都是诞生于Google(并且有很多的大会分享使它们变得更好),它们都是强型别的编译语言 —— 如果情形发生一些改变,Go 也完全可以成为像 Flutter 这样热门专案的选择。而那时候 Go 会更容易地向没有程式设计经验的人解释或传授。
假如 Flutter 已经是用 Go 开发的。那它的程式码会是什么样的?
Dart 的问题
自从 Dart 在 Chrome 中出现以来,我就一直在关注它的开发情况,我也一直认为 Dart 最终会在所有浏览器中取代 JS。2015 年,得知有关Google在 Chrome 中放弃 Dart 支援的讯息时,我非常沮丧。
Dart 是非常奇妙的!是的,当你从 JS 升级转向到 Dart 时,会感觉一切都还不错;可如果你从 Go 降级转过来,就没那么惊奇了,但是…… Dart 拥有非常多的特性 —— 类、泛型、异常、Futures、异步等待、事件循环、JIT、AOT、垃圾回收、过载 —— 你能想到的它都有。它有用于 getter/setter 的特殊语法、有用于建构函式自动初始化的特殊语法、有用于特殊语句的特殊语法等。
虽然它让能让拥有其他语言经验的人更容易熟悉 Dart —— 这很不错,也降低了入门门槛 —— 但我发现很难向没有程式设计经验的新手讲解它。
所有“特殊”的东西易被混淆 —— “名为构造方法的特殊方法”,“用于初始化的特殊语法”,“用于覆盖的特殊语法”等等。所有“隐式”的东西令人困惑 —— “这个类是从哪儿汇入的?它是隐藏的,你看不到它的实现程式码”,“为什么我们在这个类中写一个构造方法而不是其他方法?它在那里,可是它是隐藏的”等等。所有“有歧义的语法”易被混淆 —— “所以我应该在这里使用命名或者对应位置的引数吗?”,“应该使用 final 还是用 const 进行变数宣告?”,“应该使用普通函式语法还是‘箭头函式语法’”等等。这三个标签 —— “特殊”、“隐式”和“歧义” —— 可能更符合人们在程式语言中所说的“魔法”的本质。这些特性旨在帮助我们编写更简单、更干净的程式码,但实际上,它们给阅读程式增加了更多的混乱和心智负担。
而这正是 Go 截然不同并且有着自己强烈特色的地方。Go 实际上是一个非魔法的语言 —— 它将特殊、隐式、歧义之类的东西的数量讲到最低。然而,它也有一些缺点。
Go 的问题
当我们讨论 Flutter 这种 UI 框架时,我们必须把 Go 看作一个描述/指明 UI 的工具。UI 框架是一个非常复杂的主题,它需要建立一种专门的语言来处理大量的底层复杂性。最流行的方法之一是建立 DSL —— 特定领域的语言 —— 众所周知,Go 在这方面不那么尽如人意。
建立 DSL 意味着建立开发人员可以使用的自定义术语和谓词。生成的程式码应该可以捕捉 UI 布局和互动的本质,并且足够灵活,可以应对设计师的想象流,又足够的严格,符合 UI 框架的限制。例如,你应该能够将按钮放入容器中,然后将图示和文字小元件放入按钮中,可如果你试图将按钮放入文字中,编译器应该给你提示一个错误。
特定于 UI 的语言通常也是宣告性的 —— 实际上,这意味着你应该能够使用构造程式码(包括空格缩排!)来视觉化的捕获 UI 元件树的结构,然后让 UI 框架找出要执行的程式码。
有些语言更适合这样的使用方式,而 Go 从来没有被设计来完成这类的任务。因此,在 Go 中编写 Flutter 程式码应该是一个相当大的挑战!
Flutter 的优势
如果你不熟悉 Flutter,我强烈建议你花一两个周末的时间来观看教程或阅读文件,因为它无疑会改变移动开发领域的游戏规则。而且,可能不仅仅是移动端 —— 还有原生桌面应用程序和 web 应用程序的渲染器(用 Flutter 的术语来说就是嵌入式)。Flutter 容易学习,它是合乎逻辑的,它汇集了大量的 Material Design 强大元件库,有活跃的社群和丰富的工具链(如果你喜欢“构建/测试/执行”的工作流,你也能在 Flutter 中找到同样的“构建/测试/执行”的工作方式)还有大量其他的用于实践的工具箱。
在一年前我需要一个相对简单的移动应用(很明显就是 IOS 或 Android),但我深知精通这两个平台开发的复杂性是非常非常大的(至少对于这个 app 是这样),所以我不得不将其外包给另一个团队并为此付钱。对于像我这样一个拥有近 20 年的程式设计经验的开发者来说,开发这样的移动应用几乎是无法忍受的。
使用 Flutter,我用了 3 个晚上的时间就编写了同样的应用程序,与此同时,我是从头开始学习这个框架的!这是一个数量级的提升,也是游戏规则的巨大改变。
我记得上一次看到类似这种开发生产力革命是在 5 年前,当时我发现了 Go。并且它改变了我的生活。
我建议你从这个很棒的视讯教程开始。
Flutter 的 Hello, world
当你用 flutter create 建立一个新的 Flutter 专案,你会得到这个“Hello, world”应用程序和程式码文字、计数器和一个按钮,点选增加按钮,计数器会增加。
我认为用我们假想的 Go 版的 Flutter 重写这个例子是非常好的。它与我们的主题有密切的关联。看一下它的程式码(它是一个档案):
lib/main.dart:
import \'package:flutter/material.dart\';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: \'Flutter Demo\',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: \'Flutter Demo Home Page\'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
\'You have pushed the button this many times:\',
),
Text(
\'$_counter\',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: \'Increment\',
child: Icon(Icons.add),
),
);
}
}
复制程式码
我们先把它分解成几个部分,分析哪些可以对映到 Go 中,哪些不能对映,并探索目前我们拥有的选项。
对映到 Go
一开始是相对比较简单的 —— 汇入依赖项并启动 main() 函式。这里没有什么挑战性也不太有意思,只是语法上的变化:
package hello
import "github.com/flutter/flutter"
func main() {
app := NewApp()
flutter.Run(app)
}
复制程式码
唯一的不同的是不使用魔法的 MyApp() 函式,它是一个构造方法,也是一个特殊的函式,它隐藏在被称为 MyApp 的类中,我们只是呼叫一个显示定义的 NewApp() 函式 —— 它做了同样的事情,但它更易于阅读、理解和弄懂。
Widget 类
在 Flutter 中,一切皆 widget(小元件)。在 Flutter 的 Dart 版本中,每个小元件都代表一个类,这个类扩充套件了 Flutter 中特殊的 Widget 类。
Go 中没有类,因此也没有类层次,因为 Go 的世界不是面向物件的,更不必说类层次了。对于只熟悉基于类的 OOP 的人来说,这可能是一个不太好的情况,但也不尽然。这个世界是一个巨大的相互关联的事物和关系图谱。它不是混沌的,可也不是完全的结构化,并且尝试将所有内容都放入类层次结构中可能会导致程式码难以维护,到目前为止,世界上的大多数程式码库都是这样子。
我喜欢 Go 的设计者们努力重新思考这个无处不在的基于 OOP 思维,并提出了与之不同的 OOP 概念,这与 OOP 的发明者 Alan Kay 所要表达的真实意义更接近,这不是偶然。
在 Go 中,我们用一个具体的型别 —— 一个结构体来表示这种抽象:
type MyApp struct {
// ...
}
复制程式码
在一个 Flutter 的 Dart 版本中,MyApp必须继承于 StatelessWidget 类并覆盖它的 build 方法,这样做有两个作用:
自动地给予 MyApp 一些 widget 属性/方法通过呼叫 build,允许 Flutter 在其构建/渲染管道中使用跟我们的元件我不知道 Flutter 的内部原理,所以让我们不要怀疑我们是否能用 Go 实现它。为此,我们只有一个选择 —— 型别嵌入
type MyApp struct {
flutter.Core
// ...
}
复制程式码
这将增加 flutter.Core 中所有汇出的属性和方法到我们的 MyApp 中。我将它称为 Core 而不是 Widget,因为嵌入的这种型别还不能使我们的 MyApp 称为一个 widget,而且,这是我在 Vecty GopherJS 框架中看到的类似场景的选择。稍后我将简要的探讨 Flutter 和 Vecty 之间的相似之处。
第二部分 —— Flutter 引擎中的 build 方法 —— 当然应该简单的通过新增方法来实现,满足在 Go 版本的 Flutter 中定义的一些界面:
flutter.go 档案:
type Widget interface {
Build(ctx BuildContext) Widget
}
复制程式码
我们的 main.go 档案:
type MyApp struct {
flutter.Core
// ...
}
// 构建渲染 MyApp 元件。实现 Widget 的界面
func (m *MyApp) Build(ctx flutter.BuildContext) flutter.Widget {
return flutter.MaterialApp()
}
复制程式码
我们可能会注意到这里和 Dart 版的 Flutter 有些不同:
程式码更加冗长 —— BuildContext,Widget 和 MaterialApp 等方法前都明显地提到了 flutter。程式码更简洁 —— 没有 extends Widget 或者 @override 子句。Build 方法是大写开头的,因为在 Go 中它的意思是“公共”可见性。在 Dart 中,大写开头小写开头都可以,但是要使属性或方法“私有化”,名称需要使用下划线(_)开头。为了实现一个 Go 版的 Flutter Widget,现在我们需要嵌入 flutter.Core 并实现 flutter.Widget 界面。好了,非常清楚了,我们继续往下实现。
状态
在 Dart 版的 Flutter 中,这是我发现的第一个令人困惑的地方。Flutter 中有两种元件 —— StatelessWidget 和 StatefulWidget。嗯,对我来说,无状态元件只是一个没有状态的元件,所以,为什么这里要建立一个新的类呢?好吧,我也能接受。但是你不能仅仅以相同的方式扩充套件 StatefulWidget,你应该执行以下神奇的操作(安装了 Flutter 外挂的 IDE 都可以做到,但这不是重点):
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold()
}
}
复制程式码
呃,我们不仅仅要理解这里写的是什么,还要理解,为什么这样写?
这里要解决的任务是向元件中新增状态(counter)时,并允许 Flutter 在状态更改时重绘元件。这就是复杂性的根源。
其余的都是偶然的复杂性。Dart 版的 Flutter 中的办法是引入一个新的 State 类,它使用泛型并以小元件作为引数。所以 _MyAppState 是一个来源于 State of a widget MyApp 的类。好了,有点道理...但是为什么 build() 方法是在一个状态而非元件上定义的呢?这个问题在 Flutter 仓库的 FAQ 中有回答,这里也有详细的讨论,概括一下就是:子类 StatefulWidget 被例项化时,为了避免 bug 之类的。换句话说,它是基于类的 OOP 设计的一种变通方法。
我们如何用 Go 来设计它呢?
首先,我个人会尽量避免为 State 建立一个新概念 —— 我们已经在任意具体型别中隐式地包含了“state” —— 它只是结构体的属性(字段)。可以说,语言已经具备了这种状态的概念。因此,建立一个新状态只会让开发人员赶到困惑 —— 为什么我们不能在这里使用型别的“标准状态”。
当然,挑战在于使 Flutter 引擎跟踪状态发生变化并对其作出反应(毕竟这是响应式程式设计的要点)。我们不需要为状态的更改建立特殊方法和包装器,我们只需要让开发人员手动告诉 Flutter 何时需要更新小元件。并不是所有的状态更改都需要立即重绘 —— 有很多典型场景能说明这个问题。我们来看看:
type MyHomePage struct {
flutter.Core
counter int
}
// Build 渲染了 MyHomePage 元件。实现了 Widget 界面
func (m *MyHomePage) Build(ctx flutter.BuildContext) flutter.Widget {
return flutter.Scaffold()
}
// 给计数器元件加一
func (m *MyHomePage) incrementCounter() {
m.counter++
flutter.Rerender(m)
// or m.Rerender()
// or m.NeedsUpdate()
}
复制程式码
这里有很多命名和设计选项 —— 我喜欢其中的 NeedsUpdate(),因为它很明确,而且是 flutter.Core(每个元件都有它)的一个方法,但 flutter.Rerender() 也可以正常工作。它给人一种即时重绘的错觉,但是 —— 并不会经常这样 —— 它将在下一帧时重绘,状态更新的频率可能比帧的重绘的频率高的多。
但问题是,我们只是实现了相同的任务,也就是新增一个状态响应到小元件中,下面的一些问题还未解决:
新的型别泛型读/写状态的特殊规则新的特殊的方法覆盖另外,API 更简洁也更明确 —— 只需增加计数器并请求 flutter 重新渲染 —— 当你要求呼叫特殊函式 setState 时,有些变化并不明显,该函式返回另一个实际状态更改的函式。同样,隐式的魔法会有损可读性,我们设法避免了这一点。因此,程式码更简单,并且精简了两倍。
有状态的子元件
继续这个逻辑,让我们仔细看看在 Flutter 中,“有状态的小元件”是如何在另一个元件中使用的:
@override
Widget build(BuildContext context) {
return MaterialApp(
title: \'Flutter Demo\',
home: MyHomePage(title: \'Flutter Demo Home Page\'),
);
}
复制程式码
这里的 MyHomePage 是一个“有状态的小元件”(它有一个计数器),我们通过在构建过程中呼叫建构函式 MyHomePage(title:"...") 来建立它...等等,构建的是什么?
呼叫 build() 重绘小元件,可能每秒有多次绘制。为什么我们要在每次渲染中建立一个小元件?更别说在每次重绘循环中,重绘有状态的小元件了。
结论是,Flutter 用小元件和状态之间的这种分离来隐藏这个初始化/状态记录,不让开发者过多关注。它确实每次都会建立一个新的 MyHomePage 元件,但它保留了原始状态(以单例的方式),并自动找到这个“唯一”状态,将其附加到新建立的 MyHomePage 元件上。
对我来说,这没有多大意义 —— 更多的隐式,更多的魔法也更容易令人模糊(我们仍然可以新增小元件作为类属性,并在建立小元件时例项化它们)。我理解为什么这种方式不错了(不需要跟踪元件的子元件),并且它具有良好的简化重构作用(只有在一个地方删除建构函式的呼叫才能删除子元件),但任何开发者试图真正搞懂整个工作原理时,都可能会有些困惑。
对于 Go 版的 Flutter,我肯定更倾向于初始化了的状态显式且清晰的小元件,虽然这意味着程式码会更冗长。Dart 版的 Flutter 可能也可以实现这种方式,但我喜欢 Go 的非魔法特性,而这种哲学也适用于 Go 框架。因此,我的有状态子元件的程式码应该类似这样:
// MyApp 是应用顶层的元件。
type MyApp struct {
flutter.Core
homePage *MyHomePage
}
// NewMyApp 例项化一个 MyApp 元件
func NewMyApp() *MyApp {
app := &MyApp{}
app.homePage = &MyHomePage{}
return app
}
// Build 渲染了 MyApp 元件。实现了 Widget 界面
func (m *MyApp) Build(ctx flutter.BuildContext) flutter.Widget {
return m.homePage
}
// MyHomePage 是一个首页元件
type MyHomePage struct {
flutter.Core
counter int
}
// Build 渲染 MyHomePage 元件。实现 Widget 界面
func (m *MyHomePage) Build(ctx flutter.BuildContext) flutter.Widget {
return flutter.Scaffold()
}
// 增量计数器让 app 的计数器增加一
func (m *MyHomePage) incrementCounter() {
m.counter++
flutter.Rerender(m)
}
复制程式码
程式码更加冗长了,如果我们必须在 MyApp 中更改/替换 MyHomeWidget,那我们需要在 3 个地方有所改动,还有一个作用是,我们对程式码执行的每个阶段都有一个完整而清晰的了解。没有隐藏的东西在幕后发生,我们可以 100% 自信的推断程式码、效能和每个型别以及函式的依赖关系。对于一些人来说,这就是最终目标,即编写可靠且可维护的程式码。
顺便说一下,Flutter 有一个名为 StatefulBuilder 的特殊元件,它为隐藏的状态管理增加了更多的魔力。
DSL
现在,到了有趣的部分。我们如何在 Go 中构建一个 Flutter 的元件树?我们希望我们的元件树简洁、易读、易重构并且易于更新、描述元件之间的空间关系,增加足够的灵活性来插入自定义程式码,比如,按下按钮时的程式处理等等。
我认为 Dart 版的 Flutter 是非常好看的,不言自明:
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(\'You have pushed the button this many times:\'),
Text(
\'$_counter\',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: \'Increment\',
child: Icon(Icons.add),
),
);
复制程式码
每个小元件都有一个构造方法,它接收可选的引数,而令这种宣告式方法真正好用的技巧是 函式的命名引数。
命名引数
为了防止你不熟悉,详细说明一下,在大多数语言中,引数被称为“位置引数”,因为它们在函式呼叫中的引数位置很重要:
Foo(arg1, arg2, arg3)
复制程式码
使用命名引数时,可以在函式呼叫中写入它们的名称:
Foo(name: arg1, description: arg2, size: arg3)
复制程式码
它虽增加了冗余性,但帮你省略了你点选跳转函式来理解这些引数的意思。
对于 UI 元件树,它们在可读性方面起著至关重要的作用。考虑一下跟上面相同的程式码,在没有命名引数的情况下:
return Scaffold(
AppBar(
Text(widget.title),
),
Center(
Column(
MainAxisAlignment.center,
[
Text(\'You have pushed the button this many times:\'),
Text(
\'$_counter\',
Theme.of(context).textTheme.display1,
),
],
),
),
FloatingActionButton(
_incrementCounter,
\'Increment\',
Icon(Icons.add),
),
);
复制程式码
咩,是不是?它不仅难以阅读和理解(你需要记住每个引数的含义、型别,这是一个很大的心智负担),而且我们在传递那些引数时没有灵活性。例如,你可能不希望你的 Material 应用有 FloatingButton,所以你只是不传递 floatingActionButton。如果没有命名引数,你将被迫传递它(例如可能是 null/nil),或者使用一些带有反射的脏魔法来确定使用者通过建构函式传递了哪些引数。
由于 Go 没有函式过载或命名引数,因此这会是一个棘手的问题。
用 Go 实现元件树
版本 1
这个版本的例子可能只是拷贝 Dart 表示元件树的方法,但我们真正需要的是后退一步并回答这个问题 —— 在语言的约束下,哪种方法是表示这种型别资料的最佳方法呢?
让我们仔细看看 Scaffold 物件,它是构建外观美观的现代 UI 的好帮手。它有这些属性 —— appBar,drawer,home,bottomNavigationBar,floatingActionButton —— 所有都是 Widget。我们建立型别为 Scaffold 的物件的同时初始化这些属性。这样看来,它与任何普通物件例项化没有什么不同,不是吗?
我们用程式码实现:
return flutter.NewScaffold(
flutter.NewAppBar(
flutter.Text("Flutter Go app", nil),
),
nil,
nil,
flutter.NewCenter(
flutter.NewColumn(
flutter.MainAxisCenterAlignment,
nil,
[]flutter.Widget{
flutter.Text("You have pushed the button this many times:", nil),
flutter.Text(fmt.Sprintf("%d", m.counter), ctx.Theme.textTheme.display1),
},
),
),
flutter.FloatingActionButton(
flutter.NewIcon(icons.Add),
"Increment",
m.onPressed,
nil,
nil,
),
)
复制程式码
当然,这不是最漂亮的 UI 程式码。这里的 flutter 是如此的丰富,以至于要求它被隐藏起来(实际上,我应该把它命名为 material 而非 flutter),这些没有命名的引数含义并不清晰,尤其是 nil。
版本 2
由于大多数程式码都会使用 flutter 汇入,所以使用汇入点符号(.)的方式将 flutter 汇入到我们的名称空间中是没问题的:
import . "github.com/flutter/flutter"
复制程式码
现在,我们不用写 flutter.Text,而只需要写 Text。这种方式通常不是最佳实践,但是我们使用的是一个框架,不必逐行汇入,所以在这里是一个很好的实践。另一个有效的场景是一个基于 GoConvey 框架的 Go 测试。对我来说,框架相当于语言之上的其他语言,所以在框架中使用点符号汇入也是可以的。
我们继续往下写我们的程式码:
return NewScaffold(
NewAppBar(
Text("Flutter Go app", nil),
),
nil,
nil,
NewCenter(
NewColumn(
MainAxisCenterAlignment,
nil,
[]Widget{
Text("You have pushed the button this many times:", nil),
Text(fmt.Sprintf("%d", m.counter), ctx.Theme.textTheme.display1),
},
),
),
FloatingActionButton(
NewIcon(icons.Add),
"Increment",
m.onPressed,
nil,
nil,
),
)
复制程式码
比较简洁,但是那些 nil... 我们怎么才能避免那些必须传递的引数?
版本 3
反射怎么样?一些早期的 Go Http 框架使用了这种方式(例如 martini)—— 你可以通过引数传递任何你想要传递的内容,执行时将检查这是否是一个已知的型别/引数。从多个角度看,这不是一个好办法 —— 它不安全,速度相对比较慢,还具魔法的特性 —— 但为了探索,我们还是试试:
return NewScaffold(
NewAppBar(
Text("Flutter Go app"),
),
NewCenter(
NewColumn(
MainAxisCenterAlignment,
[]Widget{
Text("You have pushed the button this many times:"),
Text(fmt.Sprintf("%d", m.counter), ctx.Theme.textTheme.display1),
},
),
),
FloatingActionButton(
NewIcon(icons.Add),
"Increment",
m.onPressed,
),
)
复制程式码
好吧,这跟 Dart 的原始版本有些类似,但缺少命名引数,确实会妨碍在这种情况下的可选引数的可读性。另外,程式码本身就有些不好的迹象。
版本 4
让我们重新思考一下,在建立新物件和可选的定义他们的属性时,我们究竟想做什么?这只是一个普通的变数例项,所以假如我们用另一种方式来尝试呢:
scaffold := NewScaffold()
scaffold.AppBar = NewAppBar(Text("Flutter Go app"))
column := NewColumn()
column.MainAxisAlignment = MainAxisCenterAlignment
counterText := Text(fmt.Sprintf("%d", m.counter))
counterText.Style = ctx.Theme.textTheme.display1
column.Children = []Widget{
Text("You have pushed the button this many times:"),
counterText,
}
center := NewCenter()
center.Child = column
scaffold.Home = center
icon := NewIcon(icons.Add),
fab := NewFloatingActionButton()
fab.Icon = icon
fab.Text = "Increment"
fab.Handler = m.onPressed
scaffold.FloatingActionButton = fab
return scaffold
复制程式码
这种方法是有效的,虽然它解决了“命名引数问题”,但它也确实打乱了对元件树的理解。首先,它颠倒了建立小元件的顺序 —— 小元件越深,越应该早定义它。其次,我们丢失了基于程式码缩排的空间布局,好的缩排布局对于快速构建元件树的高阶预览非常有用。
顺便说一下,这种方法已经在 UI 框架中使用很长时间,比如 GTK 和 Qt。可以到最新的 Qt 5 框架的文件中检视程式码示例。
QGridLayout *layout = new QGridLayout(this);
layout->addWidget(new QLabel(tr("Object name:")), 0, 0);
layout->addWidget(m_objectName, 0, 1);
layout->addWidget(new QLabel(tr("Location:")), 1, 0);
m_location->setEditable(false);
m_location->addItem(tr("Top"));
m_location->addItem(tr("Left"));
m_location->addItem(tr("Right"));
m_location->addItem(tr("Bottom"));
m_location->addItem(tr("Restore"));
layout->addWidget(m_location, 1, 1);
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
layout->addWidget(buttonBox, 2, 0, 1, 2);
复制程式码
所以对于一些人来说,将 UI 用程式码来描述可能是一种更自然的方式。但很难否认这肯定不是最好的选择。
版本 5
我在想的另一个选择,是为构造方法的引数建立一个单独的型别。例如:
func Build() Widget {
return NewScaffold(ScaffoldParams{
AppBar: NewAppBar(AppBarParams{
Title: Text(TextParams{
Text: "My Home Page",
}),
}),
Body: NewCenter(CenterParams{
Child: NewColumn(ColumnParams{
MainAxisAlignment: MainAxisAlignment.center,
Children: []Widget{
Text(TextParams{
Text: "You have pushed the button this many times:",
}),
Text(TextParams{
Text: fmt.Sprintf("%d", m.counter),
Style: ctx.textTheme.display1,
}),
},
}),
}),
FloatingActionButton: NewFloatingActionButton(
FloatingActionButtonParams{
OnPressed: m.incrementCounter,
Tooltip: "Increment",
Child: NewIcon(IconParams{
Icon: Icons.add,
}),
},
),
})
}
复制程式码
还不错,真的!这些 ..Params 显得很啰嗦,但不是什么大问题。事实上,我在 Go 的一些库中经常遇到这种方式。当你有数个物件需要以这种方式例项化时,这种方法尤其有效。
有一种方法可以移除 ...Params 这种啰嗦的东西,但这需要语言上的改变。在 Go 中有一个建议,它的目标正是实现这一点 —— 无型别的复合型字面量。基本上,这意味着我们能够缩短 FloattingActionButtonParameters{...} 成 {...},所以我们的程式码应该是这样:
func Build() Widget {
return NewScaffold({
AppBar: NewAppBar({
Title: Text({
Text: "My Home Page",
}),
}),
Body: NewCenter({
Child: NewColumn({
MainAxisAlignment: MainAxisAlignment.center,
Children: []Widget{
Text({
Text: "You have pushed the button this many times:",
}),
Text({
Text: fmt.Sprintf("%d", m.counter),
Style: ctx.textTheme.display1,
}),
},
}),
}),
FloatingActionButton: NewFloatingActionButton({
OnPressed: m.incrementCounter,
Tooltip: "Increment",
Child: NewIcon({
Icon: Icons.add,
}),
},
),
})
}
复制程式码
这和 Dart 版的几乎一样!但是,它需要为每个小元件建立这些对应的引数型别。
版本 6
探索另一个办法是使用小元件的方法链。我忘记了这个模式的名称,但这不是很重要,因为模式应该从程式码中产生,而不是以相反的方式。
基本思想是,在建立一个小元件 —— 比如 NewButton() —— 我们立即呼叫一个像 WithStyle(...) 的方法,它返回相同的物件,我们就可以在一行(或一列)中呼叫越来越多的方法:
button := NewButton().
WithText("Click me").
WithStyle(MyButtonStyle1)
复制程式码
或者
button := NewButton().
Text("Click me").
Style(MyButtonStyle1)
复制程式码
我们尝试用这种方法重写基于 Scaffold 元件:
// Build renders the MyHomePage widget. Implements Widget interface.
func (m *MyHomePage) Build(ctx flutter.BuildContext) flutter.Widget {
return NewScaffold().
AppBar(NewAppBar().
Text("Flutter Go app")).
Child(NewCenter().
Child(NewColumn().
MainAxisAlignment(MainAxisCenterAlignment).
Children([]Widget{
Text("You have pushed the button this many times:"),
Text(fmt.Sprintf("%d", m.counter)).
Style(ctx.Theme.textTheme.display1),
}))).
FloatingActionButton(NewFloatingActionButton().
Icon(NewIcon(icons.Add)).
Text("Increment").
Handler(m.onPressed))
}
复制程式码
这不是一个陌生的概念 —— 例如,许多 Go 库中对配置选项使用类似的方法。这个版本跟 Dart 的版本略有不同,但它们都具备了大部分所需要的属性:
显示地构建元件树命名引数在元件树中以缩排的方式显示元件的深度处理指定功能的能力我也喜欢传统的 Go 的 New...() 例项化方式。它清楚的表明它是一个函式,并建立了一个新物件。跟解释建构函式相比,向新手解释建构函式要更容易一些:“它是一个与类同名的函式,但是你找不到这个函式,因为它很特殊,而且你无法通过检视建构函式就轻松地将它与普通函式区分开来”。
无论如何,在我探索的所有方法中,最后两个选项可能是最合适的。
最终版
现在,把所有的元件组装在一起,这就是我要说的 Flutter 的 “hello, world” 应用的样子:
main.go
package hello
import "github.com/flutter/flutter"
func main() {
flutter.Run(NewMyApp())
}
复制程式码
app.go:
package hello
import . "github.com/flutter/flutter"
// MyApp 是顶层的应用元件
type MyApp struct {
Core
homePage *MyHomePage
}
// NewMyApp 初始化一个新的 MyApp 元件
func NewMyApp() *MyApp {
app := &MyApp{}
app.homePage = &MyHomePage{}
return app
}
// Build 渲染了 MyApp 元件。实现了 Widget 界面
func (m *MyApp) Build(ctx BuildContext) Widget {
return m.homePage
}
复制程式码
home_page.go:
package hello
import (
"fmt"
. "github.com/flutter/flutter"
)
// MyHomePage 是一个主页元件
type MyHomePage struct {
Core
counter int
}
// Build 渲染了 MyHomePage 元件。实现了 Widget 界面
func (m *MyHomePage) Build(ctx BuildContext) Widget {
return NewScaffold(ScaffoldParams{
AppBar: NewAppBar(AppBarParams{
Title: Text(TextParams{
Text: "My Home Page",
}),
}),
Body: NewCenter(CenterParams{
Child: NewColumn(ColumnParams{
MainAxisAlignment: MainAxisAlignment.center,
Children: []Widget{
Text(TextParams{
Text: "You have pushed the button this many times:",
}),
Text(TextParams{
Text: fmt.Sprintf("%d", m.counter),
Style: ctx.textTheme.display1,
}),
},
}),
}),
FloatingActionButton: NewFloatingActionButton(
FloatingActionButtonParameters{
OnPressed: m.incrementCounter,
Tooltip: "Increment",
Child: NewIcon(IconParams{
Icon: Icons.add,
}),
},
),
})
}
// 增量计数器给 app 的计数器加一
func (m *MyHomePage) incrementCounter() {
m.counter++
flutter.Rerender(m)
}
复制程式码
实际上我很喜欢它。
结语
与 Vecty 的相似点我不禁注意到,我的最终实现的结果跟 Vecty 框架所提供的非常相似。基本上,通用的设计几乎是一样的,都只是向 DOM/CSS 中输出,而 Flutter 则成熟地深入到底层的渲染层,用漂亮的小元件提供非常流畅的 120fps 体验(并解决了许多其他问题)。我认为 Vecty 的设计堪称典范,难怪我实现的结果也是一个“基于Flutter 的 Vecty 变种” :)
更好的理解 Flutter 的设计
这个实验思路本身就很有趣 —— 你不必每天都要为尚未实现的库/框架编写(并探索)程式码。但它也帮助我更深入的剖析了 Flutter 设计,阅读了一些技术文件,揭开了 Flutter 背后隐藏的魔法面纱。
Go 的不足之处
我对“ Flutter 能用 Go 来写吗?”的问题的答案肯定是能,但我也有一些偏激,没有意识到许多设计限制,而且这个问题没有标准答案。我更感兴趣的是探索 Dart 实现 Flutter 能给 Go 实现提供借鉴的地方。
这次实践表明主要问题是因为 Go 语法造成的。无法呼叫函式时传递命名引数或无型别的字面量,这使得建立简洁、结构良好的类似于 DSL 的元件树变得更加困难和复杂。实际上,在未来的 Go 中,有 Go 提议新增命名引数,这可能是一个向后相容的更改。有了命名引数肯定对 Go 中的 UI 框架有所帮助,但它也引入了另一个问题即学习成本,并且对每个函式定义或呼叫都需要考虑另一种选择,因此这个特性所带来的好处尚不好评估。
在 Go 中,缺少使用者定义的泛型或者缺少异常机制显然不是什么大问题。我会很高兴听到另一种方法,以更加简洁和更强的可读性来实现 Go 版的 Flutter —— 我真的很好奇有什么方法能提供帮助。欢迎在评论区发表你的想法和程式码。
关于 Flutter 未来的一些思考
我最后的想法是,Flutter 真的是无法形容的棒,尽管我在这篇文章中指出了它的缺点。在 Flutter 中,“awesomeness/meh” 帧率是惊人的高,而且 Dart 实际上非常易于学习(如果你学过其他程式语言)。加入 Dart 的 web 家族中,我希望有一天,每一个浏览器附带一个快速并且优异的 Dart VM,其内部的 Flutter 也可以作为一个 web 应用程序框架(密切关注 HummingBird 专案,本地浏览器支援会更好)。
大量令人难以置信的设计和优化,使 Flutter 的现状是非常火。这是一个你梦寐以求的专案,它也有很棒并且不断增长的社群。至少,这里有很多好的教程,并且我希望有一天能为这个了不起的专案作出贡献。
对我来说,它绝对是一个游戏规则的变革者,我致力于全面的学习它,并能够时不时地做出很棒的移动应用。即使你从未想过你自己会去开发一个移动应用,我依然鼓励你尝试 Flutter —— 它真的犹如一股清新的空气。
作者:掘金翻译计划
连结:https://juejin.im/post/5d215b8df265da1b7b31ac8f
来源:掘金
著作权归作者所有。商业转载请联络作者获得授权,非商业转载请注明出处。