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
reduxredux的結構非常簡單,如下所示:
由於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() ); } ...}
networkflutter的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
PermissionPanel效果的源碼來自: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