[译] 思考实践:用 Go 实现 Flutter

// 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 框架中使用很长时间,比如 GTKQt。可以到最新的 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 —— 它真的犹如一股清新的空气。

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
阿里一直到现在。**

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-Twhb6CPa-1715791503309)]

[外链图片转存中…(img-MeXxcPvO-1715791503311)]

[外链图片转存中…(img-zFZ87gOU-1715791503313)]

[外链图片转存中…(img-xHLFGlNY-1715791503314)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 12
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值