首頁>技術>

一、為何 Flutter

跨端技術眾多,為何選擇 Flutter?它能帶來哪些優勢,有哪些缺點?

1.1 Flutter VS 原生

無論如何,原生的執行效率毋庸置疑是最高的,但是從工程工作量的角度來對比的話,特別是快速試錯和業務擴充套件階段,Flutter 是目前為止比較推薦的利器。

1.2 Flutter VS Web

任何跨端的技術都是基於一碼多端的思維,解決工程效率的問題,之前很多的跨端技術,例如 React Native 等都是基於web的跨端性解決方案,但是大家都知道,web 在移動端上的執行效率和 PC 上有巨大差距的,這就導致 RN 不能很有效地在移動端完成各種複雜的互動式運算(例如複雜的動畫運算,互動的執行效能等),即便是引入了 Airbnb 的 Lottie 引擎依然會在低端的手機上面顯得很卡頓(當然也可以使用一些自研的引擎技術來針對各端來解決,不過這樣就失去了跨端的意義)。

1.3 Flutter 效能

Flutter 的編譯方式和產物是決定其高效執行效率的前提,不同於 web 的跨端編譯一樣(web 的跨端編譯大多是選擇了使用 "橋" 的概念來呼叫編譯產物,通常是使用了原生端的入口 + web 端的橋來實現),Flutter 幾乎是把 dart 的原始碼透過不同平臺的編譯原理生成各平臺的產物,這種"去橋"的產物正是我們所希望得到的、貼近原生執行效能的編譯產物(當然,在 dart 最初設計的時候,是參考了很多前端的結構來完成的,特別從語法上面能夠很明顯地感受到前端的痕跡,而且最初的 dart2js 的原理也是同樣"橋"的概念)。

例如 9月23號 google 釋出的新 Flutter 版本中,在支援的 Windows 編譯產物上,就是透過類似 Visual Studio 的編譯工具(如果要將你的 Flutter 工程編譯成 Windows 產物,需要提前安裝一些 VS 相關的編譯外掛),生成了 Windows 下的工程解決方案 .sln,最終生成 dll 的呼叫方式,執行起來很流暢,可以下載附件中的 Release.zip 來嘗試執行。(Release.zip 下載)

(PS:這裡所有編譯工程都是透過同一套程式碼完成,包括上文中的 web 地址、移動端案例還有這裡的 Windows 案例)

1.4 與 RN 的效能對比

以上是同樣功能模組下,Flutter 和 RN 的一些資料上的對比,是從眾多的資料中抽取出來比較有代表性的一組。

1.5 跨端平臺的多樣性1.6 引擎

Flare-Flutter 是一款十分優秀的 Flutter 動畫引擎,編譯出的動畫已經在 Windows、移動端、web 上親測驗證過。

1.7 語法糖

A?.B如果 A 等於 null,那麼 A?.B 為 null如果 A 不等於 null,那麼 A?.B 等價於 A.BAnimal animal = new Animal('cat');Animal empty = null;//animal 非空,返回 animal.name 的值 catprint(animal?.name);//empty 為空,返回 nullprint(empty?.name);A??B如果 A 等於 null,那麼 A??B 為 B如果 A 不等於 null,那麼 A??B 為 A

1.8 綜合測評1.9 互動應用

Flutter 生成的互動可以嵌入到任何端中使用精簡的指令集進行互動,為互動場景(教學場景等帶來巨大的希望),以下是直播同步互動的 demo 場景。

二、Flutter 業務架構

Flutter 中目前是沒有現成的 mvvm 框架的,但是我們可以利用 Element 樹特性來實現 mvvm。

2.1 ViewModel
abstract class BaseViewModel {  bool _isFirst = true;  BuildContext context;  bool get isFirst => _isFirst;  @mustCallSuper  void init(BuildContext context) {    this.context = context;    if (_isFirst) {      _isFirst = false;      doInit(context);    }  }  // the default load data method  @protected  Future refreshData(BuildContext context);  @protected  void doInit(BuildContext context);  void dispose();  class ViewModelProvider<T extends BaseViewModel> extends StatefulWidget {  final T viewModel;  final Widget child;  ViewModelProvider({    @required this.viewModel,    @required this.child,  });  static T of<T extends BaseViewModel>(BuildContext context) {    final type = _typeOf<_ViewModelProviderInherited<T>>();    _ViewModelProviderInherited<T> provider =        // 查詢Element樹中快取的InheritedElement        context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;    return provider?.viewModel;  }  static Type _typeOf<T>() => T;  @override  _ViewModelProviderState<T> createState() => _ViewModelProviderState<T>();}class _ViewModelProviderState<T extends BaseViewModel>    extends State<ViewModelProvider<T>> {  @override  Widget build(BuildContext context) {    return _ViewModelProviderInherited<T>(      child: widget.child,      viewModel: widget.viewModel,    );  }  @override  void dispose() {    widget.viewModel.dispose();    super.dispose();  }}// InheritedWidget可以被Element樹快取class _ViewModelProviderInherited<T extends BaseViewModel>    extends InheritedWidget {  final T viewModel;  _ViewModelProviderInherited({    Key key,    @required this.viewModel,    @required Widget child,  }) : super(key: key, child: child);  @override  bool updateShouldNotify(InheritedWidget oldWidget) => false;
2.2 DataModel
import 'dart:convert';import 'package:pupilmath/datamodel/base_network_response.dart';import 'package:pupilmath/datamodel/challenge/challenge_ranking_list_item_data.dart';import 'package:pupilmath/utils/text_utils.dart';///歷史榜單class ChallengeHistoryRankingListResponse    extends BaseNetworkResponse<ChallengeHistoryRankingData> {  ChallengeHistoryRankingListResponse.fromJson(Map<String, dynamic> json)      : super.fromJson(json);  @override  ChallengeHistoryRankingData decodeData(jsonData) {    if (jsonData is Map) {      return ChallengeHistoryRankingData.fromJson(jsonData);    }    return null;  }}class ChallengeHistoryRankingData {  String props;  int bestRank; //最佳排名  int onlistTimes; //上榜次數  int total; //總共挑戰數  List<ChallengeHistoryRankingItemData> ranks; //先給10天  //二維碼  String get qrcode =>      TextUtils.isEmpty(props) ? '' : json.decode(props)['qrcode'] ?? '';  ChallengeHistoryRankingData.fromJson(Map<String, dynamic> json) {    props = json['props'];    bestRank = json['bestRank'];    onlistTimes = json['onlistTimes'];    total = json['total'];    if (json['ranks'] is List) {      ranks = [];      (json['ranks'] as List).forEach(          (v) => ranks.add(ChallengeHistoryRankingItemData.fromJson(v)));    }  }}///歷史戰績的itemclass ChallengeHistoryRankingItemData {  ChallengeRankingListItemData champion; //當天最好成績  ChallengeRankingListItemData user;  ChallengeHistoryRankingItemData.fromJson(Map<String, dynamic> json) {    if (json['champion'] is Map)      champion = ChallengeRankingListItemData.fromJson(json['champion']);    if (json['user'] is Map)      user = ChallengeRankingListItemData.fromJson(json['user']);  }
2.3 View
import 'dart:convert';import 'package:dio/dio.dart';import 'package:flutter/material.dart';import 'package:pupilmath/datamodel/challenge/challenge_history_ranking_list_data.dart';import 'package:pupilmath/entity_factory.dart';import 'package:pupilmath/network/constant.dart';import 'package:pupilmath/network/network.dart';import 'package:pupilmath/utils/print_helper.dart';import 'package:pupilmath/viewmodel/base/abstract_base_viewmodel.dart';import 'package:rxdart/rxdart.dart';//每日挑戰歷史戰績class ChallengeHistoryListViewModel extends BaseViewModel {  BehaviorSubject<ChallengeHistoryRankingData> _challengeObservable =      BehaviorSubject();  Stream<ChallengeHistoryRankingData> get challengeRankingListStream =>      _challengeObservable.stream;  @override  void dispose() {    _challengeObservable.close();  }  @override  void doInit(BuildContext context) {    refreshData(context);  }  @override  Future refreshData(BuildContext context) {    return _loadHistoryListData();  }  _loadHistoryListData() async {    Map<String, dynamic> parametersMap = {};    parametersMap["pageNum"] = 1;    parametersMap["pageSize"] = 10; //拿10天資料    handleDioRequest(      () => NetWorkHelper.instance          .getDio()          .get(challengeHistoryListUrl, queryParameters: parametersMap),      onResponse: (Response response) {        ChallengeHistoryRankingListResponse rankingListResponse =            EntityFactory.generateOBJ(json.decode(response.toString()));        if (rankingListResponse.isSuccessful) {          _challengeObservable.add(rankingListResponse.data);        } else {          _challengeObservable.addError(null);        }      },      onError: (error) => _challengeObservable.addError(error),    );  }  Future<ChallengeHistoryRankingData> syncLoadHistoryListData(    int pageNum,    int pageSize,  ) async {    Map<String, dynamic> parametersMap = {};    parametersMap["pageNum"] = pageNum;    parametersMap["pageSize"] = pageSize;    try {      Response response = await NetWorkHelper.instance          .getDio()          .get(challengeHistoryListUrl, queryParameters: parametersMap);      ChallengeHistoryRankingListResponse rankingListResponse =          EntityFactory.generateOBJ(json.decode(response.toString()));      if (rankingListResponse.isSuccessful) {        return rankingListResponse.data;      } else {        return null;      }    } catch (e) {      printHelper(e);    }    return null;  }
2.4 一些基礎架構2.5 View 和 ViewModel 如何實現初始化和相互作用2.6 Flutter 業務架構抽離

如果是統一系列的產品業務形態,還可以抽離出一套核心的架構,複用在同樣的生產產品線上,例如當前產品線以教育為主,利用 Flutter 的一碼多端性質,則可以把題版生產工廠、渲染題版引擎、 適配框架、 以及跨端介面的框架都抽離出來,迅速地形成可以推廣複用的模板,可以事半功倍地解決掉業務上的試錯成本問題,當然,其他產品性質的業務線均可如此。

三、Flutter 適配

任何框架中的 UI 適配都是特別繁重的工作,跨端上的適配更是如此,因此在同一套佈局裡面,各個平臺的換算過程顯得尤為重要,起初的時候,Flutter 中並沒有提供某種諸如 dp 或者 sp 的適配方式,而且考慮到直接更改底層 Matrix 換算比例的話可能會讓原本高畫質解析度的手機顯示不是那麼清楚,而 Flutter 的寬高單位都是 num,最後編譯的時候才會去對應到各個平臺的單位尺寸。

為了減輕設計師的設計負擔,這裡通常使用一套 iOS 的設計稿即可,以375 x 667的通用設計稿為例,轉換過來到android上是360 x 640 (對應1080 x 1920),這裡flutter的單位也是和對應手機的畫素密度有關的。

3.1 構造一個轉換工具類:
//目前適配iPhone和iPad機型尺寸import 'dart:io';import 'dart:ui';import 'dart:math';import 'package:pupilmath/utils/print_helper.dart';bool initScale = false;//針對iOS平臺的scale係數double iosScaleRatio = 0;//針對android平臺的scale係數// (因為所有設計稿均使用iOS的設計稿進行,所以需要轉換為android設計稿上的尺寸,// 否則無法進行小螢幕上的適配)double androidScaleRatio = 0;//文字縮放比double textScaleRatio = 0;const double baseIosWidth = 375;const double baseIosHeight = 667;const double baseIosHeightX = 812;const double baseAndroidWidth = 360;const double baseAndroidHeight = 640;void _calResizeRatio() {  if (Platform.isIOS) {    final width = window.physicalSize.width;    final height = window.physicalSize.height;    final ratio = window.devicePixelRatio;    final widthScale = (width / ratio) / baseIosWidth;    final heightScale = (height / ratio) / baseIosHeight;    iosScaleRatio = min(widthScale, heightScale);  } else if (Platform.isAndroid) {    double widthScale = (baseAndroidWidth / baseIosWidth);    double heightScale = (baseAndroidHeight / baseIosHeight);    double scaleRatio = min(widthScale, heightScale);    //取兩位小數    androidScaleRatio = double.parse(scaleRatio.toString().substring(0, 4));  }}bool isFullScreen() {  return false;}//縮放double resizeUtil(double value) {  if (!initScale) {    _calResizeRatio();    initScale = true;  }  if (Platform.isIOS) {    return value * iosScaleRatio;  } else if (Platform.isAndroid) {    return value * androidScaleRatio;  } else {    return value;  }}//縮放還原//每個螢幕的縮放比不一樣,如果在iOS裝置上出題,則題目座標值需要換算成原始座標,載入的時候再透過不同平臺換算回來double unResizeUtil(double value) {  if (iosScaleRatio == 0) {    _calResizeRatio();  }  if (Platform.isIOS) {    return value / iosScaleRatio;  } else {    return value / androidScaleRatio;  }}//文字縮放大小_calResizeTextRatio() {  final width = window.physicalSize.width;  final height = window.physicalSize.height;  final ratio = window.devicePixelRatio;  double heightRatio = (height / ratio) / baseIosHeight / window.textScaleFactor;  double widthRatio = (width / ratio) / baseIosWidth / window.textScaleFactor;  textScaleRatio = min(heightRatio, widthRatio);}double resizeTextSize(double value) {  if (textScaleRatio == 0) {    _calResizeTextRatio();  }  return value * textScaleRatio;}double resizePadTextSize(double value) {  if (Platform.isIOS) {    final width = window.physicalSize.width;    final ratio = window.devicePixelRatio;    final realWidth = width / ratio;    if (realWidth > 450) {      return value * 1.5;    } else {      return value;    }  } else {    return value;  }}double autoSize(double percent, bool isHeight) {  final width = window.physicalSize.width;  final height = window.physicalSize.height;  final ratio = window.devicePixelRatio;  if (isHeight) {    return height / ratio * percent;  } else {    return width / ratio * percent;  }
3.2 具體使用:

這樣每次如果有解析度變動或者適配方案變動的時候,直接修改 resizeUtil 即可,但是這樣帶來的問題就是,在編寫過程中單位變得很冗長,而且不熟悉團隊工程的人會容易忘寫,導致查錯時間變長,程式碼侵入性較高,於是利用 dart 語言的擴充套件函式特性,為 resizeUtil 做一些改進。

3.3 低侵入式的 resizeUtil

透過擴充套件 dart 的 num 來構造想要的單位,這裡用 dp 和 sp 來舉例,在 resizeUtil 中加入擴充套件:

extension dimensionsNum on num {  ///轉為dp  double get dp => resizeUtil(this.toDouble());  ///轉為文字大小sp  double get sp => resizeTextSize(this.toDouble());  ///轉為pad文字適配  double get padSp => resizePadTextSize(this.toDouble());

然後在佈局中直接書寫單位即可:

四、Flutter 中的一些坑4.1 泛型上的坑

剛開始在移動端上使用泛型來做資料的自動解析時,使用了 T.toString 來判斷型別,但是當編譯成 web 的 release 版本時,在移動端正常執行的程式在web上無法正常工作:

剛開始的時候把目標一直定位在編譯的方式上,因為存在 dev profile release 三種編譯模式,只有在 release 上無法執行,誤以為是 release 下編譯有 bug,隨著和 Flutter 團隊的深入討論後,發現其實是泛型在 release 模式下的坑,即在 web 版本的 release 模式下,一切都會進行壓縮(包含型別的定義),所以在 release 下,T.toString() 返回的是 null,因此無法識別出泛型特徵,具體的討論連結:

Flutter application which use canvas to build self-CustomPainter cannot work on browser if i used the release mode by command "flutter run -d chrome --release" or "flutter build web". · Issue #47967 · flutter/fluttergithub.com

In release mode everything is minified, the (T.toString() == "Construction2DEntity") comparison fails and you get entity null returned.If you change the code to (T ==Construction2DEntity) it will fix your app.

最後建議,無論在何種模式下,都直接寫成T==的形式最為安全。

class EntityFactory {  static T generateOBJ<T>(json) {    if (1 == 0) {      return null;    } else if (T == "ChallengeRankingListDataEntity") {      /// 每日挑戰排行榜      return ChallengeHomeRankingListResponse.fromJson(json) as T;    } else if (T == "KnowledgeEntity") {      return KnowledgeEntity.fromJson(json) as T;    }  }
4.2 在編譯成 web 產物後如何使用 iframe 來載入其他網頁

對於移動端來說,webview_flutter 可以解決掉載入 web 的問題,不過編譯成 web 產物後,已經無法直接使用 WebView 外掛來進行載入,此時需要用到 dart 最初設計來編寫網頁的一些方式,即 HtmlElmentView:

import 'package:flutter/material.dart';import 'dart:ui' as ui;import 'dart:html' as html;void main() {  runApp(MyApp());}class MyApp extends StatelessWidget {  @override  Widget build(BuildContext context) {    return MaterialApp(      title: 'Flutter Demo',      home: MyHomePage(),    );  }}class MyHomePage extends StatelessWidget {  @override  Widget build(BuildContext context) {    return Scaffold(      body: Center(           child: Iframe()      ),      floatingActionButton: FloatingActionButton(        onPressed: (){},        tooltip: 'Increment',        child: Icon(Icons.add),      ),     );  }}class Iframe extends StatelessWidget {  Iframe(){    ui.platformViewRegistry.registerViewFactory('iframe', (int viewId) {      var iframe = html.IFrameElement();      iframe.src='https://flutter.dev';      return iframe;  });  }  @override  Widget build(BuildContext context) {    return Container(      width:400,      height:300,      child:HtmlElementView(viewType: 'iframe')    );  }

不過這種方式會帶來新的底層重新整理渲染問題(當滑鼠移動到某個元素時,會不停地閃動重新整理),目前在新的版本上已修復,有興趣的同學可以看看:

https://github.com/flutter/fl...

4.3 Flutter 如何載入本地的 html 並且進行通訊

內建 html 是很多工程的需求,很多網上的資料都是透過把本地的 html 做成資料流的方式然後載入進來,這種做法的相容性很不好,而且編寫過程中容易出現很多檔案流過大無法讀取的問題,其實這些做法都不是很舒適,我們應該透過 IFrameElement 來進行載入並通訊,做法和前端很類似:

4.4 在 iOS 13.4 上 WebView 的手勢無法正常使用

官方的 webview_flutter 在上一個版本當 iOS 升級到13.4之後會出現手勢被攔截且無法正常使用的情況,換成flutter_webview_plugin後暫時解決掉該問題(目前 WebView 已經做了針對性的修復,但是還未驗證),但是 flutter_webview_plugin 在 iOS 上又無法寫入 user-agent,目前可以透過修改本地的外掛程式碼進行解決:

檔案位置為:

flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_webview_plugin-0.3.11/ios/Classes/FlutterWebviewPlugin.m修改內容為在146行(initWebview方法中初始化WKWebViewConfiguration後)新增如下程式碼if (@available(iOS 9.0, *)) {if (userAgent != (id)[NSNull null]) {self.webview.customUserAgent = userAgent;}}

https://github.com/flutter/fl...

五、關於佈局和運算5.1 容器 Widget 和渲染 Widget5.2 GlobalKey

透過 GlobalKey 獲取 RenderBox 來獲取渲染出的控制元件的 size 和 position 等引數:

5.3 浮點運算

在 dart 的浮點運算中,由於都是高精度的 double 運算,當運算長度過長的時候,dart 會自動隨機最後的一位小數,這樣會導致每一次有些浮點運算每一次都是不確定的,這時需要手動進行精度轉換,例如在計算兩條線段是否共線時:

5.4 Matrix 的平移和旋轉

在矩陣的換算過程中,如果使用普通的matrix.translate,會導致 rotate 之後,再進行 translate 會在旋轉的基數上面做係數疊加平移運算,這樣計算後得到的不是自己想要的結果,因此如果運算當中有 rotate 操作時,應當使用 leftTranslate 來保證每次運算的獨立性:

六、專案最佳化6.1 避免 build() 方法耗時:6.2 重繪區域最佳化:6.3 儘量避免使用 Opacity6.4 Flutter的單執行緒模型

優先全部執行完 Microtask Queue 中的 Event,直到 Microtask Queue 為空,才會執行 Event Queue 中的 Event。

6.5 耗時方法放在 Isolate

Isolate 是 Dart 裡的執行緒,每個 Isolate 之間不共享記憶體,透過訊息通訊。

Dart 的程式碼執行在 Isolate 中,處於同一個 Isolate 的程式碼才能相互訪問。

七、雜談總結

經歷了對 Flutter 長期的探索和專案驗證,目前對 Flutter 有自己的一些雜談總結:

7.1

Flutter 在移動端的表現還是很不錯的,在執行流暢度方面也是非常棒,經過最佳化過後的帶大量影象運算的 app 執行在2013年的舊 Android 手機上面依然十分流暢,iOS 的流暢程度也堪比原生。

7.2

對於 web 的應用來說,Flutter 還在不斷地改進,其中還有很多的坑沒有解決,這裡包括了移動端的 WebView 以及程式設計成的 web 應用,還不適合大面積地投入到 web 的生產環境中。

7.3

關於和 Native 的混編,為了避免產生混合棧應用中的記憶體問題和渲染問題等,建議儘量將嵌入原生的 Flutter 節點設計在葉子節點上,即業務棧跳轉到 Flutter 後儘量完成結束後再回到Native棧中。

7.4

基於“去橋”的原生編譯方式,Flutter 在未來各個平臺上的執行應該會充滿期待,目前驗證的移動端應用打包成 Windows 應用後,執行表現還是很不錯的,當然一些更大型的應用需要時間去摸索和完善。

7.5

語法方面,Flutter 中的 dart 正在變得越來越簡單,也在借鑑一些優秀的前端框架上的語法,例如 react 等,kotlin 中也有很多相似的地方,感覺 Flutter 團隊正在努力地促進大前端時代的發展。

總之,Flutter 確實帶來了很多以前的跨端方案沒法滿足的驚喜的地方,相信不久的將來一碼多端會變得越來越重要,特別是在新業務的探索成本上表現得十分搶眼。

29
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • php swoole框架之一hyperf