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

思考实践:用 Go 实现 Flutter

我最近发现了 Flutter —— 谷歌的一个新的移动开发框架,我甚至曾经将 Flutter 基础知识教给没有编程经验的人。Flutter 是用 Dart 编写的,这是一种诞生于 Chrome 浏览器的编程语言,后来改用到了控制台。这不由让我想到“Flutter 也许能够很轻易地用 Go 来实现”!html

为何不用 Go 实现呢?Go 和 Dart 都是诞生于谷歌(而且有不少的大会分享使它们变得更好),它们都是强类型的编译语言 —— 若是情形发生一些改变,Go 也彻底能够成为像 Flutter 这样热门项目的选择。而那时候 Go 会更容易地向没有编程经验的人解释或传授。前端

假如 Flutter 已是用 Go 开发的。那它的代码会是什么样的?android

VSCode 中 Go 版的 Flutter

Dart 的问题

自从 Dart 在 Chrome 中出现以来,我就一直在关注它的开发状况,我也一直认为 Dart 最终会在全部浏览器中取代 JS。2015 年,得知有关谷歌在 Chrome 中放弃 Dart 支持的消息时,我很是沮丧。ios

Dart 是很是奇妙的!是的,当你从 JS 升级转向到 Dart 时,会感受一切都还不错;可若是你从 Go 降级转过来,就没那么惊奇了,可是…… Dart 拥有很是多的特性 —— 类、泛型、异常、Futures、异步等待、事件循环、JIT、AOT、垃圾回收、重载 —— 你能想到的它都有。它有用于 getter/setter 的特殊语法、有用于构造函数自动初始化的特殊语法、有用于特殊语句的特殊语法等。git

虽然它让能让拥有其余语言经验的人更容易熟悉 Dart —— 这很不错,也下降了入门门槛 —— 但我发现很难向没有编程经验的新手讲解它。github

  • 全部“特殊”的东西易被混淆 —— “名为构造方法的特殊方法”,“用于初始化的特殊语法”,“用于覆盖的特殊语法”等等。
  • 全部“隐式”的东西使人困惑 —— “这个类是从哪儿导入的?它是隐藏的,你看不到它的实现代码”,“为何咱们在这个类中写一个构造方法而不是其余方法?它在那里,但是它是隐藏的”等等。
  • 全部“有歧义的语法”易被混淆 —— “因此我应该在这里使用命名或者对应位置的参数吗?”,“应该使用 final 仍是用 const 进行变量声明?”,“应该使用普通函数语法仍是‘箭头函数语法’”等等。

这三个标签 —— “特殊”、“隐式”和“歧义” —— 可能更符合人们在编程语言中所说的“魔法”的本质。这些特性旨在帮助咱们编写更简单、更干净的代码,但实际上,它们给阅读程序增长了更多的混乱和心智负担。golang

而这正是 Go 大相径庭而且有着本身强烈特点的地方。Go 其实是一个非魔法的语言 —— 它将特殊、隐式、歧义之类的东西的数量讲到最低。然而,它也有一些缺点。web

Go 的问题

当咱们讨论 Flutter 这种 UI 框架时,咱们必须把 Go 看做一个描述/指明 UI 的工具。UI 框架是一个很是复杂的主题,它须要建立一种专门的语言来处理大量的底层复杂性。最流行的方法之一是建立 DSL —— 特定领域的语言 —— 众所周知,Go 在这方面不那么尽如人意。编程

建立 DSL 意味着建立开发人员可使用的自定义术语和谓词。生成的代码应该能够捕捉 UI 布局和交互的本质,而且足够灵活,能够应对设计师的想象流,又足够的严格,符合 UI 框架的限制。例如,你应该可以将按钮放入容器中,而后将图标和文本小组件放入按钮中,可若是你试图将按钮放入文本中,编译器应该给你提示一个错误。windows

特定于 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”应用程序和代码文本、计数器和一个按钮,点击增长按钮,计数器会增长。

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<MyHomePage> {
  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: <Widget>[
            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 的人来讲,这多是一个不太好的状况,但也不尽然。这个世界是一个巨大的相互关联的事物和关系图谱。它不是混沌的,可也不是彻底的结构化,而且尝试将全部内容都放入类层次结构中可能会致使代码难以维护,到目前为止,世界上的大多数代码库都是这样子。

OOP 的真相

我喜欢 Go 的设计者们努力从新思考这个无处不在的基于 OOP 思惟,并提出了与之不一样的 OOP 概念,这与 OOP 的发明者 Alan Kay 所要表达的真实意义更接近,这不是偶然。

在 Go 中,咱们用一个具体的类型 —— 一个结构体来表示这种抽象:

type MyApp struct {
    // ...
}
复制代码

在一个 Flutter 的 Dart 版本中,MyApp必须继承于 StatelessWidget 类并覆盖它的 build 方法,这样作有两个做用:

  1. 自动地给予 MyApp 一些 widget 属性/方法
  2. 经过调用 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 有些不一样:

  • 代码更加冗长 —— BuildContextWidgetMaterialApp 等方法前都明显地提到了 flutter
  • 代码更简洁 —— 没有 extends Widget 或者 @override 子句。
  • Build 方法是大写开头的,由于在 Go 中它的意思是“公共”可见性。在 Dart 中,大写开头小写开头均可以,可是要使属性或方法“私有化”,名称须要使用下划线(_)开头。

为了实现一个 Go 版的 Flutter Widget,如今咱们须要嵌入 flutter.Core 并实现 flutter.Widget 接口。好了,很是清楚了,咱们继续往下实现。

状态

在 Dart 版的 Flutter 中,这是我发现的第一个使人困惑的地方。Flutter 中有两种组件 —— StatelessWidgetStatefulWidget。嗯,对我来讲,无状态组件只是一个没有状态的组件,因此,为何这里要建立一个新的类呢?好吧,我也能接受。可是你不能仅仅以相同的方式扩展 StatefulWidget,你应该执行如下神奇的操做(安装了 Flutter 插件的 IDE 均可以作到,但这不是重点):

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  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: <Widget>[
            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,
          <Widget>[
            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 —— 它真的犹如一股清新的空气。

Links

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索