Flutter初見

2021-02-14 58無線技術
Flutter初見

Flutter is a mobile app SDK for building high-performance, high-fidelity, apps for iOS and Android, from a single codebase. 

Flutter 是一款行動應用程式 SDK,致力於使用一套代碼來構建高性能、高保真的 iOS 和 Android 應用程式。

Flutter的優勢: 

開發效率高:

1. 一套代碼開發 iOS 和 Android

2. 熱加載(hot reload)

2. 創建美觀,高度定製的用戶體驗

    1. Material Design 和 Cupertino (iOS 風格)Widget 

    2. 實現定製,美觀,品牌驅動的設計,而不受 OEM Widget 集的限制

框架結構(Architecture) 

 Skia:開源的2d圖形庫。其已作為Chrome, Chrome OS, Android, Firefox, Firefox OS等其他眾多產品的圖形引擎,支持平臺還包括Windows,macOS,iOS8+,Ubuntu14.04+等。

Dart: 

1. debug:JIT(Just In Time)編譯,運行時分析、編譯,運行較慢。Hot Reload基於JIT運行

2. release:AOT(Ahead Of Time)編譯,生成了原生的arm代碼,開發期間較慢,但運行期間快

3. 一切都是對象,甚至數字,函數,和null都是對象

4. 默認publick,通過加(_)標記為私有 5. 單線程,沒有鎖的概念 

 Text:文本渲染

Dart的線程模型

Flutter與Android一樣,通過Main線程和消息循環實現UI繪製操作與UI事件,如下所示:

 

Dart的不同點: 

Dart是單線程執行,通過Future來實現異步編程,只是把任務暫時放在消息隊列裡,本質還是單線程執行,與javascript類型 

兩個消息隊列:event隊列和microtask隊列

單線程帶來的問題:單某個任務執行時間過長,超過16ms時,會導致丟幀,給用戶的感覺就是卡頓,React藉助requestIdleCallback Api實現了卡頓優化,詳情請參考

Dart解決這個問題的方案:通過Isolate真正意義上創建線程,但此線程與java裡的線程不一樣:isolates之間不會共享內存,更像進程,通過傳遞message來進行交流,Demo

一切都是Widget

Widgets是Flutter應用程式用戶界面的基礎構建模塊,Widgets包含了views,view controllers,layouts等等能力。

Flutter提供了很多基組Widgets,但這些Widgets有一個與Android最大的不同點:每個Widget的能力很單一,如Text Widget,沒有width, height, padding,color等等屬性,需要藉助其他Widget。

更多細節請查看:Flutter快速上車之Widget

StatelessWidget & StatefulWidget

Flutter的widget分為無狀態和有狀態,如下所示: 



如何選擇?下面是我的一些經驗: 1. 包含TextField的widget --- StatefulWidget 2. 用戶交互時,產出的數據,如點擊計數 1. 局部數據 --- StatefulWidget 2. 全局數據(store存儲) --- StatelessWidget 3. 默認為StatelessWidget

Widget,Element,RenderObject

Flutter裡的Widgets,Elements, RenderObject三要素與React中的Element,Instance/Fiber, Dom有點類似 

Widgets:widget tree,只是屬性集合,需要被繪製的屬性集合,每次build,都是新對象,所以屬性都要用final修飾 Flutter的性能

Elements:element tree,concrete widget tree,diff操作,每次build,不會重新構建,進行diff和update

RenderObject:真正負責layout, rendering等等操作,一般是由element創建

Flutter性能要高的原因: 

debug為字節碼,release為機器碼 

 不依賴OEM widgets

沒有bridge

Native View: 

Hybrid: 

ReactNative: 

Flutter 

注意:以上只是從實現角度分析,在機器性能好的情況下,實際差距不大

Git權限分配工具簡介

為不同類型的角色批量分配Git權限的工具,整體效果如下:

源碼下載地址:https://github.com/handsomeliuyang/flutter-igit

框架結構

pubspec.yaml:與package.json/build.grale類似,用於配置程序的信息,如下所示: 

assets:用於存放內置圖片與資源,自建目錄可修改

lib:src目錄,按功能模塊分為:

main.dart/maindev.dart:程序的入口文件,與c語言類似,dart程序的入口為main()函數,maindev.dart的區別是使用DevToolsStore,用於查看store與action

App.dart:最外層的配置,如下所示:

@overrideWidget build(BuildContext context) {    return StoreProvider( // 使用Redux的要求        store: widget.store,        child: new MaterialApp( // 使用Material要求            title: 'Flutter igit',            theme: new ThemeData( // 全局樣式                primaryColor: const Color(0xFF1C306D),                accentColor: const Color(0xFFFFAD32),            ),            home: MainPage(                devDrawerBuilder: widget.devDrawerBuilder            ),        ),    );}

按功能劃分目錄:models, networking, redux, ui, utils

redux

redux的結構非常簡單,如下所示: 

由於Flutter是一個類似MVVM框架,所以通過StoreConnector實現數據監聽,如下所示:

@overrideWidget build(BuildContext context) {    return StoreConnector<AppState, DrawListViewModel> (        distinct: true,        converter: (store) => DrawListViewModel.fromStore(store),        builder: (context, viewModel){            return DrawListContent(                header: this.header,                viewModel: viewModel,            );        },    );}

在Flutter裡,應用了Redux後的實現結構為: 

Redux是全局單例,應用的功能模塊很多,所以redux的目錄與state按功能模塊的劃分更加合適,如下所示:

class AppState {    final TradelineState tradelineState;    final PermissionState permissionState;    final ProjectState projectState;    AppState({        @required this.tradelineState,        @required this.permissionState,        @required this.projectState    });    static initial() {        return AppState(            tradelineState: TradelineState.initial(),            permissionState: PermissionState.initial(),            projectState: ProjectState.initial()        );    }        ...}

network

flutter的http請求很簡單,主要是使用兩個Api:http,Uri,如下所示:

Future<List<GitProject>> getGroups(int page, String search) async {    Uri uri = Uri.http(        AUTHORITY,        '${FIXED_PATH}/groups',        <String, String>{            'private_token': Config.LIUYANG_TOKEN,            'per_page': PER_PAGE.toString(),            'all_available': 'true',            'page':'${page}',            'search':'com.wuba'        });    final response = await http.get(uri.toString());    final jsonResponse = json.decode(response.body);    debugPrint('liuyang ${jsonResponse}');    if(response.statusCode == 200){        List<GitProject> groups = List<GitProject>();        for(int i=0; i<jsonResponse.length; i++){            groups.add(GitProject.fromJson(jsonResponse[i], ProjectType.group));        }        return groups;    } else {        throw Exception('Failed ${response.statusCode} ${response.body}');    }}

注意: 1. 上面是通過async,Future實現異步操作,但此異步並不是真正的開異步線程,只是把任務放在隊列裡,延遲執行而已,應該使用isolate實現真正的異步執行 2. 面向對象編程,每個Model裡,都有兩個Api:fromJson(),toJson()

MainPage

整體效果: 

關鍵點:

此框架頁包含:AppBar,Drawer,DevDrawer,PermissionPage

此框架默認應該是StatelessWidget,但由於AppBar的title需要動態拼接,導致只能改為StatefulWidget,如下:

   class _MyPageState extends State<MainPage> {        Widget _buildTitle(BuildContext context) {            return StoreConnector<AppState, Tradeline>(                distinct: true,                converter: (store) => store.state.tradelineState.current,                builder: (BuildContext context, Tradeline currentTradeline) {                    return Text(                        '分配 ${currentTradeline?.name ?? ''} 的igit權限'                    );                },            );        }        @override        Widget build(BuildContext context) {            return new Scaffold(                appBar: new AppBar(title: _buildTitle(context)),                drawer: Drawer(                    child: DrawList(                        header: DrawListHeader()                    ),                ),                endDrawer: widget.devDrawerBuilder != null ? widget.devDrawerBuilder(context) : null,                body: PermissionPage(),            );        }    }

dart裡創建對象時,new關鍵字不是必需的,如下:

   class Shape {    }        Shape shape = new Shape();    Shape shape1 = Shape();

在build時,個人感覺省略掉new關鍵字,可讀性更強

Drawer

效果如下: 

功能比較簡單,思路如下:

通過StoreConnector,獲取並監聽Store

構建ListView

class DrawList extends StatelessWidget {    final Widget header;    DrawList({        @required this.header    });    @override    Widget build(BuildContext context) {        return StoreConnector<AppState, DrawListViewModel> (            distinct: true,            converter: (store) => DrawListViewModel.fromStore(store),            builder: (context, viewModel){                return DrawListContent(                    header: this.header,                    viewModel: viewModel,                );            },        );    }}class DrawListContent extends StatelessWidget {    final Widget header;    final DrawListViewModel viewModel;    DrawListContent({        @required this.header,        @required this.viewModel    });    @override    Widget build(BuildContext context) {        return ListView.builder(            itemCount: this.viewModel.tradelines.length + 1,            itemBuilder: (BuildContext context, int index) {                if (index == 0) {                    return this.header;                }                Tradeline tradeline = this.viewModel.tradelines[index - 1];                bool isSelected = this.viewModel.currentTradeline.name ==                    tradeline.name;                var backgroundColor = isSelected                    ? const Color(0xFFEEEEEE)                    : Theme                    .of(context)                    .canvasColor;                return Material(                    color: backgroundColor,                    child: ListTile(                        onTap: () {                            viewModel.changeCurrentTradeline(tradeline);                            Navigator.pop(context);                        },                        selected: isSelected,                        title: Text(tradeline.name),                    ),                );            }        );    }}

關鍵點:

ListTile的屬性有限,設置Item的背景通過Material Widget,也可以通過Container Widget

ListView沒有header的概念,都是item

ListView沒有分隔線的Api,分隔線是由Item實現,通過ListTile.divideTiles()實現,其內部是通過DecoratedBox Widget實現

Navigator棧:Drawer,Dialog,Route都由Navigator棧管理,所以如下操作都是出棧操作Navigator.pop(context):

dismiss drawer

dismiss dialog

Back

Permission

Panel效果的源碼來自:flutter_gallery裡的Expansion panels例子,個人學習新技術的過程:

看官方的文檔

運行官方demo,思考如何實現,對照源碼的實現

具體的代碼,可通過下載源碼查看,這裡重點講一下Flutter的生命周期函數,在Flutter裡,StatelessWidget和StatefulWidget沒有生命周期,因為其是不可變的,只有State才有生命周期,如下所示: 

當數據變化時,StatelessWidget與StatefulWidget每次都會創建新的對象,並執行build()函數,State會被復用,造成flutter程序的如下特點:

StatelessWidget, StatefulWidget裡的成員變量都是final的,可以理解為React裡的props

State裡的成員變量可以理解為React裡的state,即為局部變量(Store裡的為全局變量)

State的initState()只執行一次,如果成員變量需要依據props而修改,可以在didUpdateWidget()裡更新

修改State的成員變量時,如果希望界面需要同步修改,需要在setState()裡修改,如下所示:--- 大家可以對比下與React的setState()有什麼區別?

   setState(() {        item.isExpanded = false;    });

如下所示:

class PermissionContent extends StatefulWidget {    final List<GitProject> projects;    final List<GitUser> users;    final Function addGitProject;    final Function deleteGitProject;    final Function getUserIdByName;    final Function deleteGitUser;    final Function allocationPermission;    const PermissionContent({        @required this.projects,        @required this.users,        @required this.addGitProject,        @required this.deleteGitProject,        @required this.getUserIdByName,        @required this.deleteGitUser,        @required this.allocationPermission    });    @override    _PermissionContentState createState() => _PermissionContentState();}class _PermissionContentState extends State<PermissionContent> {    static const Map<String, String> ACCESS_LEVEL = {...};    List<PanelItem> _panelItems;    PanelItem _userPanelItem;    PanelItem _rolePanelItem;    PanelItem _projectPanelItem;    @override    void initState() {        super.initState();        _userPanelItem = _initUserPanelItem();        _rolePanelItem = _initRolePanelItem();        _projectPanelItem = _initProjectPanelItem();        _panelItems = <PanelItem>[            _userPanelItem,            _rolePanelItem,            _projectPanelItem        ];    }    @override    void didUpdateWidget(PermissionContent oldWidget) {        super.didUpdateWidget(oldWidget);        // 更新數據        _projectPanelItem.value = widget.projects;        _userPanelItem.value = widget.users;    }    void _navigatorProjectPage(BuildContext context) async {...}    PanelItem _initUserPanelItem() {...}    PanelItem _initRolePanelItem() {...}    PanelItem _initProjectPanelItem() {...}        @override    Widget build(BuildContext context) {...}}

交互反饋

除了通過Widget構建界面外,有時我們還需要給用戶交互反饋: 1. Toasts/Snackbars:僅信息反饋,定時消失,不進Navigator棧

   Scaffold.of(context).showSnackBar(new SnackBar(        content: new Text("權限分配成功"),    ));

Dialog:信息反饋,有進一步交互,Natvigator棧管理

   // show:    showDialog(        context: context,        barrierDismissible: false,        builder: (BuildContext context){            return Dialog(                child: Row(                    mainAxisSize: MainAxisSize.min,                    children: [                        CircularProgressIndicator(),                        Text("Loading"),                    ],                ),            );        }    );    // Dismiss:    Navigator.pop(context);

Dialog僅僅只是modal,無法通過props來控制顯示與消失,只能監聽局部變量state或全局變量store來控制show與dismiss,分配權限的過程的代碼如下:

// 創建Completer對象Completer<bool> completer = Completer<bool>();// 發送action,通過igit的Api分配權限widget.allocationPermission(completer, users, level, projects);// 同時顯示LoadingDialogshowDialog(    context: context,    barrierDismissible: false,    builder: (BuildContext context){        return Dialog(            child: Row(                mainAxisSize: MainAxisSize.min,                children: [                    CircularProgressIndicator(),                    Text("Loading"),                ],            ),        );    });// 監聽成功與失敗,並顯示不同Toastscompleter.future.then((user){    Navigator.pop(context);    Scaffold.of(context).showSnackBar(new SnackBar(        content: new Text("權限分配成功"),    ));}, onError: (e){    Navigator.pop(context);    Scaffold.of(context).showSnackBar(new SnackBar(        content: new Text("權限分配失敗 ${e}"),    ));});

Project

效果如下: 

詳細細節請查看代碼,重點分享其中幾個關鍵點

LoadingView
除靜態頁面外,所有的頁面都有一個共同的加載流程:加載中...,失敗/成功。統一實現LoadingView,如下所示:

class ProjectListWrap extends StatelessWidget {    final ProjectListViewModel projectListViewModel;    ProjectListWrap({        this.projectListViewModel    });    @override    Widget build(BuildContext context) {        return LoadingView(            status: projectListViewModel.status,            loadingContent: PlatformAdaptiveProgressIndicator(),            errorContent: ErrorView(                description: '加載出錯',                onRetry: projectListViewModel.refreshProjects,            ),            successContent: ProjectListContent(                projects: projectListViewModel.projects,                nextState: projectListViewModel.nextStatus,                currentPage: projectListViewModel.currentPage,                hasNext: projectListViewModel.hasNext,                refreshProjects: projectListViewModel.refreshProjects,                fetchNextProjects: projectListViewModel.fetchNextProjects,            ),        );    }}

下滑加載下一頁
列表數據很多,通過滑動動態加載下一頁數據,監聽的方式與Android的類似,通過監聽其滑動位置,同時由於滑動是有狀態的,所以要使用StatefulWidget,如下所示:

class ProjectListContent extends StatefulWidget {    final List<GitProject> projects;    final LoadingStatus nextState;    final int currentPage;    final bool hasNext;    final Function refreshProjects;    final Function fetchNextProjects;    ProjectListContent({        @required this.projects,        @required this.nextState,        @required this.currentPage,        @required this.hasNext,        @required this.refreshProjects,        @required this.fetchNextProjects    });    @override    State<StatefulWidget> createState() => _ProjectListContentState();}class _ProjectListContentState extends State<ProjectListContent> {    final ScrollController scrollController = ScrollController();    @override    void initState() {        super.initState();        scrollController.addListener(_scrollListener);    }    @override    void dispose() {        scrollController.removeListener(_scrollListener);        scrollController.dispose();        super.dispose();    }    void _scrollListener() {        if (scrollController.position.extentAfter < 64 * 3) {            if(widget.nextState == LoadingStatus.success && widget.hasNext){                widget.fetchNextProjects(widget.currentPage + 1);            }        }    }    @override    Widget build(BuildContext context) {...}    Widget _nextStateToText() {        if(!widget.hasNext) {            return Text('加載成功,已無下一頁');        }        if(widget.nextState == LoadingStatus.error){            return Text('加載失敗,滑動重新加載');        }        return Text('加載中...');    }}

總結

Flutter是不同於ReactNative的跨端解決方案,是以一套代碼實現高開發效率與高性能為目標,沒有ReactNative的bridge,同時通過Dart解決javascript開發效率問題。

現在Flutter比較ReactNative的最大問題是:release下不支持"hot update",官方的解釋如下:

Often people ask if Flutter supports "code push" or "hot update" or other similar names for pushing out-of-store updates to apps.

Currently we do not offer such a solution out of the box, but the primary blockers are not technological. Flutter supports just in time (JIT) or interpreter based execution on both Android and iOS devices. Currently we remove these libraries during --release builds, however we could easily include them.

The primary blockers to this feature resolve around current quirks of the iOS ecosystem which may require apps to use JavaScript for this kind of over-the-air-updates functionality. Thankfully Dart supports compiling to JavaScript and so one could imagine several ways in which one compile parts of ones application to JavaScript instead of Dart and thus allows replacement of or augmentation with those parts in deployed binaries.

This bug tracks adding some supported solution like this. I'll dupe all the other reports here.

簡單翻譯:Flutter不支持release下的hot update,不是由於技術原因,而是iOS系統只支持javaScript實現無線更新功能,由於Dart可以轉換為Javasript代碼,所以有一種可能性:程序的一部分使用javascript,而不是dart,再通過動態下載這部分javascript代碼,實現hot update。

Flutter是否會成為主流的跨端解決方案,主要原因不在於其高的開發效率與高性能,主要是看Fuchsia作業系統的覆蓋程序,如果Fuchsia能成為主流的物聯網與Android設備的主流系統,Flutter才能真正成為主流。

參考

Technical Overview

Why I move to Flutter

Dart與消息循環機制[翻譯]

Flutter快速上車之Widget

Flutter, what are Widgets, RenderObjects and Elements?

Introduction to Redux in Flutter

User Feedback: Toasts / Snackbars

Code Push / Hot Update / out of band updates

相關焦點

  • 擁抱Flutter:關於Google Flutter Party的訪談
    閒魚技術團隊從flutter alpha版本開始,一直關注flutter技術,保持與flutter團隊的高頻互動,結合自己的業務特點和技術架構,推動了flutter hybird架構的實踐工作。成為flutter在全球大規模商業應用的典型樣版。
  • 從 Flutter Go 到 Flutter Go web - 手把手帶你輕鬆玩轉 Flutter-web(一)
    拉取 flutter_web 示例Flutter-web版本都是基於,web版本的 packages 包,所以要另起一個新的工程。為了避免創建的不一致性,基於官方的 flutter_web 示例做更改$ git clone https://github.com/flutter/flutter_web.git flutter_go_web3.
  • 【Flutter桌面篇】Flutter&Windows應用嘗鮮
    ---[·  git clone -b master https://github.com/flutter/flutter.git---[·  flutter --versionFlutter 1.20.0-3.0.pre.124 • channel master • https://github.com/flutter/
  • Flutter 入門路線圖
    富有表現力的精美用戶界面Flutter 內置的精美 Material Design 和 Cupertino(iOS-flavor)小部件,豐富的運動 API,流暢的自然滾動以及對平臺的了解,可為您的用戶帶來更多驚喜。native 級別的性能
  • Flutter 即學即用——03 在舊有項目引入 Flutter
    /.android/include_flutter.groovy'   ))   new File(settingsDir.parentFile,'my_flutter/.android/include_flutter.groovy' ) 解讀下這句話的意思就是指定 include_flutter.groovy
  • 【Flutter手摸手教學】02 Hello Flutter
    flutterflutter通過VSCode打開生成的flutter項目,直接刪掉lib/main.dart下面的所有內容,並把下面的代碼粘貼進去import 'package:flutterBuildContext context) { return MaterialApp( title: 'Hello Flutter', home: Scaffold( body: Center(child: Text("你好,Flutter")), ), ); }}在CMD中重新運行flutter
  • Flutter安裝及創建
    Flutter可以與現有的代碼一起工作flutter的git地址https://github.com/flutter/flutter/releasesFlutter SDK官網地址https://flutter.io/docs/development/tools
  • Flutter插件開發
    前言使用Flutter進行應用開發時,為實現一些功能(比如WebView加載網頁、實現視頻控制項等)我們會引入三方插件,這些插件我們都可以在https://pub.dartlang.org/flutter網站中進行查找,然後在flutter工程中配置pubspec.yaml文件來引入。
  • Flutter Weekly Issue 52
    [1]一個易遷移、兼容性高的 Flutter 富文本方案: https://www.yuque.com/xytech/flutter/kw6mn0[2]flutter_color_models: https://github.com/james-alex/flutter_color_models[3]FlutterToast: https://github.com
  • flutter: 一周感悟
    我之所以對 flutter 不感冒,源於我對 flutter 所使用的 dart 語言的無知 —— 我覺得既然市面上有 typescript 這樣可以滿足 flutter 需要的語言,為何要用 dart 這樣一個行將就木的語言呢?但最近有個朋友給我展示了他用 flutter 做的一個私人項目,驚豔到我,於是我開始學 flutter。
  • Flutter的體驗和實踐
    https://github.com/flutter/flutter/issues/9253iOS Pods 集成 Flutter 造成的問題由 Google 維護的編譯腳本(podhelper.rb),會在 pod install 的時候,把 Flutter 項目編譯產物和 Flutter 插件源碼 給編譯成本地 Pod 庫。
  • Flutter 混合開發
    ;import io.flutter.embedding.engine.FlutterEngine;import io.flutter.plugins.GeneratedPluginRegistrant;import io.flutter.plugin.common.MethodChannel;import android.content.ContextWrapper
  • Flutter Weekly Issue 65
    🔗 連結[1]koukicons-flutter: https://github.com/Ademking/koukicons_flutter[2]fontify: https://github.com
  • Flutter 完整開發實戰詳解 (Flutter 畫面渲染的全面解析) | 開發者說·DTalk
    I/flutter (32494): TransformLayer#f8fa5I/flutter (32494): │ owner: RenderView#2d51eI/flutter (32494): │ creator: [root]I/flutter (32494): │ offset: Offset(0.0, 0.0)I/flutter (32494
  • Flutter混合開發二-FlutterBoost使用介紹
    下面就集成FlutterBoost的方式分步說明Flutter module項目集成FlutterBoost在flutter_boost_module項目的pubspec.yaml文件中添加依賴插件配置dependencies:  flutter_boost: ^0.0.411配置完成後執行flutter packages get命令下載依賴插件到本地
  • Flutter 及 Chrome OS 團隊:Flutter 與 Chrome OS 珠聯璧合
    Flutterhttp://flutter.dev/Chrome OS 最佳實踐示例應用https://github.com/flutter/samples/tree/master/chrome-os-best-practices指南https://developer.android.google.cn/chrome-os
  • Flutter Plugin開發流程
    flutter create —org com.example —plugin flutter_text_plugin如果想支持swift或者kotlin,可以用如下命令進行創建:flutter create —org com.example —plugin -i swift -a kotlin flutter_text_plugin更多的參數選項
  • Flutter 中文文檔網站 flutter.cn 正式發布!
    今天我們非常高興的宣布:Flutter 社區中文資源網站 (flutter.cn) 和 Flutter 中文文檔正式發布,歡迎大家的訪問!我們同樣為 Flutter 的 codelabs 製作了一個單獨的二級頁面在 codelabs.flutter-io.cn,歡迎大家訪問。
  • 【手把手學習flutter】flutter打Android包基本配置和實踐
    一、背景在本地開發中,使用flutter run命令還是Android studio運行或者調試,flutter構建的是debug版本,也就是本地調試右上角出現debug標誌。當本地調試OK後,準備release版本,比如喲啊發布到應用商城,或者交付用戶使用。
  • Flutter 實戰4
    // 導入核心庫import 'dart:math';// 從外部包導入庫import 'package:test/test.dart';// 導入文件import 'path/to/my_other_file.dart';例如import 'package:flutter