Google自2017年第一次提出Flutter, 到2018年Beta, 再加之RN的各種風波與問題, 使得Flutter的熱度不斷上升, 國內不少公司都公佈Flutter在其產品中的應用, 如美團, 閑魚等. ...
作為忠實與較資深的Android汪, 最近抽出了一些時間研究了一下Google的親兒子Flutter, 尚屬皮毛, 只能算是個簡單的記錄吧.
Google自2017年第一次提出Flutter, 到2018年Beta, 再加之RN的各種風波與問題, 使得Flutter的熱度不斷上升, 國內不少公司都公佈Flutter在其產品中的應用, 如美團, 閑魚等.
前言
Flutter作為跨平臺框架, 常常被人拿出來與React Native, 以及Xamarin進行對比, 除了大家都是跨平臺框架之外且能達到近乎Native的體驗之外, Flutter與這兩者的原理大不相同.
讓我們來看看這三者的結構圖吧.
可能有一些複雜, 咱大致解釋一下.
React Native跟Xamarin都是基於mapping native代碼來實現所謂的Native體驗的框架, 只是RN基於JS引擎 + Bridge與native打交道, 並且在運行時進行綁定, 而Xamarin是基於微軟的基於Linux的C#虛擬機mono + JNI與native進行通信.
這裡Android與iOS還是有差別的, 如RN在iOS上JS引擎不支持JIT, 會一定程度影響效率, Xamarin在iOS上可以直接編譯成iOS平臺可以執行的程式, 所以在實際運行起來的性能是一樣的, 唯一的差別就是微軟得更快的支持API同步.
對於Flutter來說, 由於他的渲染引擎使用了Skia直繪, 加上基於C++的Dart引擎, 所以在不同平臺上沒有差別, 加之其實現了Android Material Design與iOS Cupertino兩套UI組件, 所以即便是自繪組件, 看起來還是跟原生的一個樣子.
通過對三種跨平臺引擎的大致瞭解, 我們可以看出來, 他們都達到了一定程度的Native體驗, 然則各自都有一定的性能損耗, 比如RN的JS引擎載入JS, 以及Bridge通信的損耗, Xamarin Mono虛擬機與Java通信的損耗, 以及Flutter Skia渲染與Native Android渲染的差異等.
Flutter筆記
如何啟動一個app
Android需要在Manfest裡面指定帶有MAIN action與LAUNCHER category的Activity聲明, 而Flutter只需要一行.
void main() => runApp(MyApp());
其中MyApp就是一個普通的Widgets(View).
View vs Widgets
Flutter沒有View, 與之對應的是Widget, 並且分為StatelessWidgets與StatefulWidgets, 前者是個靜態View, 後者是動態通過Data來更新的View.
- Stateless
Text(
'I like Flutter!',
);
- Stateful
class StatefulText extends StatefulWidget {
@override
State<StatefulWidget> createState() => _TextState();
}
class _TextState extends State<StatefulText> {
// Default placeholder text
String textToShow = "I Like Flutter";
void _updateText() {
setState(() {
// update the text
textToShow = "Flutter is Awesome!";
});
}
@override
Widget build(BuildContext context) {
...invoke _updateText
}
}
實際上是因為StatefulWidgets通過調用State
的setState
方法來觸發整個Widgets樹的重繪, 並且在重繪之前會調用傳進去的(){ ... }
block.
怎麼寫Layout, XML到哪裡去了.
實際上Flutter沒有xml了, 並且是通過Widgets的嵌套來實現一個佈局的.
如:
Center
是一個可以把子View放置在中央的容器.Row
對應的就是LinearLayout + Horizontal,Column
對應的就是LinearLayout + Vertical, 他們都具備一個屬性叫做crossAxisAlignment
, 有點類似gravity
, 來控制子View相對於父View的位置.Expanded
支持一個類似weight的屬性, 叫flex
.Container
是一個具有decoration
屬性的容器, 可以用來控制背景色, border, margin等等.Stack
有點像是一個特殊的RelatetiveLayout或者ConstraintLayout,children
屬性指定了它的子View, 第一個是Base View,alignment
屬性指定了後面的子View相對於BaseView的位置, 如alignment: const Alignment(0.6, 0.6)
指定了位於BaseView右下角的位置.ListTile
是一個特殊的ListItem, 有三個屬性, 分別是左邊的Icon (leading), 文字 (title), 以及右邊的Icon (trailing).- 還有諸如
ListView
,GridView
,Card
等等比較熟悉的Widgets.
另外有一個類似於我們Activity的Widgets:
- 叫做
MaterialApp
, 可以指定theme
,title
, 以及子Viewhome
, 還有更重要的頁面跳轉routes
.
MaterialApp(
title: 'Welcome to Flutter',
home: ...,
routes: <String, WidgetBuilder> ...,
theme: ThemeData(
primaryColor: Colors.white
),
)
還有一個類似於Fragment的:
- 叫做
Scaffold
, 中文意思是腳手架
, 它包含一個appBar (ActionBar)與一個body, appBar可以指定title與actions (類似於action button的點擊事件).
Scaffold(
appBar: AppBar(
title: Text(widget.title),
actions: <Widget>[...],
),
body: ...,
)
如何從父View中Remove一個元素
答案是沒有... 因為在Flutter看來, Widgets的樹結構是不可以被更改的, 但是如果想更改, 則是通過StatefulWidgets的方法, 通過setState來更改Data, 觸發Widgets重繪, 從而替換掉之前的Widgets.
喜歡畫Canvas的同學怎麼辦?
Flutter同樣支持, CustomPaint
作為一個 Widgets就支持傳入一個實現CustomPainter
抽象類的參數, 而CustomPainter
的抽象方法也類似於Android View的onDraw
.
void paint(Canvas canvas, Size size)
bool shouldRepaint(CustomPainter oldDelegate)
如何自定義View
不用繼承, 而使用類似Android ViewGroup的辦法, 通過組合(composing)與封裝的方法來實現, 通過小Widgets組合成需要的新Widgets.
頁面跳轉怎麼辦, 四大組件之一的Intent跑哪裡去了
貌似在講類似於Activity的MaterialApp
的時候劇透了...
就是使用Navigator
與Routes
來實現界面跳轉, 實際上是整個Widgets的替換.
routes: <String, WidgetBuilder> {
'/a': (BuildContext context) => MyPage(title: 'page A'),
'/b': (BuildContext context) => MyPage(title: 'page B'),
'/c': (BuildContext context) => MyPage(title: 'page C'),
}
Navigator.of(context).pushNamed('/b');
如何處理外部的Intent
實際上還是需要在Flutter App的Android殼子中註冊這個filter, 然後在FlutterActivity中拿到存下來,
FlutterView初始化後再通過Bridge, 官方叫MethodChannel
從Java里獲取,進行下一步邏輯.
可以看個簡單的例子.
new MethodChannel(getFlutterView(), "app.channel.shared.data").setMethodCallHandler(
new MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
if (call.method.contentEquals("getSharedText")) {
result.success(sharedText);
sharedText = null;
}
}
});
getSharedText() async {
var sharedData = await platform.invokeMethod("getSharedText");
if (sharedData != null) {
setState(() {
dataShared = sharedData;
});
}
}
常用的startActivityForResult怎麼辦.
這個Flutter有完全對應的辦法, 而且用起來很方便, 結合之前說的頁面跳轉:
Map xxx = await Navigator.of(context).pushNamed('/xxx');
Navigator.of(context).pop({xxx});
非同步怎麼辦, runOnUiThread()哪裡去了
Flutter有點像JS, 是一個單線程模式, 所以只是通過模擬來構建簡單的非同步, 關鍵字就是類似於kotlin coroutines一樣, 通過await
+async
來處理.
如:
loadData() async {
response = await http.get(xxx);
setState(() {xxx});
}
但是由於它的單線程, 所以無法做很長的阻塞操作, 像http請求的延遲正常情況可能都是毫秒級的, 但是數據的處理等, 可能就得秒級了.
這也是RN線上程方面的做android程式的一個痛點, Flutter採用了比較容易想到的曲線救國的辦法, 提供了一個叫Isolate
的對象, 它實際是一個基於socket的數據通道, 相當於把數據放在一個獨立的進程進行處理, 然後再通過socket發送回程式進程, 還記得進程間通信辦法之一的管道
嗎...
Flutter 替代OkHttp的網路庫
自帶了http庫, 直接http.get(url)
, 線上程部分的代碼實例里也有涉及.
通過類似gradle的文件pubspec.yaml
引入.
dependencies:
...
http: ^0.12
^
表示不升大版本, 並取最新版本, 比gradle的+要範圍更小.
常見的LCE(Loading Content Error)裡面的Loading怎麼show
Flutter有一個widget叫做ProgressIndicator
, 比如我們期望有一個轉圈圈的Loading界面在數據載入出來之前.
我們就可以通過StatefulWidgets, 根據數據, 或者List Widgets的個數 (如果是顯示一個List的話)來判斷是否顯示Loading, 使用子類CircularProgressIndicator
, 來替換頁面的Widgets.
當然也是通過setState(() {...})來觸發界面刷新的, 可以在initState()內觸發載入數據的非同步操作.
不同解析度的圖片資源怎麼放
這個有點像iOS了, 即有1x,2x,3x:
images/my_icon.png // Base: 1.0x image
images/2.0x/my_icon.png // 2.0x image
images/3.0x/my_icon.png // 3.0x image
不一樣的一點還需要添加到類似gradle的文件pubspec.yaml
里.
assets:
- images/my_icon.jpeg
字元串怎麼存儲
Flutter沒有像Android的string.xml
的東西, 目前來說最好的就就是存成靜態字元串.
class Strings {
static String welcomeMessage = "Welcome To Flutter";
}
Text(Strings.welcomeMessage)
Gradle變成什麼了
前面說網路庫, 圖片資源的時候提到過, 提供了一個叫pubspec.yaml
的文件, 具體支持的規則可以查看這個文檔.
Fragment與Activity呢?
之前做過類比, 如MaterialApp
有點類似於Activity, 而Scaffold
都點類似Fragment, 實際上他們兩個都是Flutter的Widgets, 也就是說其實只有View的概念了.
還有生命周期嗎?
Flutter有一個叫做WidgetsBinding
的可以提供類似生命周期的回調.
四種狀態inactive
(iOS專用), paused
(相當於onPause, 退後臺), resumed
(相當於onPostResume, 到前臺), suspending
(android專用, 相當於onStop).
一般在StatefulWidgets的State中註冊與反註冊.
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
ScrollView vs ListView
Flutter沒有ScrollView, 合併到了ListView, 通過ListView.builder創建的ListView提供了View復用的邏輯.
ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return Text(xxx);
}))
其中itemBuilder有點像Android ListView的getView, 官方文檔說它會自動回收Element給你, 但是事實上每次你都需要根據position生成新的Widgets, 所以呢應該是Flutter在內部回收了之前的Widgets併在你重新創建的時候又用上了.
BTW, 通過ListView構造來顯示就不具備這種特性, 所以大量數據需要用Builder.
Flutter橫豎屏怎麼玩.
因為它實際上還是藉助了Android程式的殼子, 所以如果AndroidManifect定義了android:configChanges="orientation|screenSize"
, 則Flutter會自己hanlde.
怎麼處理Gesture
Flutter提供了GestureDetector
, 它相當於一個Container, 將我們期望接收手勢的Widgets放進去, 再實現事件回調就行了.
GestureDetector(
child: FlutterLogo(
size: 200.0,
),
onTap: () {
print("tap");
},
)
它同樣支持其他的手勢, 如onDoubleTap
等等等.
字體怎麼弄
首先需要在pubspec.yaml
裡面配置需要的字體庫:
fonts:
- family: MyCustomFont
fonts:
- asset: fonts/MyCustomFont.ttf
- style: italic
然後在Text的style
屬性進行配置.
Text(
'This is a custom font text',
style: TextStyle(fontFamily: 'MyCustomFont'),
)
Hint哪裡去了, 錯誤信息怎麼輸出
對於輸入框的Hint基本一致, 可能就是換了個名字, 一看便知.
TextField(
decoration: InputDecoration(hintText: "This is a hint", errorText: _getErrorText()),
)
總結
Flutter在視圖渲染上另闢蹊徑, 性能優勢凸顯, 在跨平臺框架屬於一匹黑馬, 又有Google撐腰, 值得在Mobile勤耕多年的同學入手.
由於作者曾經從事過2年的Webkit開發工作, 拜讀了Flutter的渲染模式, 很像是Webkit/Chrome/Blink的思路, 通過查證, 起草者確實有大批同樣的人, 如果你還沒有入坑RN, 或許Flutter可以作為跨平臺方案學習的首選哦.
同樣Google自己也有很多Plugin去支持更多擴展功能, 如GPS, Camera, SharePreference, Database. 還例如Firebase這種親兒子級的服務也是全面支持Flutter. 這些都可以通過Dartlang來查詢.
當然也可以自己去開發需要的Plugin來適配需要的功能, 基於的技術就是上面有提的MethodChannel
, NDK的支持也是同樣的道理.
Reference
https://flutter.io/docs/get-started/flutter-for/android-devs