Flutter 初見
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/main dev.dart:程式的入口檔案,與c語言類似,dart程式的入口為main()函式,main dev.dart的區別是使用DevToolsStore,用於檢視store與action
-
App.dart:最外層的配置,如下所示:
@override Widget 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實現資料監聽,如下所示:
@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, ); }, ); }
在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); // 同時顯示LoadingDialog showDialog( context: context, barrierDismissible: false, builder: (BuildContext context){ return Dialog( child: Row( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(), Text("Loading"), ], ), ); } ); // 監聽成功與失敗,並顯示不同Toasts completer.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/">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