概述
本文承接上文,是Flutter For iOS 的第二篇文章,通過閱讀本文你將獲取如下信息:
- 線程和異步
- 項(xiàng)目結(jié)構(gòu)與本地化
- 視圖控制器
- 布局
- 手勢(shì)
- 表單
- 列表
- 其他
線程和異步
如何寫異步代碼
Dart擁有單線程執(zhí)行模型,同時(shí)也支Isolate (一種將Dart代碼執(zhí)行在另一個(gè)線程的方式)、事件循環(huán)和異步編程。除非你創(chuàng)建一個(gè)Isolate ,你的Dart代碼將一直在主UI線程中執(zhí)行,并由事件循環(huán)驅(qū)動(dòng)。Flutter的事件循環(huán)相當(dāng)于iOS中的主循環(huán),也就是說Looper 綁定在主線程上。
Dart的單線程模型并不意味著你必須將一切代碼作為一個(gè)導(dǎo)致UI卡頓的阻塞塊來執(zhí)行。相反,你可以使用Dart提供的異步功能比如說:async/awiat 來執(zhí)行異步任務(wù)。
比如說,你可以使用asyn/await執(zhí)行網(wǎng)絡(luò)代碼和繁重的工作而避免UI卡頓。
一旦網(wǎng)絡(luò)請(qǐng)求結(jié)束,通過調(diào)用setState()更新UI,觸發(fā)當(dāng)前widget的子樹和更新數(shù)據(jù)。
下面例子異步加載數(shù)據(jù)并展示在ListViews上:
參考下一節(jié)了解如何在后臺(tái)線程執(zhí)行任務(wù),與iOS有何不同。
如何將任務(wù)放到后臺(tái)線程
由于Flutter的單線程模型和事件循環(huán),你不用擔(dān)心線程管理或者開啟后臺(tái)線程。你可以放心的使用async/await方法執(zhí)行I/O操作,比如訪問磁環(huán)或者請(qǐng)求網(wǎng)絡(luò)。另一方面,如何你想執(zhí)行復(fù)雜的計(jì)算而使CPU持續(xù)的處于繁忙狀態(tài),你可以將任務(wù)已到Isolate而避免阻塞事件循環(huán)。
對(duì)于iOS操作,將方法聲明為async方法,使用await等待耗時(shí)任務(wù)完成。
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
這是對(duì)常的I/O操作如網(wǎng)絡(luò)請(qǐng)求,訪問數(shù)據(jù)庫的常規(guī)操作。
但是,當(dāng)你處理大量數(shù)據(jù)的時(shí)候這仍然可能會(huì)導(dǎo)致UI掛起。在Flutter中,使用Isolate 來使用CPU多核的優(yōu)勢(shì)來執(zhí)行耗時(shí)任務(wù)或者計(jì)算密集型任務(wù)。
Isolates 是分離線程,它不和主線程共享任何堆內(nèi)存,這也就意味著,你不能訪問主線程中的變臉,或者直接調(diào)用setState()更新主線程。Isolates正如其名,不能共享內(nèi)存。
下面代碼展示了一個(gè)簡(jiǎn)單的isolate, 如何將數(shù)據(jù)返回到主線程并更新UI的。
上面代碼中,dataLoader()是Isolate,它在一個(gè)獨(dú)立的線程中執(zhí)行。在這個(gè)isolate中你可以執(zhí)行CPU密集型任務(wù)如解析JSON,或者執(zhí)行浮躁的數(shù)學(xué)計(jì)算任務(wù),如加密或者信號(hào)處理。
你可以執(zhí)行完整代碼,如下:
如何發(fā)生網(wǎng)絡(luò)請(qǐng)求
在Flutter中使用流行的第三方庫http package 來請(qǐng)求網(wǎng)絡(luò)是非常簡(jiǎn)單的。它抽象了大量的本需要你自己實(shí)現(xiàn)的操作,使得發(fā)送請(qǐng)求非常簡(jiǎn)單。
為了使用http這個(gè)框架,你需要在pubspec.yaml中增加依賴。
dependencies:
...
http: ^0.11.3+16
為了發(fā)起網(wǎng)絡(luò)請(qǐng)求,在async方法http.get() 前添加await。
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
[...]
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
}
如何展示耗時(shí)任務(wù)的進(jìn)度
在iOS中,當(dāng)在后臺(tái)執(zhí)行一個(gè)耗時(shí)任務(wù)的時(shí)候,你通過會(huì)使用UIProgressView展示進(jìn)度。
在Flutter中,使用ProgressIndicator 組件。通過給它傳遞一個(gè)布爾標(biāo)識(shí)來控制它的展示,告訴Flutter去更新它的狀態(tài)在耗時(shí)任務(wù)執(zhí)行之前和執(zhí)行結(jié)束之后隱藏掉它。
在下面的例子中,build方法被分割為三個(gè)不同方法。如果showLoadingDialog()是true,那就渲染ProgressIndicator 否則使用網(wǎng)絡(luò)返回的數(shù)據(jù)渲染ListView。
項(xiàng)目結(jié)構(gòu)、本地化、依賴和資源管理
如何在Flutter中管理圖片,如何放置多種分辨率的圖片
與iOS將圖片和資源作為不同的類型來處理不同的是Flutter中只有一種assets。iOS中資源被放在Image.xcassert中文件中,而Flutter中放在assets文件中。與iOS一樣,assets是許多類型的文件,不僅僅是圖片,比如說你可以將json文件放到my-assets文件夾中。
my-assets/data.json
在pubspec.yaml文件中聲明:
assets:
- my-assets/data.json
然后就可以在代碼中使用AssetBunlde訪問:
import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;
Future<String> loadAsset() async {
return await rootBundle.loadString('my-assets/data.json');
}
對(duì)于圖片,F(xiàn)lutter和iOS的格式一樣,圖片可以是1倍圖,2倍圖,3倍圖或者其他任何倍數(shù)。這些所謂的 devicePixelRatio 表示的是物理像素到單個(gè)邏輯像素的比率。
Assets可以被放到任何類型的文件夾中,F(xiàn)lutter中沒有事先預(yù)定義文件的結(jié)構(gòu)。在pubSpec.yaml文件中聲明assets,然后Flutter就能識(shí)別出來。
比如說:將my_icon.png放置到Flutter項(xiàng)目中,你可能把存儲(chǔ)的文件夾叫作images。把相關(guān)系數(shù)的圖片放在不同的子文件家中,如下:a
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
接下來在pubspec.yaml中聲明圖片
assets:
- images/my_icon.png
你現(xiàn)在就可以使用AssetImage返回圖片
return AssetImage("images/a_dot_burr.jpeg");
或者直接使用Image組件
@override
Widget build(BuildContext context) {
return Image.asset("images/my_image.png");
}
更多細(xì)節(jié)參考Adding Assets and Images in Flutter。
如何存放字符串,如何管理本地化
iOS中,我們使用Localizable.strings文件管理本地化字符串,而Flutter中沒有專門的模塊處理本地化字符串,所以最好的辦法就是將字符串統(tǒng)一放到一個(gè)類中,以靜態(tài)字段的形式存儲(chǔ)。如下:
class Strings {
static String welcomeMessage = "Welcome To Flutter";
}
訪問方式如下:
Text(Strings.welcomeMessage)
默認(rèn)情況下,F(xiàn)lutter只支持英文字符串,如果你想支持其他語言,可以通過引入flutter_localizations庫。 同時(shí)你需要將Dart的intl包以便支持 i10n 機(jī)制,比如日期/時(shí)間格式化。
dependencies:
# ...
flutter_localizations:
sdk: flutter
intl: "^0.15.6"
為了使用flutter_localizations ,需要在App widget上指定 localizationsDelegates 和supportedLocales 屬性。
import 'package:flutter_localizations/flutter_localizations.dart';
MaterialApp(
localizationsDelegates: [
// Add app-specific localization delegate[s] here
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', 'US'), // English
const Locale('he', 'IL'), // Hebrew
// ... other locales the app supports
],
// ...
)
代理中包含了實(shí)際的本地化值,supportedLocales定義了要支持那些語言的本地化。上面的例子使用的是MaterialApp, 它既有針對(duì)基本W(wǎng)idget的本地化值GlobalWidgetsLocalizations,也有針對(duì)Material widget的MaterialWidgetsLocalizations本地化。如果你的App使用的是WidgetApp,那么后者就不需要了。值得注意的是這兩個(gè)代理都包含默認(rèn)值,但如果你想讓你的App本地化,你扔需要提供一個(gè)或者多個(gè)代理作為你的App本地化副本。
當(dāng)初始化完成的時(shí)候,WidgetsApp或者MaterialApp使用你指定的代理為你創(chuàng)建了一個(gè)Localizationswidget。你可從LocalizationsWidget中隨時(shí)訪問當(dāng)前設(shè)備的本地化信息,或者使用window.locale。
為了訪問本地化資源,使用Localizations.of()方法訪問有給定的delegate提供的特有的本地化類。使用intl_translation取出翻譯副本到 arb 文件中。將它們引入App中,并用intl來使用它們。
更多國際化和本地化的內(nèi)容參考: internationalization guide,它包含了不使用intl示例代碼。
需要注意的是:Flutter1.0 beta2 之前 fullter中定義的資源文件不能被原生訪問,同時(shí)原生定義的資源不能被flutter訪問,因?yàn)樗鼈兇鎯?chǔ)在不能的文件目錄下。
如何管理依賴
在iOS中,我們將依賴添加到Podfile文件中,F(xiàn)lutter使用的是Dart語言構(gòu)建的系統(tǒng)和Pub包管理器操作依賴。這些工具將原生 Android 和 iOS 包裝應(yīng)用程序的構(gòu)建委派給相應(yīng)的構(gòu)建系統(tǒng)。
如果在你的Flutter項(xiàng)目中iOS目錄下包含Podfile,只需要使用它添加iOS原生的依賴。使用 pubspec.yaml 聲明Flutter 中的外部依賴。 Pub網(wǎng)站可以找到一些比較好用的第三方依賴。
視圖控制器
Flutter中與ViewControllers相等的元素是什么?
在iOS中,ViewController表示用戶界面的一部分,通常表示一個(gè)屏幕或者部分屏幕。多個(gè)ViewController組合在一起構(gòu)造復(fù)雜的用戶界面,并幫助你規(guī)整應(yīng)用的UI部分。在Flutter中,這項(xiàng)工作落在了Widget頭上,正如導(dǎo)航那一個(gè)章節(jié)提到的,屏幕由Widget所表示,因"一切都是Widget"。使用Navigator在不同的路由間切換表示不同的屏幕或者頁面或者表示不同的狀態(tài)或者渲染相同的數(shù)據(jù)。
如何監(jiān)聽iOS的生命周期事件
在iOS中,你可以重寫ViewController中的方法來捕獲視圖的生命周期,或者在AppDelegate中注冊(cè)生命周期的回調(diào)。在Flutter中沒有這兩個(gè)概念,但是我們可以通過hookWidgetsBinding并在didChangeAppLifecycleState()方法中監(jiān)聽生命周期事件。
能夠監(jiān)聽到的生命周期事件如下:
- Inactive — 應(yīng)用程序處于不活躍狀態(tài),不能相應(yīng)用戶輸入。該事件只在iOS中有效。
- paused — 應(yīng)用程序當(dāng)前不可用,不響應(yīng)用戶輸入,但是還在后臺(tái)運(yùn)行。
- resumed — 應(yīng)用程序可用,并能響應(yīng)用戶輸入。
- suspending — 應(yīng)用程序暫時(shí)被掛起。該事件只在Android系統(tǒng)上有效。
更多細(xì)節(jié)參考:AppLifecycleStatus documentation。
布局
Flutter中的UITableView 和 UICollectionView
Flutter中使用ListView實(shí)現(xiàn)iOS中的UITableView 和 UICollectionView。實(shí)現(xiàn)代碼如下:
如何知道那個(gè)cell被點(diǎn)擊
在iOS中,通過實(shí)現(xiàn) tableView:didSelectRowAtIndexPath:方法來相應(yīng)cell的點(diǎn)擊事件,在Flutter中,使用所包含的widget本身提供的事件來處理相應(yīng)。
如何動(dòng)態(tài)更新ListView
在iOS中,我們使用reloadData來刷新表格視圖。
在Flutter中,如果更新setState()中的小部件列表,你會(huì)發(fā)現(xiàn)列表數(shù)據(jù)沒有發(fā)生變化。這是因?yàn)楫?dāng)調(diào)用setState()時(shí),F(xiàn)lutter呈現(xiàn)引擎會(huì)查看widget樹以查看是否有任何更改。當(dāng)它到達(dá)ListView時(shí),它執(zhí)行==檢查,并確定兩個(gè)ListView是相同的。沒有任何改變,因此不需要更新。
在setState()方法內(nèi)創(chuàng)建一個(gè)新List是更新ListView的一個(gè)簡(jiǎn)單的方法。并將舊列表中的數(shù)據(jù)復(fù)制到新列表中。雖然這種方法很簡(jiǎn)單,但不建議用于大型數(shù)據(jù)集,如下一個(gè)示例所示。
我們推薦使用ListView.Builder來構(gòu)建列表,它比較高效。當(dāng)你的列表包含大量數(shù)據(jù)的列表時(shí),此方法非常有用。
與創(chuàng)建一個(gè)ListView不同的是,創(chuàng)建ListView.builder 攜帶兩個(gè)參數(shù):列表的初始長度和ItemBuilder方法。
ItemBuilder方法和iOS中的table或者collection的cellForItemAt代理相似,一樣的攜帶一個(gè)位置,并返回該位置需要渲染的cell。
最后也是最重要的,onTap方法并沒有重新創(chuàng)建一個(gè)list,而是.add了一個(gè)Widget。
如何使用類似ScrollView的功能
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
手勢(shì)檢測(cè)和觸摸事件處理
如何向widget添加一個(gè)事件監(jiān)聽
如果widget支持事件處理,如RaisedButton,可以直接將相應(yīng)方法傳遞給對(duì)應(yīng)的屬性,如RaisedButton的onPressed。
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () {
print("click");
},
child: Text("Button"),
);
}
如果widget不支持事件處理,可以使用GestureDetector包裹一下,然后給onTap屬性傳遞一個(gè)方法。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Sample App'),
),
body: Center(
child: GestureDetector(
child: FlutterLogo(
size: 200,
),
onTap: () {
print('taped');
},
),
));
}
如何處理widget上的其他類型的事件
我們可以使用 GestureDetector 來實(shí)現(xiàn)如下事件的監(jiān)聽:
- 單擊
-
onTapDown— 按下手勢(shì)事件 -
onTapUp— 抬起事件 -
onTap— 點(diǎn)擊事件 -
onTapCancel— 取消點(diǎn)擊事件,onTapDown發(fā)生,但onTap沒有發(fā)生。
-
- 雙擊
-
onDoubleTap— 雙擊事件
-
- 長按
-
onLongPress— 長按事件
-
- 垂直拖動(dòng)
-
onVerticalDragStart—開始垂直移動(dòng) -
onVerticalDragUpdate— 垂直移動(dòng)進(jìn)行中。 -
onVerticalDragEnd— 垂直移動(dòng)結(jié)束。
-
- 水平拖動(dòng)
-
onHorizontalDragStart— 開始水平移動(dòng)。 -
onHorizontalDragUpdate— 水平移動(dòng)進(jìn)行中。 -
onHorizontalDragEnd— 水平移動(dòng)結(jié)束。
-
下面代碼展示了使用 GestureDetector 實(shí)現(xiàn)雙擊事件:
運(yùn)行效果:
主題和文本
如何為應(yīng)用程序設(shè)置主題
Flutter提供了一套完美符合Material Design的主題,它幫你處理了大多數(shù)需要你自己處理的樣式和主題。
為了在你的App中充分發(fā)揮Material組件的優(yōu)勢(shì),在頂層組件上聲明MaterialApp,作為你的應(yīng)用的入口。MaterialApp 是一個(gè)便利的組件,它包含了許多App通常需要的Materail Desigin風(fēng)格的組件。它通過由給WidgetsApp增加MD功能實(shí)現(xiàn)的。
同時(shí) Flutter 足夠地靈活和富有表現(xiàn)力來實(shí)現(xiàn)任何其他的設(shè)計(jì)語言。在 iOS 上,你可以用 Cupertino library 來制作遵守 Human Interface Guidelines 的界面。查看這些 widget 的集合,請(qǐng)參閱 Cupertino widgets gallery。
你也可以在你的 App 中使用 WidgetApp,它提供了許多相似的功能,但不如 MaterialApp那樣豐富。
對(duì)任何子組件定義顏色和樣式,可以給 MaterialApp widget 傳遞一個(gè) ThemeData 對(duì)象。舉個(gè)例子,在下面的代碼中,primary swatch 被設(shè)置為藍(lán)色,并且文字的選中顏色是紅色:
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
textSelectionColor: Colors.red
),
home: SampleAppPage(),
);
}
}
如何在Text widget上使用自定義字體
在 iOS 中,你在項(xiàng)目中引入任意的 ttf 文件,并在 info.plist 中設(shè)置引用。在 Flutter 中,在文件夾中放置字體文件,并在 pubspec.yaml 中引用它,就像添加圖片那樣。
fonts:
- family: MyCustomFont
fonts:
- asset: fonts/MyCustomFont.ttf
- style: italic
然后在你的 Text widget 中指定字體:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: Center(
child: Text(
'This is a custom font text',
style: TextStyle(fontFamily: 'MyCustomFont'),
),
),
);
}
如何設(shè)置Text widget的樣式
除了字體以外,你也可以給 Text widget 的樣式元素設(shè)置自定義值。Text widget 接受一個(gè) TextStyle 對(duì)象,你可以指定許多參數(shù),如下:
colordecorationdecorationColordecorationStylefontFamilyfontSizefontStylefontWeighthashCodeheightinheritletterSpacingtextBaselinewordSpacing
表單輸入
表單在Flutter中如何工作的,如何取回用戶輸入的值
在iOS中,我們通常在用戶提交的時(shí)候獲取組件上的內(nèi)容,對(duì)于具有使用獨(dú)立狀態(tài)的不可變組件的Flutter來講,你可能會(huì)好奇如何獲取用戶輸入內(nèi)容。
對(duì)于表單操作而言,與其他功能一樣也是通過特定的Widget實(shí)現(xiàn)的。通過使用 TextField或者TextFormField 可以通過 TextEditingController 取回輸入內(nèi)容。
示例代碼如下:
運(yùn)行效果
更多信息參考: Flutter Cookbook 的 Retrieve the value of a text field
如何實(shí)現(xiàn)類似文本輸入框占位符的功能
通過給decoration屬性傳遞一個(gè)InputDecoration對(duì)象來給TextField實(shí)現(xiàn)占位符的功能。
body: Center(
child: TextField(
decoration: InputDecoration(hintText: "This is a hint"),
),
)
如何展示驗(yàn)收錯(cuò)誤信息
與上面代碼一樣,只不過是再添加一個(gè)errorText字段,通過state控制錯(cuò)誤信息的提示。
示例代碼如下:
運(yùn)行效果:
與硬件、第三方服務(wù)和平臺(tái)的交互
如何與平臺(tái)和平臺(tái)原生代碼交互
Flutter不是在直接在平臺(tái)下運(yùn)行代碼的,相反,由Dart語言構(gòu)建的FlutterApp在設(shè)備本機(jī)運(yùn)行,"回避"平臺(tái)提供的SDK。比如說:在Dart中發(fā)送一個(gè)網(wǎng)絡(luò)請(qǐng)求,它是直接在Dart上下文中執(zhí)行的,而不適用我們?cè)趯懺鶤pp的時(shí)候所使用的Android或者iOSAPI。我們的FlutterApp仍然被原生app的ViewController當(dāng)做一個(gè)View所持有,但我們不用直接訪問ViewController或者原生框架。
這并不意味著Flutter應(yīng)用不能與原生API或者其他你寫的原生代碼交互。Flutter提供了 platform channels,它可以與持有你Flutter視圖的VIewController通信或者交換數(shù)據(jù)。platform channels 本質(zhì)上是一個(gè)異步通信機(jī)制,橋接了Dart代碼和其宿主ViewController,iOS框架。比如說。你可以用platform channels執(zhí)行一個(gè)原生的函數(shù),或者是從設(shè)備的傳感器中獲取數(shù)據(jù)。
除了直接使用platform channels之外,你還可以使用一系列預(yù)先制作好的 plugins。例如,你可以直接使用插件來訪問相機(jī)膠卷或是設(shè)備的攝像頭,而不必編寫你自己的集成層代碼。你可以在 Pub 上找到插件,這是一個(gè) Dart 和 Flutter 的開源包倉庫。其中一些包可能會(huì)支持集成 iOS 或 Android,或兩者均可。
如果你在 Pub 上找不到符合你需求的插件,你可以自己編寫 ,并且發(fā)布在 Pub 上。
如何訪問GPS傳感器
使用 geolocator
如何訪問相機(jī)
使用 image_picker
如何使用FaceBook登陸
如何使用Firebase
大多數(shù) Firebase 特性被 first party plugins 包含了。這些第一方插件由 Flutter 團(tuán)隊(duì)維護(hù):
- firebase_admob : Firebase AdMob
- firebase_analytics : Firebase Analytics
- firebase_auth : Firebase Auth
- firebase_core : Firebase’s Core package
- firebase_database : Firebase RTDB
- firebase_storage : Firebase Cloud Storage
- firebase_messaging : Firebase Messaging (FCM)
-
cloud_firestore : Firebase Cloud Firestore
你也可以在 Pub 上找到 Firebase 的第三方插件。
如何創(chuàng)建原生集成層代碼
如果有一些 Flutter 和社區(qū)插件遺漏的平臺(tái)相關(guān)的特性,可以根據(jù) developing packages and plugins 頁面構(gòu)建自己的插件。
Flutter 的插件結(jié)構(gòu),簡(jiǎn)要來說,就像 Android 中的 Event bus。你發(fā)送一個(gè)消息,并讓接受者處理并反饋結(jié)果給你。在這種情況下,接受者就是在 Android 或 iOS 上的原生代碼。
數(shù)據(jù)庫和本地存儲(chǔ)
如何在Flutter中使用UserDefaults
在iOS中,我們可以使用UserDefaults 來存儲(chǔ)鍵值對(duì)集合,在Flutter中,可以使用 Shared Preferences plugin插件來顯示類似的功能。 這個(gè)插件包裝了UserDefaults和Android 上的 SharedPreferences。
Flutter中和Coredata相等的功能。
可以使用 SQFlite 插件實(shí)現(xiàn)iOS中CoreData相關(guān)的功能。
通知
如何設(shè)置推送通知
在iOS,你需要在開發(fā)者網(wǎng)站上注冊(cè)app以便獲取推送權(quán)限。在Flutter中使用firebase_messaging 插件可以實(shí)現(xiàn)推送。
更多關(guān)于使用Firebase Cloud Messaging API的文檔請(qǐng)參考: firebase_messaging
參考
本文主要參考Flutter官方文檔,F(xiàn)lutter中文網(wǎng)。
由于排版原因,文中我使用了圖片的形式展示代碼,如果你需要源碼,可以關(guān)注我的公眾號(hào),回復(fù)關(guān)鍵字"flutter"獲取相關(guān)代碼。