前言
當年React Native 正火的時候,我擼了一個一席的客戶端,最近抽空把我自己的專案用Flutter 寫一下,專案地址戳這裡,走過路過隨手給個star?,不勝感激; 以下是作為前端對Flutter 的一些看法和經驗的總結;
Dart
我在上手寫Flutter 的時候,其實一開始並沒有學習Dart,覺得有點類似TypeScript,Dart 很好上手,只在遇到一些不熟悉的問題時才去翻閱Dart文件,說一下一些不一樣的概念:
-
變數宣告
-
var
在JavaScript 和Dart 中,它都可以接受任意型別,但Dart中var的變數一旦賦值,型別便會確定,則不能再改變其型別;
var a; a = 'hello'; // a 已經確定為String型別 a = 1; // 報錯,型別不能更改 複製程式碼
-
dynamic & Object
javaScript中沒有dynamic 變數宣告,與var 不同,這兩個都支援宣告後改變變數型別,但Object 宣告的變數只能使用Object所擁有的屬性和方法,而dynamic 則支援所有屬性
-
final & const
從字面上可以看出這兩個都是宣告常量,但是const 變數是編譯時常量,而final 變數則在第一次使用時初始化;
-
-
非同步支援
在Javascript 和Dart中都有相同用法的async、await,但沒有Promise,取而代之的是Future,但沒有resolve 和reject
-
建構函式 在Dart 中,子類不會繼承父類的命名建構函式。如果不顯式提供子類的建構函式,系統就提供預設的建構函式。同時,寫法也變得更簡潔;
class Point { num x; num y; Point(this.x, this.y);// 這句等同於 /* Point(num x, num y) { this.x = x; this.y = y; } */ } 複製程式碼
-
箭頭函式
在Javascript 中,箭頭函式是作為一個影響this 作用域等的存在,但在Dart 中則是作為縮寫語法的存在,兩者的概念是不同的,應該區分清楚;
UI 佈局
首先我們來看看同樣的佈局,使用HTML + CSS 和Flutter 的寫法區別
在Flutter 中,一切UI 都基於Widget,在上圖中,Container 便是一個Widget,靠style 來設定樣式(也可以使用Theme,後文中細講),子類巢狀在child 中,。
class MainApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
複製程式碼
實際上這種寫法有點類似虛擬Dom,以樹形巢狀來編寫,但是這種寫法個人覺得維護起來很要命,如果沒有足夠細分元件的話,可讀性也會變得很差,實際上,Flutter 的issues 中也有關於類JSX 寫法的討論,對這種寫法的吐槽,最近在掘金沸點上看到一張很貼切的圖:
關於Widget 可以參考Flutter 中文網的Widget 目錄,具體的我就不展開寫了,下面講講一些不常見的需要注意的問題:
-
Expanded
不能用在不確定或者無限高度Widget(如SingleChildScrollView
) 中 -
BuildContext
的概念BuildContext
實際上是當前Widget 所建立的Element物件,在獲取元件尺寸,就需要用到MediaQuery.of(context).size
,路由跳轉時,也要用到Navigator.of(context)
,比較詳細的展開和理解說明可以參考深入理解BuildContext 這篇文章; -
Widget 的狀態管理
這裡要介紹一下
InheritedWidget
,InheritedWidget
是一個特殊的Widget,你可以將其作為另一個子樹的父級放在Widgets樹中。該子樹的所有子Widget 都能與該InheritedWidget
公開的資料進行互動,從而實現了Widget 間的通訊;更多狀態管理的方式可以參考 深入探索 flutter 中的狀態管理方式
樣式
在Flutter 中,樣式並沒有抽離出來,而是以各種(混亂甚至有點怪異)組合的方式來使用,設定文字要用TextStyle
,設定邊框背景等要用decoration
,感興趣的可以看看樣式的一些用法對比;
這裡要吐槽一下樣式的管理,在Flutter 中,可以使用Theme
來共享樣式,但是單個Widget 的樣式除了DefaultTextStyle
設定預設文字樣式外沒得繼承,還是要自己一個個寫,這裡就推動了對元件進行細化(不然懶得重複寫),主題有以下使用方式
-
全域性主題
new MaterialApp( title: title, theme: new ThemeData( brightness: Brightness.dark, ), ); 複製程式碼
-
區域性主題
new Theme( data: new ThemeData( accentColor: Colors.yellow, ), child: new Text('Hello World'), ); 複製程式碼
-
擴充主題
如果你不想覆蓋所有的樣式,可以繼承App的主題,只覆蓋部分樣式,使用copyWith方法。
new Theme( data: Theme.of(context).copyWith(accentColor: Colors.yellow), child: new Text('extend theme'), ); 複製程式碼
-
獲取主題
Theme.of(context)
會查詢Widget 樹,並返回最近的一個Theme物件。如果父層級上有Theme物件,則返回這個Theme,如果沒有,就返回App的Theme。建立好主題,只要在Widget的構造方法裡面通過Theme.of(context) 方法來呼叫。new Container( color: Theme.of(context).accentColor, chile: new Text( 'Text with a background color', style: Theme.of(context).textTheme.title, ), ); 複製程式碼
狀態元件
Stateful 與StateLess
用過React 的都知道無狀態元件和有狀態元件,在Flutter中,StatelessWidget
便是無狀態元件,它不依賴於除了傳入的資料以外任何其他資料,意味著改變傳入其建構函式的引數是改變其顯示的唯一方式。而StatefulWidget
則是有狀態元件,但是跟React有一點不同,在React 中,元件的render
和state 是在一起的,而Flutter 中,StatefulWidget
需要重寫createStae()
,返回一個State,而build
方法需要放在State 中,至於為什麼不放在StatefulWidget 呢?有兩點原因:
-
狀態訪問問題
由於
build
方法在state 每次改變時都會呼叫,在StatefulWidget
有很多狀態時,build
方法需要傳入一個State 引數,那麼,只能將State的所有狀態公開才能在State類外部訪問,但公開狀態後,狀態將不再具有私密性,這樣對狀態的修改將變得不可控;Widget build(BuildContext context, State state){ //state.a etc... ... } 複製程式碼
-
繼承StatefulWidget問題
當第一個情況發生後,如果有個子Widget 繼承自一個引入了抽象方法
build(BuildContext context)
的父Widget,那麼子Widget 在實現這個build
時都需要傳入一個state,此時父Widget 就必須將自己的state 傳入給子Widget,這樣就十分不合理,因為父Widget 的state 只與自身邏輯有關,且傳遞給子Widget 還需另外的傳遞機制,因此,應該將build
方法放在State 中。class ChildWidgert extends ParentWidget{ @override Widget build(BuildContext context, State state){ super.build(context, _parentWidgetState) } } 複製程式碼
生命週期
Flutter 的生命週期如下圖:
說一些常用的:-
initState
這個函式相當於在React 中的建構函式中初始化State,可以在這一步進行資料請求載入
-
didUpdateWidget
當呼叫了
setState
改變Widget 狀態時,Flutter 會建立一個新的 Widget 來繫結這個 State 並在此方法中傳遞舊 Widget ,如果你想比對新舊 Widget 並且對 State 做一些調整,或者某些 Widget 上涉及到 controller 的變更時,就可以在此回撥方法中移除舊的 controller 並建立新的 controller;@override void didUpdateWidget(AVCycleLess oldWidget){ super.didUpdateWidget(oldWidget); } 複製程式碼
-
dispose
當Widget 被釋放(如路由切換),Widget 中存在一些監聽或持久化的變數,你就需要在 dispose 中進行釋放。
FutureBuilder
當我們進入頁面進行一些耗時的操作,比如請求資料、初始化某些設定等時,我們通常需要顯示一個載入頁面,一般做法都是判斷資料狀態來切換顯示的元件,而在Flutter 中則有FutureBuilder
這種便利的解決方案,這裡展開篇幅會很長,可以參考FutureBuilder的使用方法和注意事項
路由
在Flutter 中,路由分為靜態路由和動態路由,靜態路由無法傳遞引數,所以在需要傳遞引數的情況下只能使用動態路由;
靜態路由
靜態路由在新建App 時定義,使用Navigator.of(context).pushNamed('/router/a');
進行切換,pushNamed 返回一個Future,可以接收來自下一個頁面的返回值。
return new MaterialApp(
home: new Text('hello'),
routes: <String, WidgetBuilder> {
'/router/a': (_) => new APage(),
'/router/b': (_) => new BPage(),
},
);
// then 說明
// 當前頁面
Navigator.of(context).pushNamed('/router/b').then((value) {
// value 為下一個頁面的返回值
});
// b 頁面
Navigator.of(context).pop('some data');
複製程式碼
動態路由
動態路由使用push
方法,傳入一個route 物件,在builder 中建立一個新頁面物件,如果需要自定義動畫效果,只需要使用PageRouteBuilder
替換MaterialPageRoute
,在transitionsBuilder
中定義動畫即可。
Navigator.of(context).push(new MaterialPageRoute(builder: (_) {
return new NewPage(data: 'some data');
}));
複製程式碼
網路請求
Dio
在Flutter 中,網路請求是由HttpClient
進行的,但其操作十分麻煩,所以有Dio 這麼一個優秀的請求庫來簡化我們的工作,需要注意的是,當App 只有一個資料來源時,Dio 應該使用單例模式
序列化
當我們獲取到資料時,通常我們都會拿到一個json,在JavaScript 中,我們可以很任意地直接使用點操作符來獲取資料中的欄位,但是在Dart中,你需要引入dart:convert
,並使用JSON.decode(json)
,但它返回的是一個Map<String, dynamic>
,意味著我們直到執行時才知道值的型別,也就失去了大部分靜態型別語言特性:型別安全、自動補全和最重要的編譯時異常。
但這樣一來,我們的程式碼可能會變得非常容易出錯。我們通常需要編寫模型類來序列化JSON,官方推薦了json_serializable
(相關操作看這裡) 來輔助我們生成庫序列化JSON,通過這種方式,我們就可以直接用點操作符來運算元據了。
如果還是嫌麻煩,可以試試JSONFormat4Flutter這一工具(我還沒用過,看著很不錯的樣子。)
事件處理
在Vue 中,我們只需要使用@click
之類的方法即可監聽事件,而React 中則是onClick
之類的方法,但在Flutter 中,我們需要將需要監聽事件的元素包裹在GestureDetector
中,使用onTap
等方法來處理事件,對事件的行為表現,我們可以通過設定behavior
來控制,
enum HitTestBehavior {
deferToChild, // 子widget會一個接一個的進行命中測試,如果子Widget中有測試通過的,則當前Widget通過,這就意味著,如果指標事件作用於子Widget上時,其父(祖先)Widget也肯定可以收到該事件。
opaque,// 在命中測試時,將當前Widget當成不透明處理(即使本身是透明的),最終的效果相當於當前Widget的整個區域都是點選區域
translucent,// 當點選Widget透明區域時,可以對自身邊界內及底部可視區域都進行命中測試,這意味著點選頂部widget透明區域時,頂部widget和底部widget都可以接收到事件
}
複製程式碼
Canvas
在Flutter 中,如果需要使用Canvas,我們需要繼承CustomPainter 並重寫paint方法來繪製自定義圖形。在使用Canvas時,我們需要知道三個概念:
-
canvas
畫布物件,包括了各種繪製方法,用來繪製各種圖形
-
size
當前繪製區域的大小
-
paint
畫筆,用來控制畫出來的各種屬性,如顏色、描邊及抗鋸齒等;
使用例子如下:
class MyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
canvas.drawRect(Offset.zero & size, Paint()
..isAntiAlias = true // 抗鋸齒
..style = PaintingStyle.fill // 填充,stroke則為使用描邊
..color = Color(0xFF000000) // yanse
);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false; // 強制不重繪,提高效能
}
複製程式碼
複用
Mixin
說到mixin ,相信Vue 和React 的使用者都很熟悉,雖然React中mixin已 被高階函式或Decorator取代,但在Flutter 中,mixin 還是得以保留。
它使用with
來引入一個mixin,定義的方式如下:
class A {
int a = 1;
void b(){
print('c');
}
}
class B with A{
}
B b = new B();
print(b.a);
b.b();
複製程式碼
不過,mixin 在 Dart 中是有以下使用條件的:
- mixins類只能繼承自object
- mixins類不能有建構函式
- 一個類可以mixins多個mixins類
- 可以mixins多個類,不破壞Flutter的單繼承
Keep-alive
在使用Tab 時,切換Tab後,每個Tab 都會被銷燬然後重建,於是會多次呼叫initState,那有沒有類似Vue 中的<keep-alive>
元件一樣的存在呢?答案是有的,那就是AutomaticKeepAliveClientMixin
。只需要繼承這個mixin並實現wantKeepAlive
方法即可。但widget在不顯示之後也不會被銷燬仍然儲存在記憶體中,所以慎重使用這個方法
class APageState extends State<APage> with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
// ...
}
複製程式碼
後話
以上只是我這10天斷斷續續做出第一個粗糙的Flutter App所學到的東西,有些是查資料過程中看到的一些知識點,並沒有用在專案中,還有很多細緻的或者沒遇到過的東西值得探討,等以後遇到了有機會再講講。
參考
- Flutter 官網(強烈建議以官方文件為準,比較方便查詢)
- Flutter實戰 (十分推薦)
- Flutter 中文網