Flutter 熱更新及動態UI生成

程式設計之路從0到1發表於2021-05-11

問題

由於Dart語言在Flutter上關閉了反射,且語言本身也缺乏動態能力,因此在Flutter上實現熱更新或動態UI較為困難。

目前已有的一些動態方案:

  • 利用原生框架更新
  • 橋接動態指令碼語言
  • 修改引擎(動態橋接增強版)
  • XML/JSON配置UI

以上方案,在我看來都不可取!原因這就來一一分析。

利用原生框架更新,實際上就是更新Flutter框架相關的二進位制。Flutter應用釋出出來的產物主要包括 libflutter.solibapp.soflutterAssets,這樣,就可以通過Android端原生平臺網路請求,動態下發並載入這些產物,從而實現動態更新。

那麼為什麼這種方案不可取呢?主要原因在於iOS 的應用商店不允許動態下發和載入二進位制產物,包括動態庫之類的,故此方案只能在Android端實現。我們學習和使用Flutter,其中最重要的一點就是要有跨平臺思維,當然,這要求我們技術面要足夠廣泛,不能像原生開發一樣侷限於一隅。我們思考一個Flutter技術方案,首要思考的是能不能通用,如果不能通用,只能在一端使用,那麼該方案就是沒有意義的。Flutter的優勢和生命力,在於跨平臺,任何方案,如果使Flutter失去跨平臺,那還不如使用原生技術開發,實現起來又成熟又快,還去瞎折騰Flutter幹啥,豈不是浪費生命?

不管走多遠,千萬別忘了我們為什麼出發的!

我們再來看一下橋接動態指令碼語言的方案。本質上就是打包一個動態指令碼語言Runtime,目前有打包JavaScript的,有打包Lua的。這些Runtime其實就是打包成C語言的一個動態庫,看起來可行,其實與Dart語言互動十分困難。Dart語言目前的FFI介面能力非常弱,並不像Java的JNI一樣成熟,通常只能Dart 呼叫C語言,C反過來呼叫Dart十分困難,因此它並不是互操作,只是單向呼叫。那麼,通過FFI橋接Dart語言與目標語言(JS或Lua)互操作基本不可取。

那麼再轉換一下方向,通過Java 的JNI繫結指令碼語言的Runtime是否可行呢?在Android原生上,通過JNI橋接Java語言與指令碼語言是一種可行的方案,做得比較好的代表有Python的Kivy框架。Kivy就可以通過Python程式碼動態生成UI介面,但是作為Flutter,目標語言是Dart而不是Java,指令碼語言能輕鬆反射Java類,但是與Dart互動仍然困難,要想互動,主要可以通過序列化,相當於Flutter外掛機制一樣,大量頻繁操作,效能是一個問題。而且程式碼也很缺乏靈活性。

其實在此方案之上,還有一種增強方案,那就是修改Flutter引擎。Dart語言虛擬機器本身是C++開發的,按理說,指令碼語言通過C/C++與Dart互動是很容易的,就像Java JNI一樣,這只是因為Dart的虛擬機器並未暴露內部的C++介面,通過修改引擎,暴露介面,可以方便指令碼語言直接呼叫Dart類。但此方案更不可取。目前Dart語言本身迭代就十分頻繁了,變化也大,且其虛擬機器十分複雜,可以遇見的,後續幾乎無法維護自己拉取的引擎分支。

最後,我們來看一下,基於XML/JSON配置檔案動態生成UI的方案。此方案對某些區域性樣式可能頻繁變化的介面,其實是可行的,相當於應用主題一樣。它的主要問題在於功能單一,只能針對特定模版的UI,且不能寫邏輯,不靈活。

探索

以上分析了一些主要的動態化更新思路,這裡給出我正在探索的解決方案。那就是 LuaDardo庫。

LuaDardo是我用Dart語言編寫的Lua虛擬機器,它的名字是由葡萄牙語的兩個單片語成,可以翻譯成“鏢中月”。

Lua本身是巴西人開發的一種以嵌入其他宿主語言為目標的簡潔的高效能指令碼語言,Lua在葡萄牙語中是月亮的意思。該語言多用於遊戲開發,用Lua指令碼編寫業務邏輯,然後呼叫底層C++遊戲引擎實現渲染。

Lua語言設計十分精巧,通過一個棧完成對宿主語言的互操作。LuaDardo直接使用Dart語言編寫Lua虛擬機器,這可以讓我們以高效能的方式完成Lua與Dart語言的互操作。只需要對Flutter的Widget進行一定封裝,將Dart類繫結到Lua語言中,即可使用Lua指令碼編寫UI介面。

另外,LuaDardo直接基於Dart開發,天然的具備跨平臺能力,只要有Flutter的地方,就能使用。即使是Flutter的桌面應用,亦可具備這種動態指令碼能力。

LuaDardo本身定位是基於Dart語言的虛擬機器,我們要用Lua寫Flutter介面,還需要對Flutter控制元件的封裝擴充套件。

flutter_lua_dardo 就是一個這樣的擴充套件庫。該庫主要用於包裝Flutter介面和控制元件,這使得Flutter能夠在需要經常改變UI風格的地方使用遠端指令碼動態地更新和生成介面。

請注意,flutter_lua_dardo 只是一個實驗性的探索。它只封裝了幾個簡單的Widget。歡迎其他人一起來探索,為Lua封裝更多的Widget。

另外的,LuaDardo庫已完成了大部分工作,只是Lua 自帶的標準庫尚未完全編寫完畢,協程庫、OS庫、IO庫等尚未開始。

例子

用法

新建 Lua 指令碼 test.lua:

function getContent1()
    return Row:new({
        children={
            GestureDetector:new({
                onTap=function()
                    flutter.debugPrint("--------------onTap--------------")
                end,

                child=Text:new("click here")}),
            Text:new("label1"),
            Text:new("label2"),
            Text:new("label3"),
        },
        mainAxisAlign=MainAxisAlign.spaceEvenly,
    })
end

function getContent2()
    return Column:new({
        children={
            Row:new({
                children={Text:new("Hello"),Text:new("Flutter")},
                mainAxisAlign=MainAxisAlign.spaceAround
            }),
            Image:network('https://gitee.com/arcticfox1919/ImageHosting/raw/master/img/flutter_lua_test.png'
                ,{fit=BoxFit.cover})
        },
        mainAxisSize=MainAxisSize.min,
        crossAxisAlign=CrossAxisAlign.center
    })
end
複製程式碼

新增依賴 pubspec.yaml

dependencies:
  flutter_lua_dardo: ^0.0.2
複製程式碼

新增Dart程式碼:

class _MyHomePageState extends State<MyHomePage> {
  LuaState _ls;
  bool isChange = false;
    
  @override
  void initState() {
    loadLua();
    super.initState();
  }
  
  // 載入Lua虛擬機器
  void loadLua() async {
    String src = await rootBundle.loadString('assets/test.lua');
    try {
      LuaState ls = LuaState.newState();
      ls.openLibs();
      FlutterLua.open(ls);
      FlutterWidget.open(ls);
      ls.doString(src);
      setState(() {
        _ls = ls;
      });
    } catch (e) {
      print(e);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        alignment: Alignment.center,
        child: _ls == null
            ? CircularProgressIndicator()
             // 呼叫Lua函式,建立UI
            : FlutterWidget.findViewByName<Widget>(
            _ls, isChange ? "getContent2" : "getContent1"),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(CupertinoIcons.arrow_swap),
        onPressed: (){
          setState(() {
            isChange = !isChange;
          });
        },
      ),
    );
  }
}
複製程式碼

要了解如何繫結Dart類到Lua,你可以檢視這個 示例.

關於 LuaDardo庫, 請檢視 這裡.

視訊課程

如需要了解博主的Flutter全棧式開發課程,請 點選跳轉


關注我的公眾號:程式設計之路從0到1

相關文章