iOS開發者入門Flutter
首先說一下,爲什麼要關心iOS和Flutter的區別問題。因爲移動端開發的業務邏輯設計模式等是一致的,區別可能只在於使用的語言不同,實現邏輯的風格不同而已。所以這裏我們先分析一下iOS和Flutter的區別到底有哪些,有利於我們更快地去入門。
生命週期:
頁面加載的生命週期:
移動端開發首先要關注的一點肯定要了解一個頁面加載的生命週期,就像瞭解iOS的viewcontroller的生命週期在UI繪製的場景中有多麼重要:
iOS:
iOS的設計初衷就是MVC,所以iOS中的核心是Controller。每個Controller都有自己的生命週期:
//類的初始化方法
+ (void)initialize;
//對象初始化方法
- (instancetype)init;
//從歸檔初始化
- (instancetype)initWithCoder:(NSCoder *)coder;
//加載視圖
-(void)loadView;
//將要加載視圖
- (void)viewDidLoad;
//將要佈局子視圖
-(void)viewWillLayoutSubviews;
//已經佈局子視圖
-(void)viewDidLayoutSubviews;
//內存警告
- (void)didReceiveMemoryWarning;
//已經展示
-(void)viewDidAppear:(BOOL)animated;
//將要展示
-(void)viewWillAppear:(BOOL)animated;
//將要消失
-(void)viewWillDisappear:(BOOL)animated;
//已經消失
-(void)viewDidDisappear:(BOOL)animated;
//被釋放
-(void)dealloc;
flutter:
這一點flutter則有所不同,flutter頁面加載的核心則是build一棵渲染樹的過程。
如圖所示大體上它的生命週期可以分爲三個過程:初始化,狀態改變,銷燬。每一次build都是頁面的一次加載。
didUpdateWidget: Flutter中的Widget分爲兩種:stateful(狀態可變)和stateless(狀態不可變)。如果是stateful的widget需要實現setState方法。當調用了 setState 將 Widget 的狀態被改變時 didUpdateWidget 會被調用,Flutter 會創建一個新的 Widget 來綁定這個 State,並在這個方法中傳遞舊的 Widget ,因此如果你想比對新舊 Widget 並且對 State 做一些調整,你可以用它,另外如果你的某些 Widget 上涉及到 controller 的變更,要麼一定要在這個回調方法中移除舊的 controller 並創建新的 controller 監聽。
dispose:某些情況下你的 Widget 被釋放了,一個很經典的例子是 Navigator.pop 被調用,如果被釋放的 Widget 中有一些監聽或持久化的變量,你需要在 dispose 中進行釋放。很重要的一個應用是在 Bloc 或 Stream 時在這個回調方法中去關閉 Stream。
可參考博客:https://segmentfault.com/a/1190000015211309
App 的生命週期
iOS中的APP的生命週期在appdelegate裏設置,這裏不多說。
而在flutter中如果我們想監聽 App 級別的生命週期,可以通過向Binding 中添加一個 Observer,同時要實現didChangeAppLifecycleState來監聽指定事件的到來,並且最後還需要在 dispose 回調方法中移除這個監聽。但限制是它在iOS平臺智能監聽三種狀態。
class LifeCycleDemoState extends State<MyHomePage> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.inactive:
print('Application Lifecycle inactive');
break;
case AppLifecycleState.paused:
print('Application Lifecycle paused');
break;
case AppLifecycleState.resumed:
print('Application Lifecycle resumed');
break;
default:
print('Application Lifecycle other');
}
}
@override
void dispose(){
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}
詳細使用源碼看這裏:https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/binding.dart
佈局:
Flutter的佈局首先要建立一棵Widget樹(分析一下UI圖建立一棵控件樹),根據樹的結構層層嵌套Widget控件。margin和padding等的約束佈局設置則有點類似css。
UIView => Widgets
iOS:UIView可變,UIView發生改變本質上是間接調用(官方建議不要顯式調用,因爲這開銷很大)了LayoutSubviews方法進行佈局上的重構,drawRect進行顯示上的重構,updateConstrains進行約束上的重構。三者的更新會在每次RunLoop之後進行update cycle操作(也可以調用layoutIfNeeded等方法進行立刻更新)。(推薦一篇講述佈局不錯的文章:https://juejin.im/post/5a951c655188257a804abf94)
Flutter:Widget不可變,也正是由於不可變性,使得Widget比起UIView來說更輕量,因爲它不是控件,只是UI的描述。
- widget的分爲Stateful(狀態可變)和Stateless(狀態不可變)兩種,Stateful本質上也是不可變的,它只是將狀態拆分到State中去管理。State會存儲Widget的狀態數據,並在widget樹重建時攜帶着它,因此狀態不會丟失。
- 即使一個widget是有狀態的,包含它的父親的widget也可以是無狀態的,只要父widget本身不響應這些變化即可。
- 在UI更新這一點上感覺flutter會比iOS要麻煩一點,例如點擊按鈕導致Text的文字發生變化,iOS只需要簡單的target-action就可以,而flutter則需要將無狀態的text套在一個StatefulWidget父類裏面纔可以。
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
// Default placeholder text
String textToShow = "Flutter";
void _updateText() {
setState(() {
// update the text
textToShow = "Text is changed!";
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("My App"),
),
body: Center(child: Text(textToShow)),
floatingActionButton: FloatingActionButton(
onPressed: _updateText,
child: Icon(Icons.update),
),
);
}
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.frame = CGRectMake(300, 600, 80, 40);
[button setTitle:@"點擊" forState:UIControlStateNormal];
[button addTarget:self action:@selector(onClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
self.textField = [[UITextField alloc] initWithFrame:CGRectMake(100, 300, 200, 60)];
self.textField.text = @"iOS";
[self.view addSubview:self.textField];
}
-(void) onClick
{
self.textField.text = @"text has changed!";
}
- 在佈局方面,iOS分爲xib和storyboard還有代碼佈局的方式,flutter比較統一,通過建立一棵widget樹進行佈局,同時還有類似Center,Column等Widget進行佈局。添加約束的話,iOS通過autolayout來添加約束,而flutter則是通過使用container容器添加padding,margin等屬性來添加約束。這點非常類似html和css的佈局原理。如下所示:
-
父子視圖的移除方面,比如說舉個常見的例子,一個bool值的改變導致兩個視圖的切換,iOS需要在父view中調用addSubview()或在子view中調用removeFromSuperView() 來動態添加或移除子view,如果涉及到一些傳值問題可能還需要通過觀察者模式來觀察bool值的更新。而在flutter中則需要向父widget中傳入一個返回widget的函數,並用bool來控制子widget的創建。
class SampleApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Sample App', theme: ThemeData( primarySwatch: Colors.blue, ), home: SampleAppPage(), ); } } class SampleAppPage extends StatefulWidget { SampleAppPage({Key key}) : super(key: key); @override _SampleAppPageState createState() => _SampleAppPageState(); } class _SampleAppPageState extends State<SampleAppPage> { bool toggle = true; void _toggle() { setState(() { toggle = !toggle; }); } _getToggleChild() { if (toggle) { return Text('View One'); } else { return CupertinoButton( onPressed: () {}, child: Text('View Two'), ); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Sample App"), ), body: Center( child: _getToggleChild(), ), floatingActionButton: FloatingActionButton( onPressed: _toggle, tooltip: 'Update Text', child: Icon(Icons.update), ), ); } }
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; button.frame = CGRectMake(350, 750, 50, 30); [button setTitle:@"切換" forState:UIControlStateNormal]; [button addTarget:self action:@selector(onClick) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:button]; self.textField = [[UITextField alloc] initWithFrame:CGRectMake(100, 300, 200, 60)]; self.textField.text = @"View 1"; [self.view addSubview: self.textField]; self.button2 = [UIButton buttonWithType:UIButtonTypeSystem]; self.button2.frame = CGRectMake(100, 300, 200, 60); [self.button2 setTitle:@"View 2" forState:UIControlStateNormal]; } -(void) onClick { if (self.textField.superview) { [self.textField removeFromSuperview]; [self.view addSubview:self.button2]; } else { [self.button2 removeFromSuperview]; [self.view addSubview:self.textField]; } }
導航
UIViewController => Scaffold
Flutter中沒有專門用來管理視圖而且類似那種和View一對一的Controller類。有類似的Scaffold,其包含控制器的appBar,也可以通過body設置一個widget來做其視圖。
頁面跳轉
iOS有UINavigationController棧進行push,pop操作,其並不負責顯示,而是負責各個頁面跳轉。或者使用模態視圖。
同時注意iOS通過navigationController導航時要記得添加NavigationController,可在stroyboard設置,如果代碼的話添加如下:
ViewController *vc = [ViewController new];
UINavigationController *navigation = [[UINavigationController alloc] initWithRootViewController:vc];
self.window.rootViewController = navigation;
[self.window makeKeyWindow];
// -------- ViewController --------
-(NextViewController *)next
{
if (!_next)
{
_next = [NextViewController new];
}
return _next;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.frame = CGRectMake(350, 750, 50, 30);
[button setTitle:@"切換" forState:UIControlStateNormal];
[button addTarget:self action:@selector(onClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
-(void) onClick
{
[self.navigationController pushViewController:self.next animated:YES];
// [self presentViewController:self.next animated:YES completion:nil];
}
// -------- NextViewController --------
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// Do any additional setup after loading the view.
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.frame = CGRectMake(350, 750, 50, 30);
[button setTitle:@"返回" forState:UIControlStateNormal];
[button addTarget:self action:@selector(onClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
-(void) onClick
{
[self.navigationController popViewControllerAnimated:YES];
// [self dismissViewControllerAnimated:YES completion:nil];
}
Flutter中可以將MaterialApp理解爲iOS的導航控制器,其包含一個navigationBar以及導航棧,這點和iOS是一樣的。
Flutter中使用了 Navigator
和 Routes
。一個路由是 App 中“屏幕”或“頁面”的抽象,而一個 Navigator 是管理多個路由的 widget。可以粗略地把一個路由對應到一個 UIViewController
。Navigator 的工作原理和 iOS 中 UINavigationController
非常相似,當你想跳轉到新頁面或者從新頁面返回時,它可以 push()
和 pop()
路由。
在頁面之間跳轉,有如下選擇:
- 具體指定一個由路由名構成的
Map
。(MaterialApp) - 直接跳轉到一個路由。(WidgetApp)
下面是構建一個 Map 的例子:
void main() {
runApp(MaterialApp(
home: MyAppHome(), // becomes the route named '/'
routes: <String, WidgetBuilder> {
'/a': (BuildContext context) => MyPage(title: 'page A'),
'/b': (BuildContext context) => MyPage(title: 'page B'),
'/c': (BuildContext context) => MyPage(title: 'page C'),
},
));
}
通過把路由的名字 push
給一個 Navigator
來跳轉:
Navigator.of(context).pushNamed('/b');
頁面傳值
iOS中頁面傳值正向直接通過屬性傳輸即可,反向的話可以通過Block,delegate,通知等方式進行傳值。
而在Flutter中反向傳值就簡單了很多,舉個例子,要跳轉到“位置”路由來讓用戶選擇一個地點,可能要這麼做:
Map coordinates = await Navigator.of(context).pushNamed('/location');
之後,在 location 路由中,一旦用戶選擇了地點,攜帶結果一起 pop()
出棧:
Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});
多線程
RunLoop
VS EventLoop
說起Flutter的多線程前先談一下Flutter中的Event Loop。我們都知道前端開發框架大都是事件驅動的。意味着程序中必然存在事件循環和事件隊列。事件循環會不斷地從事件隊列中獲取和處理各種事件。
iOS:iOS中的類似機制是RunLoop,可以通過一個RunLoopObserver來實現對RunLoop的觀察,當遇到Source0,Source1或者Timer等事件會進行處理。下面圖可以很清晰的理解整個過程。(關於RunLoop的講解推薦一篇很詳細的文章:https://juejin.im/post/5add46606fb9a07abf721d1d)
Flutter:Flutter中的Event Loop和JavaScript的基本一樣。循環中有兩個隊列。一個是微任務隊列(MicroTask queue),一個是事件隊列(Event queue)。這裏類似iOS中存在set裏面的Source0和Source1。
- event queue: 主要是外部事件,負責處理I/O事件、繪製事件、手勢事件、Timer等
- microtask queue:可以自己向
isolate
內部添加事件,事件的優先級比event queue
高。
兩個隊列是有優先級的,當isolate
開始執行後,會先處理microtask
的事件,當microtask
隊列中沒有事件後,纔會處理event
隊列中的事件,並按照這個順序反覆執行。當執行microtask
的事件時會阻塞event
隊列,這會導致渲染響應手勢等event
事件響應延遲。爲保證渲染和手勢響應,所以應儘量將耗時操作放到event
隊列中。
線程和協程:
Flutter中的多線程和異步是比較難理解的一塊。先看兩個語法糖:
-
Future:
學過js的童鞋應該很容易理解這塊了,Future就是js裏的Promise,曾經js裏面延時只能層層回調,這樣很容易造成回調地獄。所以應運而生了Promise(Future),它是一個鏈式操作,可以通過追加then方法進行多層處理。
-
async/await:用法完全沿用自js。
多協程:
我們拆開來看,首先對於異步操作這塊,flutter基本上是沿用ES6的async和await這些異步語法糖以及Future。這裏涉及到一個和傳統意義不一樣的概念——協程(https://www.itcodemonkey.com/article/4620.html這篇漫畫講的比較生動,https://www.zhihu.com/question/308641794講解爲什麼協程比線程要好),最早接觸是在python裏有遇到過,在Flutter中,執行到async則表示進入一個協程,會同步執行async的代碼塊。當執行到await時,則表示有任務需要等待,CPU則去調度執行其他IO。過一段時間CPU會輪詢一次查看某個協程是否任務已經處理完成,有返回結果可以被繼續執行,如果可以被繼續執行的話,則會沿着上次離開時指針指向的位置繼續執行。也就是await標誌的位置。
iOS中有沒有類似的實現呢,其實是有的,個人感覺是串行隊列中的dispatch_async
操作,遇到耗時操作也不會阻塞,而是放到事件隊列中等待當前操作執行完再繼續執行。Flutter在當執行到await
時,保存當前的上下文,並將當前位置標記爲待處理任務,用一個指針指向當前位置,並將待處理任務放入當前線程的隊列中。在每個事件循環時都去詢問這個任務,如果需要進行處理,就恢復上下文進行任務處理。
多線程:
而async和await是沿用自js的,但有一點要注意的是,js是腳本語言,所以必須是單線程的,到這裏就足夠了。而flutter是手機框架,很可能我們要進行很耗時的IO操作,這僅僅憑異步是解決不了的,必須要多線程。這裏flutter引入了新的方案,叫做isolate。
但isolate和普通的Thread還不同,它具有獨立的內存,isolate間的通信通過port來實現,這個port消息傳遞的過程是異步的。實例化一個isolate的過程也就是實例化isolate這個結構體、在堆中分配線程內存,配置port。
感覺從操作來看其實isolate更像是進程,而實際async則更像是線程操作。
loadData() async {
// 通過spawn新建一個isolate,並綁定靜態方法
ReceivePort receivePort =ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
// 獲取新isolate的監聽port
SendPort sendPort = await receivePort.first;
// 調用sendReceive自定義方法
List dataList = await sendReceive(sendPort, 'https://jsonplaceholder.typicode.com/posts');
print('dataList $dataList');
}
// isolate的綁定方法
static dataLoader(SendPort sendPort) async{
// 創建監聽port,並將sendPort傳給外界用來調用
ReceivePort receivePort =ReceivePort();
sendPort.send(receivePort.sendPort);
// 監聽外界調用
await for (var msg in receivePort) {
String requestURL =msg[0];
SendPort callbackPort =msg[1];
Client client = Client();
Response response = await client.get(requestURL);
List dataList = json.decode(response.body);
// 回調返回值給調用者
callbackPort.send(dataList);
}
}
// 創建自己的監聽port,並且向新isolate發送消息
Future sendReceive(SendPort sendPort, String url) {
ReceivePort receivePort =ReceivePort();
sendPort.send([url, receivePort.sendPort]);
// 接收到返回值,返回給調用者
return receivePort.first;
}
(代碼引用自https://lequ7.com/2019/04/26/richang/shen-ru-li-jie-Flutter-duo-xian-cheng/)
網絡請求和數據解析
這點不做詳述,理解了Dart的多線程以及網絡請求的基本原理也很容易搞懂這塊(https://flutterchina.club/networking/)。
總結
文章不從性能進行分析,而是僅僅從語法方面進行入門比對。其實如果做過Web前端的童鞋學期Flutter來說應該會比較簡單,因爲Flutter中的Widget佈局方式完全類似於html和css。而Dart語言本身有非常像js,尤其是ES6特性引入很多語法糖後的js,這些語法糖大大簡化了代碼的複雜度。