給 iOS 開發者的 Flutter 指南
這篇文章是為那些想將已有的 iOS 開發經驗運用到 Flutter 開發中的 iOS 開發者所作。 如果你理解 iOS framework 的基本原理,那麼你可以將這篇文章作為學習 Flutter 開發的起點。
本文結構如下:
1. 視圖2. 導航3. 線程和非同步4. 工程結構、本地化、依賴和資源
5. ViewControllers6. 布局7. 手勢檢測與 touch 事件處理8. 主題和文字9. 表單輸入10. 和硬體、第三方服務以及系統平台交互11. 資料庫和本地存儲12. 通知一、Views
1.1 UIView 相當於 Flutter 中的什麼?
在 iOS 中,你在 UI 中創建的大部分視圖都是 UIView
的實例。而在構造布局時,這些視圖也可以作為其他視圖的容器。
在 Flutter 中,Widget
可以類比為 UIView
,你可以把它理解為「聲明和構造 UI 的方法」,但它們又並非完全相同:
首先,widget 擁有著不同的生命周期: 整個生命周期內它是不可變的,且只能夠存活到被修改的時候。一旦 widget 實例或者它的狀態發生了改變, Flutter 框架就會創建一個新的由 Widget
實例構造而成的樹狀結構。而在 iOS 里,修改一個視圖並不會導致它重新創建實例,它作為一個可變對象,只會繪製一次,只有調用 setNeedsDisplay()
之後才會發生重繪。
其次,Flutter 的 widget 是很輕量的,一部分原因就是由於它的不可變特性。因為它並不是視圖,也不直接繪製任何內容,而是作為對 UI 及其特性的一種描述,而被「注入」到視圖中去。
Flutter 包含了 Material Components 庫。內容都是 一些遵循了 Material Design 設計規範 的組件。Material Design 是 一種靈活的支持全平台 的設計體系,其中也包括了 iOS。
但是 Flutter 的靈活性和表現力使其能夠適配任何的設計語言。在 iOS 中,你可以通過 Cupertino widgets 來構造類似於Apple iOS 設計語言的介面。
1.2 我該如何更新 Widgets?
在 iOS 可以直接對視圖進行修改。但是在 Flutter 中,widget 都是不可變的,所以也不能夠直接對其修改。所以,你必須通過修改 widget 的 state 來達到更新視圖的目的。
於是,就引入了 Stateful widget 和 Stateless widget 的概念。和字面意思相同,StatelessWidget
就是 一個沒有綁定狀態的 widget。
當某個 widget 不需要依賴任何別的初始配置來對這個 widget 進行描述時,StatelessWidget
會是很有用的。
舉個例子,在 iOS 中,你需要把 logo 當作 image
並將它放置在 UIImageView
中, 如果在運行時這個 logo 不會發生變化,那麼對應 Flutter 中你應該使用 StatelessWidget
。
但是如果你想要根據 HTTP 請求的返回結果動態的修改 UI,那麼你應該使用 StatefulWidget
。在 HTTP 請求結束 後,通知 Flutter 更新這個 widget 的 State
,然後 UI 就會得到更新。
StatefulWidget
和 StatelessWidget
最重要的區別就是,StatefulWidget
中有一個 State
對象,它用來存儲一些狀態的信息,並在整個生命周期內保持不變。
如果你對此還存有疑慮,記住一點:如果一個 widget 在 build
方法之外(比如運行時下發生用戶點擊事件)被修改,那麼就應該是有狀態的。如果一個 widget 一旦生成就不再發生改變,那麼它就是無狀態的。然而,即使一個 widget 是有狀態的,如果不是自身直接響應修改(或別的輸入),那麼他的父容器也可以是無狀態的。
下面是如何使用 StatelessWidget
的示例。Text
是一個常用的 StatelessWidget
。如果你看了 Text
的源代碼,就會發現它繼承於 StatelessWidget
。
Text(
I like Flutter!,
style: TextStyle(fontWeight: FontWeight.bold),
);
如上述代碼所示, Text
沒有攜帶任何狀態。它只會渲染初始化時傳入內容。
如果你希望在點擊 FloatingActionButton
時 I like Flutter
能產生動態的改變,只需要把 Text
放到 StatefulWidget
中,並在用戶點擊按鈕時更新它即可。
下面是示例代碼:
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@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> {
// Default placeholder text
String textToShow = "I Like Flutter";
void _updateText() {
setState(() {
// update the text
textToShow = "Flutter is Awesome!";
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: Center(child: Text(textToShow)),
floatingActionButton: FloatingActionButton(
onPressed: _updateText,
tooltip: Update Text,
child: Icon(Icons.update),
),
);
}
}
1.3 如何對 widget 布局? Storyboard 在哪?
在 iOS 開發中,你可能會經常使用 Storyboard 來組織你的視圖,並直接通過 Storyboard 或者 在 ViewController 中通過代碼來設置約束。而在 Flutter 中,你要通過代碼來對 widget 進行 組織來形成一個 widget 樹狀結構。
下面的例子展示了如何展示一個帶有 padding 的 widget:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: Center(
child: CupertinoButton(
onPressed: () {
setState(() { _pressedCount += 1; });
},
child: Text(Hello),
padding: EdgeInsets.only(left: 10.0, right: 10.0),
),
),
);
}
你可以為任何 widget 添加 padding,來達到類似在 iOS 中視圖約束的作用。
你可以在widget 目錄中查看 Flutter 提供 的所有 widget 布局方法。
1.4 如何添加或移除一個組件?
在 iOS 中,你可以通過調用父視圖的 addSubview()
方法或者 removeFromSuperview()
方法 來動態的添加或移除視圖。
在 Flutter 中,因為 widget 是不可變的,所以沒有提供直接同 addSubview()
作用相同的方法。但是你可以通過向父視圖傳遞一個返回值是 widget 的方法,並通過一個 boolean flag 來控制子視圖的存在。
下面的例子中像你展示了如何讓用戶通過點擊 FloatingActionButton
按鈕來達到在兩個 widget 中切換的目的:
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@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> {
// Default value for toggle
bool toggle = true;
void _toggle() {
setState(() {
toggle = !toggle;
});
}
_getToggleChild() {
if (toggle) {
return Text(Toggle One);
} else {
return CupertinoButton(
onPressed: () {},
child: Text(Toggle 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),
),
);
}
}
1.5 如何添加動畫?
在 iOS 里,你可以使用調用視圖的 animate(withDuration:animations:)
方法來創建動畫。
在 Flutter 里,通過使用動畫庫將 widget 封裝到 animated widget 中來實現帶動畫效果。AnimationController
是一個可以暫停、尋找、停止、反轉動畫的 Animation<double>
類型。它需要一個 Ticker
,在屏幕刷新時發出信號量,並在運行時對每一幀都產生一個 0~1 的線性差值。你可以創建一個或多個 Animation
,並把它們添加到控制器中。
比如,你可以使用 CurvedAnimation
來實現一個曲線翻頁動畫。這種情況下,控制器就是動畫進度的主要數據源, 而 CurvedAnimation
計算曲線並替換控制器的默認線性運動。和 widget 一樣,在 Flutter 里動畫也可以複合嵌套。
當構建一個 widget 樹時,可以將 Animation
賦值給 widget 用戶表現動畫能力的屬性, 比如 FadeTransition
的 opacity 屬性,然後告訴控制器啟動動畫。
下面的示例描述了當你點擊 FloatingActionButton
時,如何實現一個視圖漸淡出成 logo 的 FadeTransition
效果:
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: Fade Demo,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyFadeTest(title: Fade Demo),
);
}
}
class MyFadeTest extends StatefulWidget {
MyFadeTest({Key key, this.title}) : super(key: key);
final String title;
@override
_MyFadeTest createState() => _MyFadeTest();
}
class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {
AnimationController controller;
CurvedAnimation curve;
@override
void initState() {
controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Container(
child: FadeTransition(
opacity: curve,
child: FlutterLogo(
size: 100.0,
)
)
)
),
floatingActionButton: FloatingActionButton(
tooltip: Fade,
child: Icon(Icons.brush),
onPressed: () {
controller.forward();
},
),
);
}
@override
dispose() {
controller.dispose();
super.dispose();
}
}
關於更多的內容,可以查看Animation 和 Motion widgets,Animations 教程, 以及Animations 概覽。
1.6 如何渲染到屏幕上?
在 iOS 里,可以使用 CoreGraphics
繪製線條和圖形到屏幕上。Flutter 里有一套基於 Canvas
實現的 API,有兩個類可以幫助你進行繪製:CustomPaint
和 CustomPainter
,後者實現了繪製圖形到 canvas 的演算法。
想要學習在 Flutter 里如何實現一個畫筆,可以查看 Collin 在 StackOverflow 里的回答。
class SignaturePainter extends CustomPainter {
SignaturePainter(this.points);
final List<Offset> points;
void paint(Canvas canvas, Size size) {
var paint = Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5.0;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null)
canvas.drawLine(points[i], points[i + 1], paint);
}
}
bool shouldRepaint(SignaturePainter other) => other.points != points;
}
class Signature extends StatefulWidget {
SignatureState createState() => SignatureState();
}
class SignatureState extends State<Signature> {
List<Offset> _points = <Offset>[];
Widget build(BuildContext context) {
return GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
setState(() {
RenderBox referenceBox = context.findRenderObject();
Offset localPosition =
referenceBox.globalToLocal(details.globalPosition);
_points = List.from(_points)..add(localPosition);
});
},
onPanEnd: (DragEndDetails details) => _points.add(null),
child: CustomPaint(painter: SignaturePainter(_points), size: Size.infinite),
);
}
}
1.7 如何設置視圖 Widget 的透明度?
在 iOS 里,視圖都有一個 opacity 或者 alpha 屬性。而在 Flutter 里,大部分時候你都需要封裝 widget 到一個 Opacity widget 中來實現這一功能。
1.8 如何構建自定義 widgets?
在 iOS 里,你可以直接繼承 UIView
或者使用已經存在的視圖,然後重寫並實現對應的方法來達到想要的效果。在 Flutter 里,構建自定義 widget 需要通過合成一些小的 widget(而不是對它們進行擴展)來實現。
例如,如果你要構建一個 CustomButton
,並在構造器中傳入它的文本標籤?那就組合 RaisedButton
和文本標籤,而不是繼承 RaisedButton
:
class CustomButton extends StatelessWidget {
final String label;
CustomButton(this.label);
@override
Widget build(BuildContext context) {
return RaisedButton(onPressed: () {}, child: Text(label));
}
}
像你使用其他 Flutter 的 widget 一樣,下面我們使用 CustomButton
:
@override
Widget build(BuildContext context) {
return Center(
child: CustomButton("Hello"),
);
}
二、導航
2.1 如何在不同頁面之間切換?
在 iOS 里,想要在多個 viewcontroller 中切換,可以使用 UINavigationController
管理 viewcontroller 構成的棧進行顯示。
在 Flutter 中,使用 Navigator
和 Routes
也可以實現類似的功能。一個 Routes
是應用中屏幕或者頁面的抽象概念,而一個 Navigator
是管多個 Route
的 widget。
可以把 Route
理解為 UIViewController
。而 Navigator
的工作方式和 iOS 的 UINavigationController
類似,當你想要進入或退出一個新頁面的時候,它也可以進行 push()
和 pop()
操作。
想要在不同頁面間跳轉,你有兩個選擇:
1.構建由 route 名稱組成的 Map
(MaterialApp)
2.直接跳轉到一個 route(WidgetApp)
下面的示例構建了一個 Map
:
void main() {
runApp(CupertinoApp(
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),
},
));
}
通過把 route 的名稱 push
給一個 Navigator
來跳轉:
Navigator.of(context).pushNamed(/b);
Navigator
類不僅用來處理 Flutter 中的路由,還被用來獲取你剛 push 到棧中的路由返回的結果。通過 await
等待路由返回的結果來達到這點。
舉個例子,要跳轉到「位置」路由來讓用戶選擇一個地點,你可能要這麼做:
Navigator
類對 Flutter 中的路由事件做處理,還可以用來獲取入棧之後的路由的結果。這需要通過 push()
返回的 Future
中的await 來實現。
例如,要打開一個「定位」頁面來讓用戶選擇他們的位置,你需要做如下事情:
Map coordinates = await Navigator.of(context).pushNamed(/location);
然後,在」定位「頁面中,一旦用戶選擇了自己的定位,就 pop()
出棧並返回結果。
Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});
2.2 如何跳轉到其他應用?
在 iOS 里,想要跳轉到其他應用,可以使用特定的 URL scheme。對於系統級別的應用,scheme 都是 取決於應用的。在 Flutter 里想要實現這個功能,需要創建原生平台的整合層,或者使用已經存在的插件,例如 url_launcher。
2.3 如何退回到 iOS 原生的 viewcontroller?
在 Dart 代碼中調用 SystemNavigator.pop() 將會調用下面的 iOS 代碼:
UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
if ([viewController isKindOfClass:[UINavigationController class]]) {
[((UINavigationController*)viewController) popViewControllerAnimated:NO];
}
三、線程和非同步
3.1 如何編寫非同步代碼?
Dart 是單線程執行模型,支持 Isolate
(一種在其他線程運行 Dart 代碼的方法)、事件循環和非同步編程。 除非生成了 Isolate
,否則所有 Dart 代碼將永遠在主 UI 線程運行,並由事件循環驅動。Flutter 中的事件循環類似於 iOS 中的 main loop—,也就是主線程上的 Looper
。
Dart 的單線程模型並不意味著你需要以阻塞 UI 的形式來執行代碼,相反,你更應該使用 Dart 語言提供的非同步功能, 比如使用 async / await
來實現非同步操作。
例如,你可以使用 async / await
來執行網路代碼以避免 UI 掛起,讓 Dart 來完成這個繁重的任務:
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
一旦 await
等待的網路操作結束,通過調用 setState()
來更新 UI,這將會觸發 widget 子樹的重新構建並更新數據。
下面的示例展示了如何非同步載入數據,並在 ListView
中展示出來:
import dart:convert;
import package:flutter/material.dart;
import package:http/http.dart as http;
void main() {
runApp(SampleApp());
}
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> {
List widgets = [];
@override
void initState() {
super.initState();
loadData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
}));
}
Widget getRow(int i) {
return Padding(
padding: EdgeInsets.all(10.0),
child: Text("Row ${widgets[i]["title"]}")
);
}
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
}
更多關於在後台工作的信息,以及 Flutter 和 iOS 的區別,請參考下一章節。
3.2 如何讓你的工作在後台線程執行?
由於 Flutter 是單線程模型,而且執行著一個 event loop(就像 Node.js),你不需要為線程管理或 是開啟後台線程操心。如果你在處理 I/O 操作,例如磁碟訪問或網路請求,那麼你安全地使用 async / await
就可以了。但是,如果你需要大量的計算來讓 CPU 保持忙碌狀態,你需要使用 Isolate
來防治阻塞 event loop。
對於 I/O 操作,把方法聲明為 async
方法,然後通過 await
來等待非同步方法的執行完成:
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
這就是處理網路或資料庫請求等 I/O 操作的經典做法。
然而,有時候你需要處理大量的數據,從而導致 UI 掛起。在 Flutter 里,當處理長期運行或者運算密集的任務時,可以使用 Isolate
來發揮出多核 CPU 的優勢。
Isolates 是相互隔離的執行線程,並不和主線程共享內存。這意味著你不能夠訪問主線程的變數,也不能 使用 setState()
來更新 UI。Isolates 正如起字面意思是不能共享內存(例如靜態變數表)的。
下面的例子展示了在一個簡單的 isolate 中,如何把數據推到主線程上用來更新 UI:
loadData() async {
ReceivePort receivePort = ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
// The echo isolate sends its SendPort as the first message
SendPort sendPort = await receivePort.first;
List msg = await sendReceive(sendPort, "https://jsonplaceholder.typicode.com/posts");
setState(() {
widgets = msg;
});
}
// The entry point for the isolate
static dataLoader(SendPort sendPort) async {
// Open the ReceivePort for incoming messages.
ReceivePort port = ReceivePort();
// Notify any other isolates what port this isolate listens to.
sendPort.send(port.sendPort);
await for (var msg in port) {
String data = msg[0];
SendPort replyTo = msg[1];
String dataURL = data;
http.Response response = await http.get(dataURL);
// Lots of JSON to parse
replyTo.send(json.decode(response.body));
}
}
Future sendReceive(SendPort port, msg) {
ReceivePort response = ReceivePort();
port.send([msg, response.sendPort]);
return response.first;
}
在這裡,dataLoader()
就是運行在獨立線程上的 Isolate
。在 Isolate
中,你可以處理 CPU 密集型任務(如解析一個 龐大的 JSON 文件),或者處理複雜的數學運算,比如加密操作或者信號處理等。
下面是一個完整示例:
import dart:convert;
import package:flutter/material.dart;
import package:http/http.dart as http;
import dart:async;
import dart:isolate;
void main() {
runApp(SampleApp());
}
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> {
List widgets = [];
@override
void initState() {
super.initState();
loadData();
}
showLoadingDialog() {
if (widgets.length == 0) {
return true;
}
return false;
}
getBody() {
if (showLoadingDialog()) {
return getProgressDialog();
} else {
return getListView();
}
}
getProgressDialog() {
return Center(child: CircularProgressIndicator());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: getBody());
}
ListView getListView() => ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
});
Widget getRow(int i) {
return Padding(padding: EdgeInsets.all(10.0), child: Text("Row ${widgets[i]["title"]}"));
}
loadData() async {
ReceivePort receivePort = ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
// The echo isolate sends its SendPort as the first message
SendPort sendPort = await receivePort.first;
List msg = await sendReceive(sendPort, "https://jsonplaceholder.typicode.com/posts");
setState(() {
widgets = msg;
});
}
// the entry point for the isolate
static dataLoader(SendPort sendPort) async {
// Open the ReceivePort for incoming messages.
ReceivePort port = ReceivePort();
// Notify any other isolates what port this isolate listens to.
sendPort.send(port.sendPort);
await for (var msg in port) {
String data = msg[0];
SendPort replyTo = msg[1];
String dataURL = data;
http.Response response = await http.get(dataURL);
// Lots of JSON to parse
replyTo.send(json.decode(response.body));
}
}
Future sendReceive(SendPort port, msg) {
ReceivePort response = ReceivePort();
port.send([msg, response.sendPort]);
return response.first;
}
}
3.3 如何發起網路請求?
在 Flutter 里,想要構造網路請求十分簡單,直接使用 http 庫即可。它把你可能要實現的網路操作進行了抽象封裝,讓處理網路請求變得十分簡單。
要使用 http 庫,需要在 pubspec.yaml
中把它添加為依賴:
dependencies:
...
http: ^0.11.3+16
構造網路請求,需要在 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);
});
}
}
3.4 如何展示耗時任務的進度?
在 iOS 中,在後台運行耗時任務時,會使用 UIProgressView
。
在 Flutter 中,應該使用 ProgressIndicator
。它在渲染時通過一個 boolean flag 來控制是否顯示進度。在耗時任務開始前,告訴 Flutter 去更新狀態,並在任務結束後隱藏。
在下面的例子中,build 函數被分為三個不同的函數。
當 showLoadingDialog()
是 true
(當 widgets.length == 0
),則渲染 ProgressIndicator
。否則,當數據從網路請求中返回時,渲染 ListView
。
import dart:convert;
import package:flutter/material.dart;
import package:http/http.dart as http;
void main() {
runApp(SampleApp());
}
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> {
List widgets = [];
@override
void initState() {
super.initState();
loadData();
}
showLoadingDialog() {
return widgets.length == 0;
}
getBody() {
if (showLoadingDialog()) {
return getProgressDialog();
} else {
return getListView();
}
}
getProgressDialog() {
return Center(child: CircularProgressIndicator());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: getBody());
}
ListView getListView() => ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
});
Widget getRow(int i) {
return Padding(padding: EdgeInsets.all(10.0), child: Text("Row ${widgets[i]["title"]}"));
}
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
}
四、工程結構、本地化、依賴和資源
4.1 如何在 Flutter 中引入 圖片資源?如何處理多解析度?
在 iOS中,圖片和其他資源會被視為不同的資源分別處理,而在 Flutter 中只有資源這一個概念。 iOS 里被放置在 Images.xcasset
文件夾的資源在 Flutter 中都被放置到了 assets 文件夾中。 和 iOS 一樣,assets 中可以放置任意類型的文件,而不僅僅是圖片。 例如,你可以把一個 JSON 文件放置到 my-assets
文件夾中。
my-assets/data.json
在 pubspec.yaml
中聲明 assets:
assets:
- my-assets/data.json
在代碼中通過使用 AssetBundle
訪問資源:
import dart:async show Future;
import package:flutter/services.dart show rootBundle;
Future<String> loadAsset() async {
return await rootBundle.loadString(my-assets/data.json);
}
對於圖片,Flutter 和 iOS 一樣遵循了一個簡單的基於屏幕密度的格式。
Image assets 可能是 1.0x 2.0x 3.0x
或者其他任意的倍數。而 devicePixelRatio
則 表達了物理解析度到邏輯解析度的對照比例。
Assets 可以放在任何屬性的文件夾中—Flutter 沒有任何預置的文件結構。你需要在 pubspec.yaml
中 聲明 assets (包括路徑),然後 Flutter 將會識別它們。
例如,要添加一個名為 my_icon.png
的圖片到你的 Flutter 工程中,你可以把它存儲在 images
文件夾下。 把基礎的圖片(一倍圖)放到 images
文件夾下,然後把其他倍數的圖片放置到對應的比例下的子文件夾中:
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.jpeg
你可以用 AssetImage
來訪問這些圖片:
return AssetImage("images/a_dot_burr.jpeg");
或者在 Image
widget 中直接使用:
@override
Widget build(BuildContext context) {
return Image.asset("images/my_image.png");
}
關於更多的細節,請參見 在 Flutter 中添加資源和圖片。
4.2 字元串存儲在哪裡?如何處理本地化?
iOS 里有 Localizable.strings
文件,而 Flutter 則不同,目前並沒有關於字元串的處理系統。 目前,最佳的方案就是在靜態區聲明你的文本,然後進行訪問。例如:
class Strings {
static String welcomeMessage = "Welcome To Flutter";
}
並且這樣訪問你的字元串:
Text(Strings.welcomeMessage)
默認情況下,Flutter 只支持美式英語的本地化字元串。如果你需要添加其他語言支持,請引入 flutter_localizations
庫。 同時你可能還需要添加 intl
庫來使用 i10n 機制,比如 日期/時間的格式化等。
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
],
// ...
)
supportedLocales
指定了應用支持的語言,而這些 delegates 則包含了實際的本地化內容。上面的示例 使用了一個 MaterialApp
,所以它既使用了處理基礎 widget 本地化的 GlobalWidgetsLocalizations
, 也使用了處理 Material widget 本地化的 MaterialWidgetsLocalizations
。如果你在應用中使用的是 WidgetsApp
,就不需要後者了。注意,這兩個 delegates 雖然都包含了「默認」值,但是如果你想要實現本地化,就必須在本地提供一個或多個 delegates 的實現副本。
當初始化的時候,WidgetsApp
(或 MaterialApp
)會根據你提供的 delegates 創建一個 Localizations
widget。 Localizations widget 可以隨時從當前上下文中獲取設備所用的語言,也可以使用 Window.locale。
要使用本地化資源,使用 Localizations.of()
方法可以訪問提供代理的特定本地化類。使用 intl_translation 庫解壓翻譯的副本到 arb 文件,然後在應用中通過 intl
來引用它們。
關於 Flutter 中國際化和本地化的細節內容,請參看 internationalization guide,裡面包含有使用和不使用 intl
庫的示例代碼。
注意在 Flutter 1.0 beta 2 之前,在 Flutter 里定義的資源是不能被原生代碼訪問的,反之亦然,而原生的資源也是不能在 Flutter 中使用,因為它們都被放在了獨立的文件夾中。
4.3 Cocoapods 相當於 Flutter 中的什麼?該如何添加依賴?
在 iOS 里,可以通過 Podfile
添加依賴。而 Flutter 使用 Dart 構建系統和 Pub 包管理器來處理依賴。這些工具將原生應用的打包任務分發給相應 Android 或 iOS 構建系統。
如果你的 Flutter 項目 iOS 文件夾中存在 Podfile,那麼請僅在裡面添加原生平台的依賴。總而言之, 在 Flutter 中使用 pubspec.yaml
來聲明外部依賴。你可以通過 Pub 來查找一些優秀的 Flutter 第三方包。
五、ViewControllers
5.1 ViewController 相當於 Flutter 中的什麼?
在 iOS 里,一個 ViewController 是用戶界面的一部分,通常是作為屏幕或者其中的一部分來使用。 這些組合在一起構成了複雜的用戶界面,並以此對應用的 UI 做不斷的擴充。 在 Flutter 中,這一任務又落到了 Widget 這裡。就像在導航那一章提到的, Flutter 中的屏幕也是使用 Widgets 表示的,因為「萬物皆 widget!」。使用 Naivgator
在不同的 Route
之間切換,而不同的路由則代表了不同的屏幕或頁面,或是不同的狀態,也可能是渲染相同的數據。
5.2 如何監聽 iOS 中的生命周期?
在 iOS 里,可以重寫 ViewController
的方法來捕獲自身的生命周期,或者在 AppDelegate
中註冊生命 周期的回調。Flutter 中則沒有這兩個概念,但是你可以通過在 WidgetsBinding
的 observer 中掛鉤子,也可以 通過監聽didChangeAppLifecycleState()
事件,來實現相應的功能。
可監聽的生命周期事件有:
inactive
- 應用當前處於不活躍狀態,不接收用戶輸入事件。這個事件只在 iOS 上有效,Android 中沒有類似的狀態。paused
- 應用處於用戶不可見狀態,不接收用戶輸入事件,但仍在後台運行。resumed
- 應用可見,也響應用戶輸入。suspending
- 應用被掛起,在 iOS 平台沒有這一事件。
更多細節,請參見 AppLifecycleStatus文檔。
六、布局
6.1 UITableView 和 UICollectionView 相當於 Flutter 中的什麼?
在 iOS 里,你可能使用 UITableView
或者 UICollectionView
來展示一個列表。而在 Flutter 里,你可以使用 ListView 來達到類似的實現。在 iOS 中,你通過 delegate 方法來確定顯示的行數,相應位置的 cell,以及 cell 的尺寸。
由於 Flutter 中 widget 的不可變特性,你需要向 ListView
傳遞一個 widget 列表,Flutter 會確保滾動快速而流暢。
import package:flutter/material.dart;
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@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> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: ListView(children: _getListData()),
);
}
_getListData() {
List<Widget> widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(Padding(padding: EdgeInsets.all(10.0), child: Text("Row $i")));
}
return widgets;
}
}
6.2 如何確定列表中被點擊的元素?
在 iOS 中,tableView:didSelectRowAtIndexPath:
代理方法可以用來實現該功能。而在 Flutter 中,需要通過 widget 傳遞進來的 touch 響應處理來實現。
import package:flutter/material.dart;
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@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> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: ListView(children: _getListData()),
);
}
_getListData() {
List<Widget> widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(GestureDetector(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text("Row $i"),
),
onTap: () {
print(row tapped);
},
));
}
return widgets;
}
}
6.3 如何動態更新?
在 iOS 中,可以更新列表數據,調用 reloadData
方法通知 tableView 或 collectionView。
在 Flutter 里,如果你在 setState()
中更新了 widget 列表,你會發現展示的數據並不會立刻更新。這是因為當 setState()
被調用時,Flutter 的渲染引擎回去檢索 widget 樹是否有改變。當它獲取到 ListView
,會進行 ==
判斷,然後發現兩個 ListView 是相等的。沒發現有改變,所以也就不會進行更新。
更新 ListView
簡單的方法是在 setState()
創建一個新的 List,然後拷貝舊列表中的所有數據到新列表。這樣雖然簡單,但是像下面示例一樣數據量很大時,並不推薦這樣做。
import package:flutter/material.dart;
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@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> {
List widgets = [];
@override
void initState() {
super.initState();
for (int i = 0; i < 100; i++) {
widgets.add(getRow(i));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: ListView(children: widgets),
);
}
Widget getRow(int i) {
return GestureDetector(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text("Row $i"),
),
onTap: () {
setState(() {
widgets = List.from(widgets);
widgets.add(getRow(widgets.length + 1));
print(row $i);
});
},
);
}
}
一個高效且有效的方法是使用 ListView.Builder
來構建列表。當你的數據量很大,且需要構建動態列表時,這個方法會非常好用。
import package:flutter/material.dart;
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@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> {
List widgets = [];
@override
void initState() {
super.initState();
for (int i = 0; i < 100; i++) {
widgets.add(getRow(i));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
},
),
);
}
Widget getRow(int i) {
return GestureDetector(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text("Row $i"),
),
onTap: () {
setState(() {
widgets.add(getRow(widgets.length + 1));
print(row $i);
});
},
);
}
}
與創建 ListView
不同,創建 ListView.Builder
需要兩個關鍵參數:初始化列表長度和 ItemBuilder
函數。
ItemBuilder
方法和 cellForItemAt
代理方法非常類似,它接收位置參數,然後返回想要在該位置渲染的 cell。
最後,也是最重要的,注意 onTap()
方法並沒有重新創建列表,而是使用 .add
方法進行添加。
6.4 ScrollView 相當於 Flutter 里的什麼?
在 iOS 中,把視圖放在 ScrollView
里來允許用戶在需要時滾動內容。
在 Flutter 中,使用 ListView
widget 是最簡單的辦法。它和 iOS 中 ScrollView
及 TableView
表現一致,也可以給它的 widget 做垂直排版。
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
Text(Row One),
Text(Row Two),
Text(Row Three),
Text(Row Four),
],
);
}
關於 Flutter 中排布的更多細節,請參閱 布局教程。
七、手勢檢測與 touch 事件處理
7.1 如何給 Flutter 的 widget 添加點擊事件?
在 iOS 中,通過把 GestureRecognizer
綁定給 UIView 來處理點擊事件。在 Flutter 中, 有兩種方法來添加事件監聽者:
1. 如果 widget 本身支持事件檢測,則直接傳遞處理函數給它。例如,RaisedButton
擁有 一個 onPressed
參數:
@override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () {
print("click");
},
child: Text("Button"),
);
}
2. 如果 widget 本身不支持事件檢測,那麼把它封裝到一個 GestureDetector 中,並給它的 onTap 參數傳遞一個函數:
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
child: FlutterLogo(
size: 200.0,
),
onTap: () {
print("tap");
},
),
),
);
}
}
7.2 我怎麼處理 widget 上的其他手勢?
你可以使用 GestureDetector
來監聽更多的手勢,例如:
- 單擊事件
onTapDown
—— 用戶在特定區域發生點觸屏幕的一個即時操作。onTapUp
—— 用戶在特定區域發生觸摸抬起的一個即時操作。onTap
—— 從點觸屏幕之後到觸摸抬起之間的單擊操作。onTapCancel
—— 用戶在之前觸發了onTapDown
時間,但未觸發 tap 事件。
- 雙擊事件
onDoubleTap
—— 用戶在同一位置發生快速點擊屏幕兩次的操作。
- 長按事件
onLongPress
—— 用戶在同一位置長時間觸摸屏幕的操作。
- 垂直拖動事件
onVerticalDragStart
—— 用戶手指接觸屏幕,並且將要進行垂直移動事件。onVerticalDragUpdate
—— 用戶手指接觸屏幕,已經開始垂直移動,且會持續進行移動。onVerticalDragEnd
—— 用戶之前手指接觸了屏幕並發生了垂直移動操作,並且停止接觸前還在以一定的速率移動。
- 水平拖動事件
onHorizontalDragStart
—— 用戶手指接觸屏幕,並且將要進行水平移動事件。onHorizontalDragUpdate
—— 用戶手指接觸屏幕,已經開始水平移動,且會持續進行移動。onHorizontalDragEnd
—— 用戶之前手指接觸了屏幕並發生了水平移動操作,並且停止接觸前還在以一定的速率移動。
下面的示例展示了 GestureDetector
是如何實現雙擊時旋轉 Flutter 的 logo 的:
AnimationController controller;
CurvedAnimation curve;
@override
void initState() {
controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
}
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
child: RotationTransition(
turns: curve,
child: FlutterLogo(
size: 200.0,
)),
onDoubleTap: () {
if (controller.isCompleted) {
controller.reverse();
} else {
controller.forward();
}
},
),
),
);
}
}
八、主題和文字
8.1 如何設置應用主題?
Flutter 實現了一套漂亮的 Material Design 組件,而且開箱可用,它提供了許多常用的樣式和主題。
為了充分發揮應用中 Material Components 的優勢,聲明一個頂級的 widget,MaterialApp,來作為你的應用 入口。MaterialApp 是一個封裝了大量常用 Material Design 組件的 widget。它基於 WidgetsApp 添加了 Material 的 相關功能。
但是 Flutter 有足夠的靈活性和表現力來實現任何設計語言。在 iOS 上,可以使 用Cupertino library來 製作遵循Human Interface Guidelines的 界面。關於這些 widget 的全部集合,可以參看Cupertino widgets gallery。
也可以使用 WidgetApp
來做為應用入口,它提供了一部分類似的功能介面,但是不如 MaterialApp
強大。
定義所有子組件顏色和樣式,可以直接傳遞 ThemeData
對象給 MaterialApp widget
。例如,在下面的代碼中,primary swatch 被設置為藍色,而文本選中後的顏色被設置為紅色:
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: Sample App,
theme: ThemeData(
primarySwatch: Colors.blue,
textSelectionColor: Colors.red
),
home: SampleAppPage(),
);
}
}
8.2 如何給 Text widget 設置自定義字體?
在 iOS 里,可以在項目中引入任何的 ttf
字體文件,並在 info.plist
文件中聲明並進行引用。在 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),
),
),
);
}
8.3 我怎麼給我的 Text widget 設置樣式?
除了字體以外,你也可以自定義 Text widget 的其他樣式。Text
widget 接收一個 TextStyle
對象的參數,可以指定很多參數,例如:
color
decoration
decorationColor
decorationStyle
fontFamily
fontSize
fontStyle
fontWeight
hashCode
height
inherit
letterSpacing
textBaseline
wordSpacing
九、表單輸入
9.1 Flutter 中如何使用表單?我怎麼拿到用戶的輸入?
我們知道 Flutter 使用的是不可變而且狀態分離的 widget,你可能會好奇這種情況下如何處理用戶的輸入。在 iOS 上,一般會在提交數據時查詢當前組件的數值或動作。那麼在 Flutter 中會怎麼樣呢?
和 Flutter 的其他部分一樣,表單處理要通過特定的 widget 來實現。如果你有一個 TextField
或者 TextFormField
, 你可以通過 TextEditingController
來 獲取用戶的輸入:
class _MyFormState extends State<MyForm> {
// Create a text controller and use it to retrieve the current value.
// of the TextField!
final myController = TextEditingController();
@override
void dispose() {
// Clean up the controller when disposing of the Widget.
myController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(Retrieve Text Input),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: myController,
),
),
floatingActionButton: FloatingActionButton(
// When the user presses the button, show an alert dialog with the
// text the user has typed into our text field.
onPressed: () {
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
// Retrieve the text the user has typed in using our
// TextEditingController
content: Text(myController.text),
);
},
);
},
tooltip: Show me the value!,
child: Icon(Icons.text_fields),
),
);
}
}
你在Flutter Cookbook的Retrieve the value of a text field中可以找到更多的相關內容以及詳細的代碼列表。
9.2 Text field 中的 placeholder 相當於什麼?
在 Flutter 里,通過向 Text
widget 傳遞一個 InputDecoration
對象,你可以輕易的顯示文本框的提示信息,或是 placeholder。
body: Center(
child: TextField(
decoration: InputDecoration(hintText: "This is a hint"),
),
)
9.3 如何展示驗證錯誤信息?
就和顯示提示信息一樣,你可以通過向 Text
widget 傳遞一個 InputDecoration
來實現。
然而,你並不想在一開始就顯示錯誤信息。相反,在用戶輸入非法數據後,應該更新狀態,並傳遞一個新的 InputDecoration
對象。
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@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> {
String _errorText;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: Center(
child: TextField(
onSubmitted: (String text) {
setState(() {
if (!isEmail(text)) {
_errorText = Error: This is not an email;
} else {
_errorText = null;
}
});
},
decoration: InputDecoration(hintText: "This is a hint", errorText: _getErrorText()),
),
),
);
}
_getErrorText() {
return _errorText;
}
bool isEmail(String emailString) {
String emailRegexp =
r^(([^<>()[]\.,;:s@"]+(.[^<>()[]\.,;:s@"]+)*)|(".+"))@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}])|(([a-zA-Z-0-9]+.)+[a-zA-Z]{2,}))$;
RegExp regExp = RegExp(emailRegexp);
return regExp.hasMatch(emailString);
}
}
十、和硬體、第三方服務以及系統平台交互
10.1 如何與系統平台以及平台原生代碼進行交互?
Flutter 並不直接在平台上運行代碼;而是以 Dart 代碼的方式原生運行於設備之上,這算是繞過了平台的 SDK 的限制。 這意味著,例如,你用 Dart 發起了一個網路請求,它會直接在 Dart 的上下文中運行。 你不需要調用寫 iOS 或者 Android 原生應用時常用的 API 介面。你的 Flutter 應用仍舊被原生平台 的 ViewController
當做一個 view 來管理,但是你不能夠直接訪問 ViewController
自身或是對應的原生框架。
這並不意味著 Flutter 應用不能夠和原生 API,或是原生代碼進行交互。Flutter 提供了用來和宿主 ViewController
通信 和交換數據的 platform channels。 platform channels 本質上是一個橋接了 Dart 代碼與宿主 ViewController
和 iOS 框架的非同步通信模型。你可以通過 platform channels 來執行原生代碼的方法,或者獲取設備的感測器信息等數據。
除了直接使用 platform channels 之外,也可以使用一系列包含了原生代碼和 Dart 代碼,實現了特定功能的現有插件。例如,你在 Flutter 中可以直接使用插件來訪問相冊或是設備攝像頭,而不需要自己重新集成。Pub 是一個 Dart 和 Flutter 的開源包倉庫,你可以在這裡找到需要的插件。有些包可能支持集成 iOS 或 Android,或兩者皆有。
如果你在 Pub 找不到自己需要的包,你可以自己寫一個, 並發布到 Pub 上。
10.2 如何訪問 GPS 感測器?
使用 geolocator 插件,這一插件由社區提供。
10.3 如何訪問攝像頭?
image_picker 是常用的訪問相機的插件。
10.4 我怎麼登錄 Facebook?
登錄 Facebook 可以使用 flutter_facebook_login 插件。
10.5 如何集成 Firebase 功能?
大多數的 Firebase 特性都在 官方維護的插件 中實現了。 這些插件由 Flutter 官方團隊維護:
- 搭配 firebase_admob 插件來使用 Firebase AdMob
- 搭配 firebase_analytics 插件來使用 Firebase Analytics
- 搭配 firebase_auth 插件來使用 Firebase Auth
- 搭配 firebase_core 插件來使用 Firebase 核心庫
- 搭配 firebase_database 插件來使用 Firebase RTDB
- 搭配 firebase_storage 插件來使用 Firebase Cloud Storage
- 搭配 firebase_messaging 插件來使用 Firebase Messaging (FCM)
- 搭配 cloud_firestore 插件來使用 Firebase Cloud Firestore
在 Pub 上你也可以找到一些第三方的 Firebase 插件,主要實現了官方插件沒有直接實現的功能。
10.6 如何構建自己的插件?
如果有一些 Flutter 和遺漏的平台特性,可以 根據 developing packages and plugins 構建 自己的插件。
Flutter 的插件結構,簡單來說,更像是 Android 中的 Event bus:你發送一個消息,並讓接受者處理並反饋 結果給你。這種情況下,接受者就是在 iOS 或 Android 的原生代碼。
十一、資料庫和本地存儲
11.1 Flutter 中如何訪問 UserDefaults?
在 iOS 里,可以使用屬性列表存儲一個鍵值對的集合,也就是我們所說的 UserDefaults。
在 Flutter 里,可以使用 Shared Preferences 插件來實現相同的功能。這個插件封裝了 UserDefaults
以及 Android 里類似的 SharedPreferences
。
11.2 CoreData 相當於 Flutter 中的什麼?
在 iOS 里,你可以使用 CoreData 來存儲結構化的數據。這是一個基於 SQL 資料庫的上層封裝,可以使關聯模型的查詢變得更加簡單。
在 Flutter 里,可以使用 SQFlite 插件來實現這個功能。
十二、通知
12.1 如何設置推送通知?
在 iOS 里,你需要向開發者中心註冊來允許推送通知。
在 Flutter 里,使用 firebase_messaging
插件來實現這個功能。
關於 Firebase Cloud Messaging API 的更多信息,可以 查看 firebase_messaging 插件文檔。
推薦閱讀: