[譯] 思考實踐:用 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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索