[譯] 思考實踐:用 Go 實現 Flutter

掘金翻譯計劃發表於2019-07-07

思考實踐:用 Go 實現 Flutter

我最近發現了 Flutter —— 谷歌的一個新的移動開發框架,我甚至曾經將 Flutter 基礎知識教給沒有程式設計經驗的人。Flutter 是用 Dart 編寫的,這是一種誕生於 Chrome 瀏覽器的程式語言,後來改用到了控制檯。這不禁讓我想到“Flutter 也許可以很輕易地用 Go 來實現”!

為什麼不用 Go 實現呢?Go 和 Dart 都是誕生於谷歌(並且有很多的大會分享使它們變得更好),它們都是強型別的編譯語言 —— 如果情形發生一些改變,Go 也完全可以成為像 Flutter 這樣熱門專案的選擇。而那時候 Go 會更容易地向沒有程式設計經驗的人解釋或傳授。

假如 Flutter 已經是用 Go 開發的。那它的程式碼會是什麼樣的?

VSCode 中 Go 版的 Flutter

Dart 的問題

自從 Dart 在 Chrome 中出現以來,我就一直在關注它的開發情況,我也一直認為 Dart 最終會在所有瀏覽器中取代 JS。2015 年,得知有關谷歌在 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”應用程式和程式碼文字、計數器和一個按鈕,點選增加按鈕,計數器會增加。

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前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章