Flutter技術雜談

在這裏插入圖片描述

張龑(網易有道技術團隊)html

Flutter的性能分析、工程架構、以及一些細節處理前端

1.爲什麼Flutter

跨端技術衆多,爲什麼選擇(Flutter),它能帶來哪些優點,有哪些缺點。react

先看看具體的工程效果

web端的連接android

flutter工程效果ios

Flutter VS 原生

不管如何,原生的運行效率毋庸置疑是最高的,可是從工程工做量的角度來對比的話,特別是快速試錯和業務擴展階段,flutter是目前爲止比較推薦的利器。git

在這裏插入圖片描述

Flutter VS Web

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

Flutter性能

lutter的編譯方式和產物是決定其高效運行效率的前提,不一樣於web的跨端編譯同樣(web的跨端編譯大可能是選擇了使用 "橋" 的概念來調用編譯產物,一般是使用了原生端的入口 + web端的橋來實現),Flutter幾乎是把dart的源碼經過不一樣平臺的編譯原理生成各平臺的產物,這種「去橋」的產物正式咱們所但願獲得的、貼近原生運行性能的編譯產物(固然,在dart最初設計的時候,是參考了不少前端的結構來完成的,特別從語法上面可以很明顯地感覺到前端的痕跡,並且最初的dart2js的原理也是一樣「」的概念)。web

例如 9月23號 google發佈的新flutter版本中,在支持的windows編譯產物上,就是經過相似visual studio的編譯工具(若是要將你的flutter工程編譯成windows產物,須要提早安裝一些VS相關的編譯插件),生成了windows下的工程解決方案.sln,最終生成dll的調用方式,運行起來很流暢,能夠下載附件中的Release.zip來嘗試運行:編程

在這裏插入圖片描述

在這裏插入圖片描述

(PS:這裏全部編譯工程都是經過同一套代碼完成,包括上文中的web地址、移動端案例還有這裏的windows案例)json

與RN的性能對比:

在這裏插入圖片描述

以上是一樣功能模塊下,Flutter和RN的一些數據上的對比,是從衆多的數據中抽取出來比較有表明性的一組

跨端平臺的多樣性

![]](img-blog.csdnimg.cn/20201022165…)

引擎

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

語法糖

綜合測評

互動應用

flutter生成的互動能夠嵌入到任何端中使用精簡的指令集進行互動,爲互動場景(教學場景等帶來巨大的但願),如下是直播同步互動的demo場景

2.Flutter業務架構

flutter中目前是沒有現成的mvvm框架的,可是咱們能夠利用Element樹特性來實現mvvm

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;
}
複製代碼

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)));
    }
  }
}

///歷史戰績的item
class 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']);
  }
}
複製代碼

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;
  }
}
複製代碼

一些基礎架構

view和viewmodel如何實現初始化和相互做用:

Flutter業務架構抽離

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

3.Flutter適配

任何框架中的UI適配都是特別繁重的工做,跨端上的適配更是如此,所以在同一套佈局裏面,各個平臺的換算過程顯得尤其重要,起初的時候,flutter中並無提供某種諸如 dp 或者 sp 的適配方式,並且考慮到直接更改底層matrix換算比例的話可能會讓本來高清分辨率的手機顯示不是那麼清楚,而flutter的寬高單位都是num,最後編譯的時候纔會去對應到各個平臺的單位尺寸。爲了減輕設計師的設計負擔,這裏一般使用一套ios的設計稿便可,以375 x 667的通用設計稿爲例,轉換過來到android上是360 x 640 (對應1080 x 1920),這裏flutter的單位也是和對應手機的像素密度有關的。

構造一個轉換工具類:

//目前適配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;
  }
}
複製代碼

具體使用:

這樣每次若是有分辨率變更或者適配方案變更的時候,直接修改resizeUtil便可,可是這樣帶來的問題就是,在編寫過程當中單位變得很冗長,並且不熟悉團隊工程的人會容易忘寫,致使查錯時間變長,代碼侵入性較高,因而利用dart語言的擴展函數特性,爲resizeUtil作一些改進。

低侵入式的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());
}
複製代碼

而後在佈局中直接書寫單位便可:

4.Flutter中的一些坑

泛型上的坑

剛開始在移動端上使用泛型來作數據的自動解析時,使用了T.toString來判斷類型,可是當編譯成web的release版本時,在移動端正常運行的程序在web上沒法正常工做:

剛開始的時候把目標一直定位在編譯的方式上,由於存在dev profile release三種編譯模式,只有在release上沒法運行,誤覺得是release下編譯有bug,隨着和flutter團隊的深刻討論後,發現實際上是泛型在release模式下的坑,即在web版本的release模式下,一切都會進行壓縮(包含類型的定義),因此在release下,T.toString()返回的是null,所以沒法識別出泛型特徵,具體的討論連接:github.com/flutter/flu…

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;
    }
  }
}
複製代碼

在編譯成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')
    );
  }
}
複製代碼

不過這種方式會帶來新的底層刷新渲染問題(當鼠標移動到某個元素時,會不停地閃動刷新),目前在新的版本上已修復,有興趣的同窗能夠看看:github.com/flutter/flu…

Flutter如何加載本地的html而且進行通訊

內置html是不少工程的需求,不少網上的資料都是經過把本地的html作成數據流的方式而後加載進來,這種作法的兼容性很很差,並且編寫過程當中容易出現不少文件流過大沒法讀取的問題,其實這些作法都不是很溫馨,咱們應該經過IFrameElement來進行加載並通訊,作法和前端很相似:

在ios13.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; } }

關於webview_flutter的手勢問題還在不斷的討論中:github.com/flutter/flu…

5.關於佈局和運算

容器widget和渲染widget

GlobalKey

經過GlobalKey獲取RenderBox來獲取渲染出的控件的size和position等參數:

浮點運算

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

Matrix的平移和旋轉

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

6.項目優化

避免build() 方法耗時:

重繪區域優化:

儘可能避免使用Opacity

Flutter的單線程模型

優先所有執行完Microtask Queue中的Event,直到Microtask Queue爲空,纔會執行Event Queue中的Event

耗時方法放在isolate

7.雜談總結

經歷了對flutter長期的探索和項目驗證,目前對flutter有本身的一些雜談總結:

(1).flutter在移動端的表現仍是很不錯的,在運行流暢度方面也是很是棒,通過優化事後的帶大量圖像運算的App運行在2013年的舊android手機上面依然十分流暢,ios的流暢程度也堪比原生;

(2).對於web的應用來講,flutter還在不斷地改進,其中還有不少的坑沒有解決,這裏包括了移動端的webview以及編程成的web應用,還不適合大面積的投入到web的生產環境中;

(3).關於和Native的混編,爲了不產生混合棧應用中的內存問題和渲染問題等,建議儘可能將嵌入原生的flutter節點設計在葉子節點上,即業務棧跳轉到flutter後儘可能完成結束後再回到Native棧中;

(4).基於「去橋」的原生編譯方式,flutter在將來各個平臺上的運行應該會充滿期待,目前驗證的移動端應用打包成windows應用後,運行表現仍是很不錯的,固然一些更大型的應用須要時間去摸索和完善;

(5).語法方面,flutter中的dart正在變得愈來愈簡單,也在借鑑一些優秀的前端框架上的語法,例如react等,kotlin中也有不少類似的地方,感受flutter團隊正在努力地促進大前端時代的發展。

總之,flutter確實帶來了不少之前的跨端方案無法知足的驚喜的地方,相信不久的未來一碼多端會變得愈來愈重要,特別是在新業務的探索成本上表現得十分搶眼。

以上是一些對flutter的一些粗淺的總結,歡迎有興趣的小夥伴一塊兒探討。

網易技術熱愛者隊伍持續招募隊友中!網易有道,與你同道,由於熱愛因此選擇, 期待志同道合的你加入咱們,簡歷可發送至郵箱:bjfanyudan@corp.netease.com

附件: 連接:pan.baidu.com/s/1_JjnD1q5… 提取碼:7r4i

相關文章
相關標籤/搜索