Web前端工程師實現Native APP需求,用flutter做可攻可守的混合開發

語言: CN / TW / HK

導讀:近些年來前端技術不斷推陳出新,使得web前端工程師開發Native App的成本逐漸降低,跨端技術也使得應用開發成本大幅度降低。這種背景下,越來越多的web前端工程師團隊正在逐漸嘗試獨立開發App,使用各種綜合性方案做著效能與效率上的平衡。

開發一款Native APP往往都是一件複雜的事情,現在市面上大多數的APP都不是單純使用某種技術研發而成,而是隨著專案需求的複雜程度、迭代效率、團隊具體情況等諸多原因的影響逐步發展成為各種技術混合開發的APP架構。

技術無分好壞,適合自己團隊的才是最合適的技術選型。啥意思?我來說說吧,比如你的團隊幾乎都使用vue開發,你非要選擇使用React Native全員開發APP,成本就很大也很難推動;又比如你的團隊客戶端研發工程師很多,那就沒必要非得用React Native,特別不人道。這就好像這麼多年來,人們一直倡導通過優化工具提升效率,而不是直接通過技術幹掉那些人。

那麼,什麼是合適的技術選型?我覺得對於一個團隊,一方面能夠滿足需求優化體驗,另一方面就是降本提效簡單靈活。

為了幫助Web前端工程師實現APP需求理清楚知識脈絡,本文將簡單介紹開發Native APP常用的技術方法,再介紹如何用flutter做可攻可守的混合開發。

01

純原生開發

移動端APP能夠帶來很好的體驗,能夠利用很多系統能力加持使之密閉生態發展迅速。但是使用原生方式(Native)來開發 App,不僅要求分別針對 iOS 和 Android 平臺,使用不同的語言實現同樣的產品功能,還要對不同的終端裝置和不同的作業系統進行功能適配,並承擔由此帶來的測試維護升級工作。

圖:原生開發App語言

02

純跨端方案

為了減少開發維護成本,近年來很多“一套程式碼,多端執行”的跨平臺方案猶如雨後春筍般的湧現出來,跨端方案針對不同業務目標,有Native APP的跨端,有小程式的跨端,也有橫向的PC端移動端和web的跨端,這些技術方案都有各自的優缺點,其中 React Native 和 Flutter 都是非常具有代表性的技術方案。

圖:跨端檢視效果

1、React Native

RN 希望開發者能夠在效能、展示、互動能力和迭代交付效率之間做到平衡。它在 Web 容器方案的基礎上,優化了載入、解析和渲染這三大過程,以相對簡單的方式支援了構建移動端頁面必要的 Web 標準,保證了便捷的前端開發體驗。並且在保留基本渲染能力的基礎上,用原生自帶的 UI 元件實現代替了核心的渲染引擎,從而保證了良好的渲染效能。

圖:RN 原理圖

但是,由於 React Native 的技術方案所限,使用原生控制元件承載介面渲染,在犧牲了部分 Web 標準靈活性的同時,固然解決了不少效能問題,但也引入了新的問題。除開通過 JavaScript 虛擬機器進行原生介面的呼叫,JavaScript只能解釋執行,而帶來的通訊低效不談,由於框架本身不負責渲染,而是由原生代理,因此我們還需要面對大量平臺相關的邏輯。要用好 React Native,除了掌握這個框架外,開發者還必須同時熟悉 iOS 和 Android 系統。

2、Flutter

在 Google 的強力帶動下,Flutter 開闢了全新的思路,提供了一整套從底層渲染邏輯到上層開發語言的完整解決方案。檢視渲染完全閉環在其框架內部,一切皆widget,不依賴於底層作業系統提供的任何元件,依靠底層影象渲染引擎 Skia從根本上保證了檢視渲染在 Android 和 iOS 上的高度一致性。

圖:Flutter 架構圖

Flutter 的開發語言 Dart,是 Google 專門為大前端開發量身打造的專屬語言,藉助於先進的工具鏈和編譯器,成為了少數同時支援JIT和AOT的語言之一,開發期除錯效率高,釋出期執行速度快、執行效能好,在程式碼執行效率上可以媲美原生 App。Dart 避免了搶佔式排程和共享記憶體,可以在沒有鎖的情況下進行物件分配和垃圾回收,在效能方面表現相當不錯。雖然 Dart 開發語言有一定的學習成本,但是學習成本並不高,很容易上手。

2022年3月 Flutter 2 釋出,聲稱使用相同的程式碼庫為 iOS、Android、Windows、macOS 和 Linux 五種作業系統構建原生應用,甚至可以嵌入到汽車、電視和智慧家電,為環境計算提供最普適、可移植的體驗。

圖:跨端方案的對比圖

從 Web 容器時代到以 React Native 為代表的泛 Web 容器時代,最後再到以 Flutter 為代表的自繪引擎時代,這些優秀的跨平臺開發框架們慢慢抹平了各個平臺的差異,使得作業系統的邊界變得越來越模糊。

03

混合開發

混合開發也稱為Hybird開發方式,是一種非常高效的APP開發方式,主要通過多種技術共同研發同一個APP應用,讓專業的人做專業的部分,在技術、效率、能力、目標之間做平衡,達到核心部分體驗優秀、部分功能迭代迅速成本低的開發方案。目前大多數APP也都是採用的多技術共用的開發方式,比如淘寶、美團、百度等APP都有混合開發的影子。

下面介紹幾種常見的混合開發方案。

1、原生 + webview 方案

這個方案主要是通過原生開發出Native APP的外殼,內部的頁面和功能主要通過常規的web技術實現,系統端能力通過原生殼子暴露出API給web實現能力呼叫。

這種技術對於不同的業務側重點會有些不同,如果客戶端主導的業務native會重一些,如果是純web前端的團隊web實現會非常多。這個主要是根據團隊現狀和業務需要做實現上的調整,如果一個部分非常可能會成為未來核心業務則大概率會使用Native及時實現或者重構。

圖:原生 + Webview 方案

2、原生 + Flutter方案

原生APP的開發成本是非常高的,就像前文所說的同一個APP需要分別針對 iOS 和 Android 平臺進行開發,也就是說有幾種平臺就需要幾個技術方向的人力,這樣的產品實現是非常消耗人力的。所以人們一直在尋找合適的方案一次開發就能夠執行在不同的平臺上,Flutter 就是 Google 開發的優秀的跨端開發Native APP技術方案,通過使用統一的程式語言 Dart ,根據一切皆 widget 的結構組織,通過觸發自研渲染引擎Skia在系統應用中繪製頁面,能夠達到無差異的渲染效果。

無差異的繪製UI和互動表現,這也是Flutter最強大的地方,對於UI檢視的渲染和互動體驗基本上與原生開發的表現差距不大,但是能夠節省大約1 / 2的人力,這樣的降本提效方法十分受到歡迎。

圖:原生 + Flutter 通訊互動圖

圖:Flutter原生混編

當然,不同專案的面臨的情況也不同,我們主要介紹的是以flutter為主的開發結構,但是很多業務原來使用原生開發的,後來為了實現一些低成本的需求開發,所以在原生開發的結構上接入 Flutter 技術進行混編開發。

3、原生 + Flutter + Webview 方案

如果團隊中有很多易變的需求,或者說團隊中純web前端成員佔據大多數,那麼引入 webview 是一個不錯的方案,雖然 webview 也存在很多問題,但是相對來說比起 RN 還是容易定位和解決的。

圖:原生 + Flutter + Webview

常見webview外掛依賴於Flatter嵌入Android和 iOS本機檢視的機制,底層使用AndroidView和UiKitView。iOS12版本已經廢棄UIWebView強推WKWebView,WKWebView 在獨立程序中載入網頁。其中 webview_flutter 是官方維護的 WebView外掛,特性是基於原生和 Flutter SDK封裝,繼承 StatefulWidget,因此支援內嵌於 flutter Widget 樹中,這是比較靈活的。

圖:Flutter Webview外掛對比

04

用Flutter做混合開發的經驗

我們的專案包括APP必備的開屏,然後是登陸頁面,登入成功後是載入頁面,然後開啟首頁,在首頁可以開啟直播浮窗,跳轉到大多數頁面直播浮窗都懸浮在那裡,包括首頁和其他頁面都是迭代頻率非常高的。

圖:專案需求

假設我們有一個 APP 專案,你的團隊大部分人都是 vue 的深度依賴開發者,專案初期很多需求可能都會不斷底推倒重來,需求迭代頻率非常高,但是也需要一定程度的保證使用者體驗,並且支援多個平臺。這種背景下,使用 flutter + webview + 原生 的開發方案確實能夠順利的完成任務。

那麼,根據專案需求我們可以簡單劃分各個部分設計。例如開屏、登入和載入頁面的變更頻率不高而且使用者體驗要求平穩順滑,這裡可以採用 flutter 技術來實現;對於1對1視訊通話部分可以考慮使用 RTC 和 webRTC 技術,而原生方式接入 sdk 和實現需求的穩定性和延時是比較可靠的,所以使用原生方式實現;其他的頁面如果面臨業務初級階段、變化頻率高等情況,我們可以考慮使用 web 技術實現(H5),如果存在對於客戶端能力或者系統能力的呼叫,可以使用 Jsbridge 中間層實現。

圖:設計簡圖

1、結構實現

因為是從 0 到 1 的專案,我們選擇建立一個flutter專案,按照設計的結構進行研發。專案入口在 main.dart 中實現;通過 routers.dart 的業務邏輯實現頁面間的切換;具體的頁面實現在 pages 目錄中相應的同名檔案;UI 元件位於 widgets 中,可以通過 import 進行使用;熟悉客戶端研發的同學們會發現,在flutter專案裡有 Android 和 iOS 目錄,原生開發在相應的原生目錄中實現就可以;flutter擴充套件外掛可以通過引用放置到plugins目錄中,具體的配置在 pubspec.yaml 檔案中實現。

app-project
|----android // Android原生目錄
| |--gradle
| |--build.gradle // Android配置檔案
| |--key.jks
| |--key.properties
| |--app
| |--libs
| |--src
| |--profile
| | |--AndroidManifest.xml
| |--main
| |--kotlin
| |--res
| |--AndroidManifest.xml
|----ios
| |--Gemfile
| |--Podfile // ios 的pod 依賴檔案
| |--Flutter
| |--scripts
| | |--AppIcon.sh
| | |--flutterbuild.sh
| | |--setup.sh
| |--Runner
| |--Info.plist
| |--main.m
| |--Assets.xcassets
| |--Base.lproj
| |--AppDelegate.h
| |--AppDelegate.m
|----lib // Flutter 工作區
| |--assets // 本地的資源工作區
| |--config
| |--jsBridge // 提供webview jsbrige
| | |--live
| | |--login
| | |--cache
| |--pages // flutter 頁面
| | |--live
| | |--loading
| | |--webViewPage
| | |--login
| |--routers
| | |--routers.dart
| |--utils
| | |--cache.dart
| | |--color.dart
| | |--network.dart
| | |--events.dart
| |--widgets
| | |--routers.dart
| |--main.dart // 入口檔案
|----plugins
| |--flutter-inappwebview //webview庫
| |--flutter-login
|----test
|----web
|----pubspec.yaml // flutter 配置檔案




1.1、頁面實現

如果你有過原生系統(Android、iOS)或原生 JavaScript 開發經驗的話,應該知道檢視開發是命令式的,需要精確地告訴作業系統或瀏覽器用何種方式去做事情。與此不同的是,Flutter 的檢視開發是宣告式的,其核心設計思想就是將檢視和資料分離,這與 React 的設計思路完全一致。例如程式碼實現如下:

// 頁面實現事例
import 'package:flutter/widgets.dart';
class MyAPP extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const Center(child: Text('Hello Qun'));
}
}
void main() => runApp(new MyAPP());

當你所要構建的使用者介面不隨任何狀態資訊的變化而變化時,需要選擇使用 StatelessWidget,反之則選用 StatefulWidget。 渲染也非常有意思,Widget 是 Flutter 世界裡對檢視的一種結構化描述,裡面儲存的是有關檢視渲染的配置資訊; Element 則是 Widget 的一個例項化物件,將 Widget 樹的變化做了抽象,能夠做到只將真正需要修改的部分同步到真實的 Render Object 樹中,最大程度地優化了從結構化的配置資訊到完成最終渲染的過程; 而 RenderObject,則負責實現檢視的最終呈現,通過佈局、繪製完成介面的展示。

圖:介面生成的“三棵樹”

Dart 是單執行緒的,這意味著程式碼是有序的,按照在 main 函數出現的次序一個接一個地執行,不會被其他程式碼中斷。關於元件層面的原始指標事件的監聽,Flutter 提供了 Listener Widget,可以監聽其子 Widget 的原始指標事件。

// 事件響應
Listener(
child: Container(
color: Colors.yellow,// 背景色
width: 700,
height: 700,
),
// 手勢按下回調
onPointerDown: (event) => print("down $event"),
// 手勢移動回撥
onPointerMove: (event) => print("move $event"),
// 手勢擡起回撥
onPointerUp: (event) => print("up $event")
);


1.2、路由設定

如果說 UI 框架的檢視元素的基本單位是元件,那應用程式的基本單位就是頁面了。對於擁有多個頁面的應用程式而言,需要有一個統一的機制來管理頁面之間的跳轉,通常被稱為路由管理或導航管理。

在 Flutter 中,頁面之間的跳轉是通過 Route 和 Navigator 來管理的。根據是否需要提前註冊頁面識別符號,Flutter 中的路由管理可以分為兩種方式:

  • 基本路由:無需提前註冊,在頁面切換時需要自己構造頁面例項。

// 基本路由
class FirstPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton(
// 開啟頁面
onPressed: ()=> Navigator.push(context, MaterialPageRoute(
builder: (context) => SecondPage()
));
);
}
}
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton(
// 回退頁面
onPressed: ()=> Navigator.pop(context)
);
}
}
  • 命名路由:需要提前註冊頁面識別符號,在頁面切換時通過識別符號直接開啟新的路由。

// 命名路由
MaterialApp(
...
// 註冊路由
routes:{
"uri_page":(context)=>UriPage(),
},
);
// 使用名字開啟頁面
Navigator.pushNamed(context,"uri_page");

1.3、外掛引用

依賴庫的依賴是通過pubspec.xml中配置完成。

// 版本配置,iOS最終是以identify中的版本配置為準
version: 1.0.30+1
// flutter sdk環境配置
environment:
sdk: ">=2.12.0-0 <3.0.0"
flutter: ">=1.22.2"
// 依賴配置
dependencies:
flutter:
sdk: flutter
permission_handler: ^7.1.0
flutter_inappwebview:
path: ./plugins/flutter_inappwebview
joymo_app_upgrade:
path: ./plugins/joymo_app_upgrade
tal_login:
path: ./plugins/tal_login
student_101_live:
path: ./plugins/student_101_live
dev_dependencies:
flutter_test:
sdk: flutter


1.4、原生接入

開發過程不免需要原生底層能力的支援,上文已經介紹了使用platform channel方式完成對接,這裡用程式碼示例下(原生的直播能力如何對接,以單獨的外掛StudentLive為例子)。

// 定義一個 flutter 側的 名稱為"student_101_live"的MethodChannel 
static const MethodChannel _channel =
const MethodChannel('student_101_live');


// 展示原生端直播UI
static Future<void> showLive(Map<String,dynamic>? map) async {

try{
// 呼叫原生方法名為:"showLiveDialog"方法
await _channel.invokeMethod('showLiveDialog',map);
}on MissingPluginException catch(e){
print('MissingPluginException, please check plugin has been registered or not,error message:${e.message}');
}on PlatformException catch(e){
print('invoke method log---showLiveDialog failed,error message:${e.message}');
}
}


iOS側的程式碼,每個plugin自動編譯會生成一個對應的Student101LivePlugin類:

// 這個plugin類是模版編譯生成,繼承自FlutterPlugin
@implementation Student101LivePlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
// 定義一個和flutter側相同名稱的"student_101_live" 的FlutterMethodChannel
FlutterMethodChannel* channel = [FlutterMethodChannel
methodChannelWithName:@"student_101_live"
binaryMessenger:[registrar messenger]];


Student101LivePlugin* instance = [[Student101LivePlugin alloc] init];
// 設定一個對應dart的methodDelegate
[registrar addMethodCallDelegate:instance channel:channel];

}


- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
// 判斷是否和flutter側的方法對應起來
if ([@"showLiveDialog" isEqualToString:call.method]) {
// 這裡method delegate的處理,完成原生方法的呼叫
if (call.arguments != nil) {
[self initLiveView:call.arguments];
}
result(@"iOS showLiveDialog");
}else {
result(FlutterMethodNotImplemented);
}
}

上面就已經完成了原生的接入,那這個studen101LivePlugin 如何完成註冊的呢,系統已經幫忙做了,檢視ios/Runner/GeneratedPluginRegistrant.m,可以看到依賴的外掛都被註冊,這樣你只用關注業務程式碼的編寫即可。

@implementation GeneratedPluginRegistrant
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
[Student101LivePlugin registerWithRegistrar:[registry registrarForPlugin:@"Student101LivePlugin"]];
[FLTURLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTURLLauncherPlugin"]];
}




總結一下: 在fluttter 側和原生側分別建立一個名稱相同的channel,然後再根據channel下的各個方法完成對接的呼叫,是不是很簡單。

1.5、web接入

在 APP 中使用H5進行頁面編寫的方案往往包括 web 資源離線方案和 server資源方案,這兩種方案最大的區別就是前者的靜態資源已經下發到APP中可以離線使用,後者需要通過向 web 服務端請求資源返回後通過webview進行渲染展現。

1.5.1、web 資源離線方案

通過為 webview 指定訪問網站的 URL ,通過向服務端請求頁面進行渲染。

1.5.2、service資源方案

離線方案往往與線上方案相結合,能夠提供更好的使用者體驗和也不需要服務端承載大量的訪問壓力。

圖:離線化web方案

前端應用(Web)保持原有的開發部署流程,僅通過webpack plugin修改打包流程,建立與配置平臺的聯絡,並生成壓縮檔案,上傳到CDN。這就讓現有的H5應用方便的具備離線包的能力,後續新開發的離線應用也可以同時具備靜態釋出的能力。

離線包配置平臺(Configuration Platform)用於管理各個離線包應用,包括對應用的增刪改查,版本管理,配置檢視,設定檢查更新的url等功能,由webpack plugin生成的配置會通過介面入庫。每次版本更新都會儲存下來,通過上線操作生效。

APP後端(APP Server)主要提供APP側離線包配置查詢的介面,也包含ios和android離線包開啟與否的開關,在配置平臺設定生效的離線包應用會以列表的形式提供給APP使用。

客戶端(APP)通過介面獲取離線包清單,如果某個應用版本需要更新,先將本地包資源刪除,再進行更新。通過整體考量,目前我們沒有做增量更新和差量包的維護,為了 減少應用體積過大帶來的更新問題,我們提供分包的配置,可以分步實現離線化。在開啟離線包功能的WebView中,對所有資源請求進行反向代理,如果資源在本地有快取,走本地快取,沒有則請求線上資源。

1.6、開屏配置

Flutter 開屏頁面(也稱為啟動頁)也是在各家平臺上自己設定的,iOS 和 Android 都不相同。

iOS:由於 iOS 必須使用 Xcode storyboard 提供應用啟動頁面,在專案根專案下執行 open ios/Runner.xcworkspace 開啟Flutter應用程式的Xcode專案,然後選擇 Runner/Assets.xcassets,將需要的啟動圖拖到 LaunchImage 影象集中即可。

Android:因 Android 啟動頁面是個 AndroidView,同時Flutter第一幀也在繪製,這時候兩者之間會有空隙。Flutter 2.5之後做了優化(將Android啟動頁保持到Flutter的第一幀渲染完成),也改變了啟動頁面的設定方式。簡單介紹下2.5以後的設定方式,在Android AndroidManifest.xml配置

<activity
android:name=".MyActivity"
// 1、配置啟動的style
android:theme="@style/LaunchTheme"
// ...
>
<meta-data
// 2、配置普通的style,系統會從啟動style直接過渡到這個style
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>

2、編譯釋出

Flutter 雖然實現多端的 UI 統一,最終的釋出還是交給了各家平臺釋出流程。

Android

通常的流程如下:全程通過Android studio完成。

Flutter clean 清理自動編譯生成的快取檔案,避免因為快取檔案導致的編譯錯誤。

Flutter clean 


=========執行輸出=========
Cleaning Xcode workspace... 4.1s
Deleting build... 1,116ms
Deleting .dart_tool... 6ms
Deleting .packages... 0ms
Deleting Generated.xcconfig... 1ms
Deleting flutter_export_environment.sh... 0ms
Deleting .flutter-plugins-dependencies... 0ms
Deleting .flutter-plugins... 0ms


Flutter pub get 拉取工程依賴庫。

Flutter pub get 


=========部分執行輸出=========
Running "flutter pub get" in studentflutter...
executing: [/Users/xx/talworkproject/xx/studentflutter/]
/Users/xx/talworkproject/xx/fluttersdk/bin/cache/dart-sdk/bin/pub --verbose get --no-precompile
FINE: Pub 2.14.4
MSG : Resolving dependencies...
SLVR: fact: app_student_flutter is 1.0.30+1
SLVR: derived: app_student_flutter
SLVR: fact: app_student_flutter depends on flutter any from sdk
SLVR: fact: app_student_flutter depends on permission_handler ^7.1.0
SLVR: fact: app_student_flutter depends on limiting_direction_csx ^0.1.0
SLVR: fact: app_student_flutter depends on url_launcher ^6.0.0-nullsafety.4
SLVR: fact: app_student_flutter depends on package_info ^2.0.0
SLVR: fact: app_student_flutter depends on shared_preferences ^2.0.5
SLVR: fact: app_student_flutter depends on connectivity_plus ^1.0.2
SLVR: fact: app_student_flutter depends on dio ^4.0.0
SLVR: fact: app_student_flutter depends on app_settings ^4.1.0
SLVR: fact: app_student_flutter depends on fluttertoast ^8.0.7

Flutter build apk 最終產生一個可以釋出的APK檔案,釋出出去。

Flutter build apk --release/--debug 

iOS

通常流程如下:需要Xcode+Android studio/VScode完成,前兩步同Android 。

Flutter clean
Flutter pub get

Flutter build ipa 生成一個構建歸檔。

Flutter build ipa --release/--debug/--profile

後面的操作和純原生的 iOS 釋出流程相同,使用 xcode 完成歸檔校驗,upload to App store等。

1、平臺編譯流程

flutter 雖遮蔽了平臺的編譯流程,但是依然脫離不了各平臺編譯流程,那它是如何做到呢?因篇幅有限以 Android Fluttter(純Flutter專案) 編譯幾個重要編譯步驟來說明:

圖:Flutter在Android上的編譯

App模組下的 build.gradle,這個是安卓的編譯配置檔案,承載編譯的重點內容。

...省略
apply plugin: FlutterPlugin
class FlutterPlugin implements Plugin<Project> {
// ......
// 重點入口
@Override
void apply(Project project) {
//......
project.extensions.create("flutter", FlutterExtension)
// 重點:新增flutter構建相關的各種task
this.addFlutterTasks(project)
//......
// flutter shell 命令
String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter"
flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile();
String flutterProguardRules = Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools",
"gradle", "flutter_proguard_rules.pro")
// 給所有的buildtypes 新增依賴
project.android.buildTypes.all this.&addFlutterDependencies
}

/**
* Adds the dependencies required by the Flutter project.
* This includes:
* 1. The embedding
* 2. libflutter.so
*/
// 重要,看上面註釋就可以看出主要是The embedding和libflutter.so
void addFlutterDependencies(buildType) {
String flutterBuildMode = buildModeFor(buildType)
//.....
platforms.each { platform ->
String arch = PLATFORM_ARCH_MAP[platform].replace("-", "_")
// Add the `libflutter.so` dependency.
addApiDependencies(project, buildType.name,
"io.flutter:${arch}_$flutterBuildMode:$engineVersion")
}
}
//......
// 重點:整個編譯過程中的重點和難點,最終是產出flutter層的產物 app.so和libs.jar,篇幅有限
private void addFlutterTasks(Project project) {
//......
String taskName = toCammelCase(["compile", FLUTTER_BUILD_PREFIX, variant.name])
FlutterTask compileTask = project.tasks.create(name: taskName, type: FlutterTask) {
//......
}
// 中間產物lib.jar
File libJar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/libs.jar")
Task packFlutterAppAotTask = project.tasks.create(name: "packLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}", type: Jar) {
destinationDir libJar.parentFile
archiveName libJar.name
dependsOn compileTask
targetPlatforms.each { targetPlatform ->
String abi = PLATFORM_ARCH_MAP[targetPlatform]
from("${compileTask.intermediateDir}/${abi}") {
include "*.so"
// Move `app.so` to `lib/<abi>/libapp.so`
rename { String filename ->
return "lib/${abi}/lib${filename}"
}
}
}
}

if (isFlutterAppProject()) {
project.android.applicationVariants.all { variant ->
// assemble task任務,最終走到 Android assemble 任務中
Task assembleTask = getAssembleTask(variant)
Task copyFlutterAssetsTask = addFlutterDeps(variant)
def variantOutput = variant.outputs.first()
def processResources = variantOutput.hasProperty("processResourcesProvider") ?
variantOutput.processResourcesProvider.get() : variantOutput.processResources
processResources.dependsOn(copyFlutterAssetsTask)
// Copy the output APKs into a known location, so `flutter run` or `flutter build apk`
// flutter build apk的歸檔
variant.outputs.all { output ->
assembleTask.doLast {
// .......
project.copy {
from new File("$outputDirectoryStr/${output.outputFileName}")
into new File("${project.buildDir}/outputs/flutter-apk");
rename {
return "${filename}.apk"
}
}
}
}
}
// ...
}
// ...
}
}


flutter shell -> flutter dart command -> flutter 中的 gradle 指令碼(最終和平臺編譯方式結合起來)-> flutter shell 將各種編譯指令碼鏈路化。

3、常見問題

  • 亂用動畫問題: 不要亂用 Animation 動畫,這個動畫佔用主執行緒資源,如果一直高頻觸發會搶佔資源影響其他程式碼邏輯的執行。

  • 互動衝突問題:在螢幕假死的現象觸發後,要檢查原生、flutter和web事件之間是否衝突,這裡面的邏輯關係需要搞清楚。

  • 聯調問題:在聯調階段,web、flutter和原生之間需要考慮經常切換環境的開發場景。

  • Flutter sdk版本問題:統一Flutter sdk 環境避免在不同開發者中產生一些因為環境配置的問題。

05

總結

技術的發展使得作為一名Web前端工程師實現Native APP需求更加容易,可以考慮使用flutter做可攻可守的混合開發,隨著業務的穩定逐漸將穩定業務原生化,藉助 web 高性價比的開發成本又不會因為開發效率措施商機。整個混合開發的全鏈路還是比較長的,從各種技術的功能開發、聯調、編譯和釋出都會積累非常多的實戰經驗,這份經歷也是非常寶貴的。

參考資料

  • Flutter官網:

    docs.flutter.dev/get-started

  • 跨端React Native 方案架構概覽:

    www.react-native.cn/docs/architecture-overview

  • Flutter效能優化-WebView使用姿勢:

    blog.csdn.net/c6E5UlI1N/article/details/109698925

  • Flutter如何和Native通訊-Android視角:

    www.jianshu.com/p/d9eeb15b3fa0

  • Flutter中呼叫原生-platform channels(平臺通道):

    www.jianshu.com/p/b8f1fccaabd3

  • Web前端學習之2021年最受歡迎的JavaScript移動框架:

    blog.csdn.net/qf2019/article/details/120883825

  • LofterFrontendTeam:LOFTER App離線包方案及相關效能分析:

    zhuanlan.zhihu.com/p/389883836

掃描下方二維碼新增 「好未來技術」 微信官方賬號

進入好未來技術官方交流群與作者實時互動~

(若掃碼無效,可通過微訊號 TAL-111111 直接新增)

- 也許你還想看 -

Web 基礎系列之 ES6

從輸入網址到內容返回解析|前端工程師需要掌握這些知識

提高服務穩定性之熔斷怎麼做|文末新春福利

我知道你“在看”喲~