Flutter 通用基礎庫 flutter_luakit_plugin

收藏待读

Flutter 通用基礎庫 flutter_luakit_plugin

使用flutter_luakit_plugin作為基礎庫開發flutter應用

文章開頭我們先開門見山給出使用flutter_luakit_plugin作為基礎庫開發和普通flutter的區別。由於flutter定位是便攜UI包,flutter提供的基礎庫功能是不足以滿足複雜數據的app應用的,一般flutter開發模式如下圖所示,當flutter滿足不了我們的需求的時候,使用methodchannel和eventchannel調用native接口。

Flutter 通用基礎庫 flutter_luakit_plugin

image

而使用flutter_luakit_plugin作為基礎庫的開發模式如下圖所示,用lua來寫邏輯層代碼,用flutter寫UI代碼。luakit 提供了豐富的功能支持,可以支持大部分app的邏輯層開發,包括數據庫orm,線程管理,http請求,異步socket,定時器,通知,json等等。 用戶只需要寫dart代碼和lua代碼,不需要寫oc、swift或java、kotlin代碼,從而大幅提升代碼的一致性(所有運行代碼都是跨平台的)

Flutter 通用基礎庫 flutter_luakit_plugin

image

flutter_luakit_plugin由來

Flutter誕生的時候我很興奮,因為我對跨平台開發UI的看法一直是不看好的,最主要的原因是無法獲得體驗一致性,但是Flutter前無古人的解決了這個問題,真正做到一端開發的UI,無論多複雜,在另一端是可以得到一致的體驗的,做不到這點的跨平台UI方案實際上並沒有達到跨平台節省工作量的效果,Flutter做到了。

Flutter1.0.0 發佈了,我認為移動端跨平台開發所需要所有元素都已經齊備了,我們嘗試使用Flutter做一些功能,一個版本之後我們總結了一些問題。

  • Flutter是一套UI解決方案,但一個功能除了UI,還需要很多支持,網絡請求,長連接,短連接,數據庫,線程控制等等,這些方面Flutter生態中提供得比較差,沒有ios 或者android那麼多成熟的解決方案。Flutter 為了克服這問題,提供了一個解決方案,利用methodchannel和eventchannel調用ios和android的接口,利用原生成熟的方案做底層邏輯支撐。我們一開始也是這樣解決,但後續的麻煩也來了,由於methodchannel和eventchannel實現的方法是不跨平台的,Flutter從ios和android得到的數據的格式,事件調用的時機等,兩個平台的實現是不一樣的,基本不可能完全統一,可以這樣說,一個功能在一個端能跑通, 在另一個端第一次跑一定跑不通 ,然後就要花大量的時間進行調試,適配,這樣做之後跨平台的優勢蕩然無存,大家就會不斷扯皮。相信我,下面的對話會成為你們的日常。
ios開發:「你們android寫的界面ios跑不起來」

Android 開發:「我們android能跑啊,iOS接口寫得不對吧」

ios開發:「哪裡不對,android寫的界面,android幫忙調吧」

Android 開發:「我又不是ios開發,我怎麼調」
  • 當一個已有的app要接入flutter,必然會產生一種情況,就是flutter體系裏面的數據和邏輯,跟外部原生app的邏輯是不通的,簡單說明一下,就是flutter寫的業務邏輯通常是用dart語言寫的,我們在原生用object-c、swift或者java、kotlin寫的代碼是不可以脫離flutter的界面調用dart寫的邏輯的,這種互通性的缺失,會導致很多數據聯動做不到,譬如原生界面要現實一個flutter頁面存下來的數據,或者原生界面要為flutter頁面做一些預加載,這些都很不方便,主要是下圖中,當flutter界面沒調用時,從原生調用flutter接口是不允許的。
Flutter 通用基礎庫 flutter_luakit_plugin

image

之前我曾經開源一個純邏輯層的跨平台解決方案 luakit (附上luakit的起源),裏面提供一個業務開發所需要的基本能力,包括網絡請求,長連接,短連接,orm數據庫,線程,通知機制等等,而且這些能力都是穩定的、跨平台而且經過實際業務驗證過的方案。

做完一個版本純flutter之後,我意識到可以用一種新的開發模式來進行flutter開發,這樣可以避免我上面提到的兩個問題,我們團隊馬上付諸實施,做了另一個版本的flutter+luakit的嘗試,即用flutter做界面,用lua來寫邏輯,結構圖如下。

Flutter 通用基礎庫 flutter_luakit_plugin

image

新的方案開發效率得到極大的提升,不客氣的說真正實現了跨平台,一個業務,從頁面到邏輯,所有的代碼一氣呵成全部由腳本完成(dart+lua),完全不用object-c、swift或者java、kotlin來寫邏輯,這樣一個業務基本就可以無縫地從一端直接搬到另一端使用,所以我寫了這篇文章來介紹我們團隊的這個嘗試,也把我們的成果 flutter_luakit_plugin 開源了出來,讓這種開發模式幫助到更多flutter開發團隊。

細說開發模式

下一步我們一起看看如何用flutter配合lua實現全部代碼都是跨平台的。我們提供了一個 demo project ,供大家參考。

  • dart寫界面

    在demo中所有的ui都寫在了 main.dart ,當然在真實業務中肯定複雜很多,但是並不影響我們的開發模式。

  • dart調用lua邏輯接口

FlutterLuakitPlugin.callLuaFun("WeatherManager", "getWeather").then((dynamic d) {
  print("getWeather" + d.toString());
  setState(() {
    weathers = d;
  });
});

上面這段代碼的意思是調用WeatherManager的lua模塊,裏面提供的getWeather方法,然後把得到的數據以future的形式返回給dart,上面的代碼相當於調用下面一段lua代碼

require('WeatherManager').getWeather( function (d) 
   
end)

然後剩下的事情就到lua,在lua裏面可以使用luakit提供的所有強大功能,一個app所需要的絕大部分的功能應該都提供了,而且我們還會不斷擴展。

大家可能會擔心dart和lua的數據格式轉換問題,這個不用擔心,所有細節在flutter_luakit_plugin都已經做好封裝,使用者儘管像使用dart接口那樣去使用lua接口即可。

  • 在lua中實現所有的非UI邏輯

    這個 demo(WeatherManager.lua) 已經演示了如何使用luakit的相關功能,包括,網絡,orm數據庫,多線程,數據解析,等等

  • 如果實在有flutter_luakit_plugin沒有支持的功能,可以走回flutter提供的methodchannel和eventchannel的方式實現

如何接入flutter_luakit_plugin

經過了幾個月磨合實踐,我們團隊已經把接入flutter_luakit_plugin的成本降到最低,可以說是非常方便接入了。我們已經把flutter_luakit_plugin發佈到flutter官方的插件倉庫。首先,要像其他flutter插件一樣,在pubspec.yaml裏面加上依賴,可參考 demo配置

flutter_luakit_plugin: ^1.0.0

然後在ios項目的podfile加上ios的依賴,可參考 demo配置

source 'https://github.com/williamwen1986/LuakitPod.git'
source 'https://github.com/williamwen1986/curl.git'
pod 'curl', '~> 1.0.0'
pod 'LuakitPod', '~> 1.0.13'

然後在android項目app的build.gradle文件加上android的依賴,可參考 demo配置

repositories {
    maven { url "https://jitpack.io" }
}

dependencies {
    implementation 'com.github.williamwen1986:LuakitJitpack:1.0.6'
}

最後,在需要使用的地方加上import就可以使用lua腳本了

import 'package:flutter_luakit_plugin/flutter_luakit_plugin.dart';

lua腳本我們默認的執行根路徑在android是 assets/lua,ios默認的執行根路徑是Bundle路徑。

flutter_luakit_plugin開發環境IDE–AndroidStudio

flutter 官方推薦的IDE是androidstudio和visual studio code。我們在開發中覺得androidstudio更好用,所有我們同步也開發了luakit的androidstudio

插件,名字就叫luakit。luakit插件提供了以下的一些功能。

  • 遠程lua調試
  • 查找函數使用
  • 跳到函數定義
  • 跳到文件
  • 參數名字提示
  • 代碼自動補全
  • 代碼格式化
  • 代碼語法檢查
  • 標準lua api自動補全
  • luakit api自動補全

大部分功能,跟其他IDE沒太多差別,這裡我就不細講了,我重點講一下遠程lua調試功能,因為這個跟平時調試ios和android設備有點不一樣,下面我們詳細介紹androidstudio luakit插件的使用。

androidstudio安裝luakit插件

AndroidStudio->Preference..->Plugins->Browse reprositories…

Flutter 通用基礎庫 flutter_luakit_plugin

image

搜索Luakit並安裝Luakit插件然後重啟androidstudio

Flutter 通用基礎庫 flutter_luakit_plugin

image

配置lua項目

打開 Project Struture 窗口

[圖片上傳失敗…(image-f34c9e-1547005486526)]

選擇 Modules、 Mark as Sources

Flutter 通用基礎庫 flutter_luakit_plugin

image

添加調試器

選擇 Edit Configurations …

Flutter 通用基礎庫 flutter_luakit_plugin

image

Select plus

Flutter 通用基礎庫 flutter_luakit_plugin

image

添加Lua Remote(Mobdebug)

Flutter 通用基礎庫 flutter_luakit_plugin

image

遠程lua調試

在開始調試lua之前,我們要在需要調試的lua文件加上下面一句lua代碼。然後設上斷點,即可調試。lua代碼裏面有兩個參數,第一個是你調試用的電腦的ip地址,第二個是調試端口,默認是8172。

require("mobdebug").start("172.25.129.165", 8172)
Flutter 通用基礎庫 flutter_luakit_plugin

image

luakit的調試是通過socket來傳遞調試信息的,所有調試機器務必我電腦保持在同一網段,有時候可能做不到,這裡我們給出一下辦法解決,我們日常調試也是這樣解決的。首先讓你的手機開熱點,然後你的電腦連上手機的熱點,現在就可以保證你的手機和電腦是同一網段了,然後查看電腦的ip地址,填到lua代碼上,就可以實現調試了。

Flutter 通用基礎庫 flutter_luakit_plugin

image

Flutter 通用基礎庫 flutter_luakit_plugin

image

flutter_luakit_plugin提供的api介紹

(1) 數據庫orm操作

這是flutter_luakit_plugin裏面提供的一個強大的功能,也是flutter現在最缺的,簡單高效的數據庫操作,flutter_luakit_plugin提供的數據庫orm功能有以下特徵

  • 面向對象
  • 自動創建和更新表結構
  • 自帶內部對象緩存
  • 定時自動transaction
  • 線程安全,完全不用考慮線程問題

具體可參考 demo lua ,下面只做簡單介紹。

定義數據模型

-- Add the define table to dbData.lua
-- Luakit provide 7 colum types
-- IntegerField to sqlite integer 
-- RealField to sqlite real 
-- BlobField to sqlite blob 
-- CharField to sqlite varchar 
-- TextField to sqlite text 
-- BooleandField to sqlite bool
-- DateTimeField to sqlite integer
user = {
    __dbname__ = "test.db",
    __tablename__ = "user",
    username = {"CharField",{max_length = 100, unique = true, primary_key = true}},
    password = {"CharField",{max_length = 50, unique = true}},
    age = {"IntegerField",{null = true}},
    job = {"CharField",{max_length = 50, null = true}},
    des = {"TextField",{null = true}},
    time_create = {"DateTimeField",{null = true}}
    },
-- when you use, you can do just like below
local Table = require('orm.class.table')
local userTable = Table("user")

插入數據

local userTable = Table("user")
local user = userTable({
    username = "user1",
    password = "abc",
    time_create = os.time()
})
user:save()

更新數據

local userTable = Table("user")
local user = userTable.get:primaryKey({"user1"}):first()
user.password = "efg"
user.time_create = os.time()
user:save()

刪除數據

local userTable = Table("user")
local user = userTable.get:primaryKey({"user1"}):first()
user:delete()

批量更新數據

local userTable = Table("user")
userTable.get:where({age__gt = 40}):update({age = 45})

批量刪除數據

local userTable = Table("user")
userTable.get:where({age__gt = 40}):delete()

select數據

local userTable = Table("user")
local users = userTable.get:all()
print("select all -----------")
local user = userTable.get:first()
print("select first -----------")
users = userTable.get:limit(3):offset(2):all()
print("select limit offset -----------")
users = userTable.get:order_by({desc('age'), asc('username')}):all()
print("select order_by -----------")
users = userTable.get:where({ age__lt = 30,
    age__lte = 30,
    age__gt = 10,
    age__gte = 10,
    username__in = {"first", "second", "creator"},
    password__notin = {"testpasswd", "new", "hello"},
    username__null = false
    }):all()
print("select where -----------")
users = userTable.get:where({"scrt_tw",30},"password = ? AND age < ?"):all()
print("select where customs -----------")
users = userTable.get:primaryKey({"first","randomusername"}):all()
print("select primaryKey -----------")

聯表操作

local userTable = Table("user")
local newsTable = Table("news")
local user_group = newsTable.get:join(userTable):all()
print("join foreign_key")
user_group = newsTable.get:join(userTable,"news.create_user_id = user.username AND user.age < ?", {20}):all()
print("join where ")
user_group = newsTable.get:join(userTable,nil,nil,nil,{create_user_id = "username", title = "username"}):all()
print("join matchColumns ")

(2) 通知機制

通知機制提供了一個低耦合的事件互通方法,即在原生或者lua或者dart註冊消息,在任何地方拋出的消息都可以接收到。

Flutter 添加監聽消息

void notify(dynamic d) {

}

FlutterLuakitPlugin.addLuaObserver(3, notify);

Flutter 取消監聽

FlutterLuakitPlugin.removeLuaObserver(3, notify);

Flutter拋消息

FlutterLuakitPlugin.postNotification(3, data);

lua 添加監聽消息 demo code

local listener

lua_notification.createListener(function (l)
    listener = l
    listener:AddObserver(3,
        function (data)
            print("lua Observer")
            if data then
                for k,v in pairs(data) do
                    print("lua Observer"..k..v)
                end
            end
        end
    )
end);

lua拋消息 demo code

lua_notification.postNotification(3,
{
    lua1 = "lua123",
    lua2 = "lua234"
})

ios 添加監聽消息 demo code

_notification_observer.reset(new NotificationProxyObserver(self));
_notification_observer->AddObserver(3);
- (void)onNotification:(int)type data:(id)data
{
    NSLog(@"object-c onNotification type = %d data = %@", type , data);
}

ios拋消息 demo code

post_notification(3, @{@"row":@(2)});

android 添加監聽消息 demo code

LuaNotificationListener  listener = new LuaNotificationListener();
INotificationObserver  observer = new INotificationObserver() {
    @Override
    public void onObserve(int type, Object info) {
        HashMap map = (HashMap)info;
        for (Map.Entry entry : map.entrySet()) {
            Log.i("business", "android onObserve");
            Log.i("business", entry.getKey());
            Log.i("business",""+entry.getValue());
        }
    }
};
listener.addObserver(3, observer);

android拋消息 demo code

HashMap map = new HashMap();
map.put("row", new Integer(2));
NotificationHelper.postNotification(3, map);

(3) http request

flutter本身提供了http請求庫dio,不過當項目的邏輯接口想在flutter,原生native都可用的情況下,flutter寫的邏輯代碼就不太合適了,原因上文已經提到,原生native是不可以隨意調用flutter代碼的,所以遇到這種情況,只有luakit合適,lua寫的邏輯接口可以在所有地方調用,flutter 、ios、android都可以方便的使用lua代碼,下面給出luakit提供的http接口, demo code

-- url , the request url
-- isPost, boolean value represent post or get
-- uploadContent, string value represent the post data
-- uploadPath,  string value represent the file path to post
-- downloadPath, string value to tell where to save the response
-- headers, tables to tell the http header
-- socketWatcherTimeout, int value represent the socketTimeout
-- onResponse, function value represent the response callback
-- onProgress, function value represent the onProgress callback
lua_http.request({ url  = "http://tj.nineton.cn/Heart/index/all?city=CHSH000000",
    onResponse = function (response)
    end})

(4) Async socket

異步socket長連接功能也是很多app開發所依賴的,flutter只支持websocket協議,如果app想使用基礎的socket協議,那就要使用flutter_luakit_plugin提供的socket功能了,使用也非常簡單, demo code ,在callback裏面拿到數據後可以使用上文提到的通知機制把數據傳回到flutter層。

local socket = lua_asyncSocket.create("127.0.0.1",4001)

socket.connectCallback = function (rv)
    if rv >= 0 then
        print("Connected")
        socket:read()
    end
end
    
socket.readCallback = function (str)
    print(str)
    timer = lua_timer.createTimer(0)
    timer:start(2000,function ()
        socket:write(str)
    end)
    socket:read()
end

socket.writeCallback = function (rv)
    print("write" .. rv)
end

socket:connect()

(5) json 解析

json是最常用數據類型,使用可參考 demo

local t = cjson.decode(responseStr)

responseStr = cjson.encode(t)

(6) 定時器timer

定時器也是項目開發中經常用到的一個功能,定時器我們在orm框架的lua源碼裏面有用到, demo

local _timer

_timer = lua_timer.createTimer(1)//0代表單次,1代表重複

_timer:start(2000,function ()
    
end)

_timer:stop()

(7) 還有所有普通適合lua用的庫都可以在flutter_luakit_plugin使用

flutter技術積累相關鏈接

flutter通用基礎庫flutter_luakit_plugin

flutter_luakit_plugin使用例子

《手把手教你編譯Flutter engine》

《手把手教你解決 Flutter engine 內存漏》

修復內存泄漏後的flutter engine(可直接使用)

修復內存泄漏後的flutter engine使用例子

持續更新中…

原文 : 簡書

相關閱讀

免责声明:本文内容来源于簡書,已注明原文出处和链接,文章观点不代表立场,如若侵犯到您的权益,或涉不实谣言,敬请向我们提出检举。