原文:https://flutterchina.club/flutter-for-ios/#views
本文檔適用那些希望將現有 iOS 經驗應用于 Flutter 的開發者。如果你擁有 iOS 開發基礎,那么你可以使用這篇文檔開始學習 Flutter 的開發。
開發 Flutter 時,你的 iOS 經驗和技能將會大有裨益,因為 Flutter 依賴于移動操作系統的眾多功能和配置。Flutter 是用于為移動設備構建用戶界面的全新方式,但它也有一個插件系統用于和 iOS(及 Android)進行非 UI 任務的通信。如果你是 iOS 開發專家,則你不必將 Flutter 徹底重新學習一遍。
你可以將此文檔作為 cookbook,通過跳轉并查找與你的需求最相關的問題。
Views
UIView 相當于 Flutter 中的什么?
在 iOS 中,構建 UI 的過程中將大量使用 view 對象。這些對象都是 UIView
的實例。它們可以用作容器來承載其他的 UIView,最終構成你的界面布局。
在 Flutter 中,你可以粗略地認為 Widget
相當于 UIView
。Widget 和 iOS 中的控件并不完全等價,但當你試圖去理解 Flutter 是如何工作的時候,你可以認為它們是“聲明和構建 UI 的方法”。
然而,Widget 和 UIView 還是有些區別的。首先,widgets 擁有不同的生存時間:它們一直存在且保持不變,直到當它們需要被改變。當 widgets 和它們的狀態被改變時,Flutter 會構建一顆新的 widgets 樹。作為對比,iOS 中的 views 在改變時并不會被重新創建。但是與其說 views 是可變的實例,不如說它們被繪制了一次,并且直到使用 setNeedsDisplay()
之后才會被重新繪制。
此外,不像 UIView,由于不可變性,Flutter 的 widgets 非常輕量。這是因為它們本身并不是什么控件,也不會被直接繪制出什么,而只是 UI 的描述。
Flutter 包含了 Material 組件庫。這些 widgets 遵循了 Material 設計規范。MD 是一個靈活的設計系統,并且為包括 iOS 在內的所有系統進行了優化。
但是用 Flutter 實現任何的設計語言都非常的靈活和富有表現力。在 iOS 平臺,你可以使用 Cupertino widgets 來構建遵循了 Apple’s iOS design language 的界面。
我怎么來更新 Widgets?
在 iOS 上更新 views,只需要直接改變它們就可以了。在 Flutter 中,widgets 是不可變的,而且不能被直接更新。你需要去操縱 widget 的 state。
這也正是有狀態的和無狀態的 widget 這一概念的來源。一個 StatelessWidget
正如它聽起來一樣,是一個沒有附加狀態的 widget。
StatelessWidget
在你構建初始化后不再進行改變的界面時非常有用。
舉個例子,你可能會用一個 UIImageView
來展示你的 logo image
。如果這個 logo 在運行時不會改變,那么你就可以在 Flutter 中使用 StatelessWidget
。
如果你希望在發起 HTTP 請求時,依托接收到的數據動態的改變 UI,請使用 StatefulWidget
。當 HTTP 請求結束后,通知 Flutter 框架 widget 的 State
更新了,好讓系統來更新 UI。
有狀態和無狀態的 widget 之間一個非常重要的區別是,StatefulWidget
擁有一個 State
對象來存儲它的狀態數據,并在 widget 樹重建時攜帶著它,因此狀態不會丟失。
如果你有疑惑,請記住以下規則:如果一個 widget 在它的 build
方法之外改變(例如,在運行時由于用戶的操作而改變),它就是有狀態的。如果一個 widget 在一次 build 之后永遠不變,那它就是無狀態的。但是,即便一個 widget 是有狀態的,包含它的父親 widget 也可以是無狀態的,只要父 widget 本身不響應這些變化。
下面的例子展示了如何使用一個 StatelessWidget
。一個常見的 StatelessWidget
是 Text
widget。如果你查看 Text 的實現,你會發現它是 StatelessWidget 的子類。
Text(
'I like Flutter!',
style: TextStyle(fontWeight: FontWeight.bold),
);
閱讀上面的代碼,你可能會注意到 Text
widget 并不顯示地攜帶任何狀態。它通過傳入給它的構造器的數據來渲染,除此之外再無其他。
但是,如果你希望 I like Flutter
在點擊 FloatingActionButton
時動態的改變呢?
為了實現這個,用 StatefulWidget
包裹 Text
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 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),
),
);
}
}
我怎么對 widget 布局?我的 Storyboard 在哪?
在 iOS 中,你可能會用 Storyboard 文件來組織 views,并對它們設置約束,或者,你可能在 view controller 中使用代碼來設置約束。在 Flutter 中,你通過編寫一個 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 catalog 中查看 Flutter 提供的布局。
我怎么在我的約束中添加或移除組件?
在 iOS 中,你在父 view 中調用 addSubview()
或在子 view 中調用 removeFromSuperview()
來動態地添加或移除子 views。在 Flutter 中,由于 widget 不可變,所以沒有和 addSubview()
直接等價的東西。作為替代,你可以向 parent 傳入一個返回 widget 的函數,并用一個布爾值來控制子 widget 的創建。
下面這個例子展示了在點擊 FloatingActionButton
時如何動態地切換兩個 widgets:
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),
),
);
}
}
我怎么對 widget 做動畫?
在 iOS 中,你通過調用 animate(withDuration:animations:)
方法來給一個 view 創建動畫。在 Flutter 中,使用動畫庫來包裹 widgets,而不是創建一個動畫 widget。
在 Flutter 中,使用 AnimationController
。這是一個可以暫停、尋找、停止、反轉動畫的 Animation<double>
類型。它需要一個 Ticker
當 vsync 發生時來發送信號,并且在每幀運行時創建一個介于 0 和 1 之間的線性插值(interpolation)。你可以創建一個或多個的 Animation
并附加給一個 controller。
例如,你可能會用 CurvedAnimation
來實現一個 interpolated 曲線。在這個場景中,controller 是動畫過程的“主人”,而 CurvedAnimation
計算曲線,并替代 controller 默認的線性模式。
當構建 widget 樹時,你會把 Animation
指定給一個 widget 的動畫屬性,比如 FadeTransition
的 opacity,并告訴控制器開始動畫。
下面這個例子展示了在點擊 FloatingActionButton
之后,如何使用 FadeTransition
來讓 widget 淡出到 logo 圖標:
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 tutorial 以及 Animations overview。
我該怎么繪圖?
在 iOS 上,你通過 CoreGraphics
來在屏幕上繪制線條和形狀。Flutter 有一套基于 Canvas
類的不同的 API,還有 CustomPaint
和 CustomPainter
這兩個類來幫助你繪圖。后者實現你在 canvas 上的繪圖算法。
想要學習如何實現一個筆跡畫筆,請參考 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),
);
}
}
Widget 的透明度在哪里?
在 iOS 中,什么東西都會有一個 .opacity 或是 .alpha 的屬性。在 Flutter 中,你需要給 widget 包裹一個 Opacity widget 來做到這一點。
我怎么創建自定義的 widgets?
在 iOS 中,你編寫 UIView
的子類,或使用已經存在的 view 來重載并實現方法,以達到特定的功能。在 Flutter 中,你會組合(composing)多個小的 widgets 來構建一個自定義的 widget(而不是擴展它)。
舉個例子,如果你要構建一個 CustomButton
,并在構造器中傳入它的 label?那就組合 RaisedButton
和 label,而不是擴展 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"),
);
}
導航
我怎么在不同頁面之間跳轉?
在 iOS 中,你可以使用管理了 view controller 棧的 UINavigationController
來在不同的 view controller 之間跳轉。
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');
Navigator
類不僅用來處理 Flutter 中的路由,還被用來獲取你剛 push 到棧中的路由返回的結果。通過 await
等待路由返回的結果來達到這點。
舉個例子,要跳轉到“位置”路由來讓用戶選擇一個地點,你可能要這么做:
Map coordinates = await Navigator.of(context).pushNamed('/location');
之后,在 location 路由中,一旦用戶選擇了地點,攜帶結果一起 pop()
出棧:
Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});
我怎么跳轉到其他 App?
在 iOS 中,要跳轉到其他 App,你需要一個特定的 URL Scheme。對系統級別的 App 來說,這個 scheme 取決于 App。為了在 Flutter 中實現這個功能,你可以創建一個原生平臺的整合層,或者使用現有的 plugin,例如 url_launcher。
線程和異步
我怎么編寫異步的代碼?
Dart 是單線程執行模型,但是它支持 Isolate
(一種讓 Dart 代碼運行在其他線程的方式)、事件循環和異步編程。除非你自己創建一個 Isolate
,否則你的 Dart 代碼永遠運行在 UI 線程,并由 event loop 驅動。Flutter 的 event loop 和 iOS 中的 main loop 相似——Looper
是附加在主線程上的。
Dart 的單線程模型并不意味著你寫的代碼一定是阻塞操作,從而卡住 UI。相反,使用 Dart 語言提供的異步工具,例如 async
/ await
,來實現異步操作。
舉個例子,你可以使用 async
/ await
來讓 Dart 幫你做一些繁重的工作,編寫網絡請求代碼而不會掛起 UI:
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 的區別,請參考下一章節。
你是怎么把工作放到后臺線程的?
由于 Flutter 是單線程并且跑著一個 event loop 的(就像 Node.js 那樣),你不必為線程管理或是開啟后臺線程而操心。如果你正在做 I/O 操作,如訪問磁盤或網絡請求,安全地使用 async
/ await
就完事了。如果,在另外的情況下,你需要做讓 CPU 執行繁忙的計算密集型任務,你需要使用 Isolate
來避免阻塞 event loop。
對于 I/O 操作,通過關鍵字 async
,把方法聲明為異步方法,然后通過await
關鍵字等待該異步方法執行完成(譯者語:這和javascript中是相同的):
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;
}
}
我怎么發起網絡請求?
在 Flutter 中,使用流行的 http package 做網絡請求非常簡單。它把你可能需要自己做的網絡請求操作抽象了出來,讓發起請求變得簡單。
要使用 http
包,在 pubspec.yaml
中把它添加為依賴:
dependencies:
...
http: ^0.11.3+16
發起網絡請求,在 http.get()
這個 async
方法中使用 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);
});
}
}
我怎么展示一個長時間運行的任務的進度?
在 iOS 中,在后臺運行耗時任務時你會使用 UIProgressView
。
在 Flutter 中,使用一個 ProgressIndicator
widget。通過一個布爾 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);
});
}
}
工程結構、本地化、依賴和資源
我怎么在 Flutter 中引入 image assets?多分辨率怎么辦?
iOS 把 images 和 assets 作為不同的東西,而 Flutter 中只有 assets。被放到 iOS 中 Images.xcasset
文件夾下的資源在 Flutter 中被放到了 assets 文件夾中。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
。把基礎圖片(1.0x)放置到 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");
}
更多細節,參見 Adding Assets and Images in Flutter。
我在哪里放置字符串?我怎么做本地化?
不像 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
定義了 App 支持哪些地區。上面的例子使用了一個 MaterialApp
,所以它既有 GlobalWidgetsLocalizations
用于基礎 widgets,也有 MaterialWidgetsLocalizations
用于 Material wigets 的本地化。如果你使用 WidgetsApp
,則無需包括后者。注意,這兩個代理雖然包括了“默認”值,但如果你想讓你的 App 本地化,你仍需要提供一或多個代理作為你的 App 本地化副本。
當初始化時,WidgetsApp
或 MaterialApp
會使用你指定的代理為你創建一個 Localizations
widget。Localizations
widget 可以隨時從當前上下文中訪問設備的地點,或者使用 Window.locale
。
要訪問本地化文件,使用 Localizations.of()
方法來訪問提供代理的特定本地化類。如需翻譯,使用 intl_translation
包來取出翻譯副本到 arb 文件中。把它們引入 App 中,并用 intl
來使用它們。
更多 Flutter 中國際化和本地化的細節,請訪問 internationalization guide ,那里有不使用 intl
包的示例代碼。
注意,在 Flutter 1.0 beta 2 之前,在 Flutter 中定義的 assets 不能在原生一側被訪問。原生定義的資源在 Flutter 中也不可用,因為它們在獨立的文件夾中。
Cocoapods 相當于什么?我該如何添加依賴?
在 iOS 中,你把依賴添加到 Podfile
中。Flutter 使用 Dart 構建系統和 Pub 包管理器來處理依賴。這些工具將本機 Android 和 iOS 包裝應用程序的構建委派給相應的構建系統。
如果你的 Flutter 工程中的 iOS 文件夾中擁有 Podfile,請僅在你為每個平臺集成時使用它??傮w來說,使用 pubspec.yaml
來在 Flutter 中聲明外部依賴。一個可以找到優秀 Flutter 包的地方是 Pub。
ViewControllers
ViewController 相當于 Flutter 中的什么?
在 iOS 中,一個 ViewController 代表了用戶界面的一部分,最常用于一個屏幕,或是其中一部分。它們被組合在一起用于構建復雜的用戶界面,并幫助你拆分 App 的 UI。在 Flutter 中,這一任務回落到了 widgets 中。就像在界面導航部分提到的一樣,一個屏幕也是被 widgets 來表示的,因為“萬物皆 widget!”。使用 Navigator
在 Route
之間跳轉,或者渲染相同數據的不同狀態。
我該怎么監聽 iOS 中的生命周期事件?
在 iOS 中,你可以重寫 ViewController
中的方法來捕獲它的視圖的生命周期,或者在 AppDelegate
中注冊生命周期的回調函數。在 Flutter 中沒有這兩個概念,但你可以通過 hook WidgetsBinding
觀察者來監聽生命周期事件,并監聽 didChangeAppLifecycleState()
的變化事件。
可觀察的生命周期事件有:
-
inactive
- 應用處于不活躍的狀態,并且不會接受用戶的輸入。這個事件僅工作在 iOS 平臺,在 Android 上沒有等價的事件。 -
paused
- 應用暫時對用戶不可見,雖然不接受用戶輸入,但是是在后臺運行的。 -
resumed
- 應用可見,也響應用戶的輸入。 -
suspending
- 應用暫時被掛起,在 iOS 上沒有這一事件。
更多關于這些狀態的細節和含義,請參見 AppLifecycleStatus
documentation 。
布局
UITableView 和 UICollectionView 相當于 Flutter 中的什么?
在 iOS 中,你可能用 UITableView 或 UICollectionView 來展示一個列表。在 Flutter 中,你可以用 ListView
來達到相似的實現。在 iOS 中,你通過代理方法來確定行數,每一個 index path 的單元格,以及單元格的尺寸。
由于 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;
}
}
我怎么知道列表的哪個元素被點擊了?
iOS 中,你通過 tableView:didSelectRowAtIndexPath:
代理方法來實現。在 Flutter 中,使用傳遞進來的 widget 的 touch handle:
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;
}
}
我怎么動態地更新 ListView?
在 iOS 中,你改變列表的數據,并通過 reloadData()
方法來通知 table 或是 collection view。
在 Flutter 中,如果你想通過 setState()
方法來更新 widget 列表,你會很快發現你的數據展示并沒有變化。這是因為當 setState()
被調用時,Flutter 渲染引擎會去檢查 widget 樹來查看是否有什么地方被改變了。當它得到你的 ListView
時,它會使用一個 ==
判斷,并且發現兩個 ListView
是相同的。沒有什么東西是變了的,因此更新不是必須的。
一個更新 ListView
的簡單方法是,在 setState()
中創建一個新的 list,并把舊 list 的數據拷貝給新的 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()
函數里并沒有重新創建一個 list,而是 .add
了一個 widget。
ScrollView 相當于 Flutter 里的什么?
在 iOS 中,你給 view 包裹上 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 總如何排布 widget 的文檔,請參閱 layout tutorial。
手勢檢測及觸摸事件處理
我怎么給 Flutter 的 widget 添加一個點擊監聽者?
在 iOS 中,你給一個 view 添加 GestureRecognizer
來處理點擊事件。在 Flutter 中,有兩種方法來添加點擊監聽者:
-
如果 widget 本身支持事件監測,直接傳遞給它一個函數,并在這個函數里實現響應方法。例如,
RaisedButton
widget 擁有一個RaisedButton
參數:@override Widget build(BuildContext context) { return RaisedButton( onPressed: () { print("click"); }, child: Text("Button"), ); }
-
如果 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"); }, ), ), ); } }
我怎么處理 widget 上的其他手勢?
使用 GestureDetector
你可以監聽更廣闊范圍內的手勢,比如:
- Tapping
-
onTapDown
— 在特定位置輕觸手勢接觸了屏幕。 -
onTapUp
— 在特定位置產生了一個輕觸手勢,并停止接觸屏幕。 -
onTap
— 產生了一個輕觸手勢。 -
onTapCancel
— 觸發了onTapDown
但沒能觸發 tap。
-
- Double tapping
-
onDoubleTap
— 用戶在同一個位置快速點擊了兩下屏幕。
-
- Long pressing
-
onLongPress
— 用戶在同一個位置長時間接觸屏幕。
-
- Vertical dragging
-
onVerticalDragStart
— 接觸了屏幕,并且可能會垂直移動。 -
onVerticalDragUpdate
— 接觸了屏幕,并繼續在垂直方向移動。 -
onVerticalDragEnd
— 之前接觸了屏幕并垂直移動,并在停止接觸屏幕前以某個垂直的速度移動。
-
- Horizontal dragging
-
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();
}
},
),
),
);
}
}
主題和文字
我怎么給 App 設置主題?
Flutter 實現了一套漂亮的 MD 組件,并且開箱可用。它接管了一大堆你需要的樣式和主題。
為了充分發揮你的 App 中 MD 組件的優勢,聲明一個頂級 widget,MaterialApp,用作你的 App 入口。MaterialApp 是一個便利組件,包含了許多 App 通常需要的 MD 風格組件。它通過一個 WidgetsApp 添加了 MD 功能來實現。
但是 Flutter 足夠地靈活和富有表現力來實現任何其他的設計語言。在 iOS 上,你可以用 Cupertino library 來制作遵守 Human Interface Guidelines 的界面。查看這些 widget 的集合,請參閱 Cupertino widgets gallery。
你也可以在你的 App 中使用 WidgetApp,它提供了許多相似的功能,但不如 MaterialApp
那樣強大。
對任何子組件定義顏色和樣式,可以給 MaterialApp
widget 傳遞一個 ThemeData
對象。舉個例子,在下面的代碼中,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(),
);
}
}
我怎么給 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'),
),
),
);
}
我怎么給我的 Text widget 設置樣式?
除了字體以外,你也可以給 Text widget 的樣式元素設置自定義值。Text
widget 接受一個 TextStyle
對象,你可以指定許多參數,比如:
color
decoration
decorationColor
decorationStyle
fontFamily
fontSize
fontStyle
fontWeight
hashCode
height
inherit
letterSpacing
textBaseline
wordSpacing
表單輸入
Flutter 中表單怎么工作?我怎么拿到用戶的輸入?
我們已經提到 Flutter 使用不可變的 widget,并且狀態是分離的,你可能會好奇在這種情境下怎么處理用戶的輸入。在 iOS 中,你經常在需要提交數據時查詢組件當前的狀態或動作,但這在 Flutter 中是怎么工作的呢?
在表單處理的實踐中,就像在 Flutter 中任何其他的地方一樣,要通過特定的 widgets。如果你有一個 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),
),
);
}
}
你可以在這里獲得更多信息,或是完整的代碼列表: Retrieve the value of a text field,來自 Flutter Cookbook 。
Text field 中的 placeholder 相當于什么?
在 Flutter 中,你可以輕易地通過向 Text widget 的裝飾構造器參數重傳遞 InputDecoration
來展示“小提示”,或是占位符文字:
body: Center(
child: TextField(
decoration: InputDecoration(hintText: "This is a hint"),
),
)
我怎么展示驗證錯誤信息?
就像展示“小提示”一樣,向 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 em) {
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(p);
return regExp.hasMatch(em);
}
}
和硬件、第三方服務以及平臺交互
我怎么和平臺,以及平臺的原生代碼交互?
Flutter 的代碼并不直接在平臺之下運行,相反,Dart 代碼構建的 Flutter 應用在設備上以原生的方式運行,卻“側步躲開了”平臺提供的 SDK。這意味著,例如,你在 Dart 中發起一個網絡請求,它就直接在 Dart 的上下文中運行。你并不會用上平常在 iOS 或 Android 上使用的原生 API。你的 Flutter 程序仍然被原生平臺的 ViewController
管理作一個 view,但是你并不會直接訪問 ViewController
自身,或是原生框架。
但這并不意味著 Flutter 不能和原生 API,或任何你編寫的原生代碼交互。Flutter 提供了 platform channels ,來和管理你的 Flutter view 的 ViewController 通信和交互數據。平臺管道本質上是一個異步通信機制,橋接了 Dart 代碼和宿主 ViewController,以及它運行于的 iOS 框架。你可以用平臺管道來執行一個原生的函數,或者是從設備的傳感器中獲取數據。
除了直接使用平臺管道之外,你還可以使用一系列預先制作好的 plugins。例如,你可以直接使用插件來訪問相機膠卷或是設備的攝像頭,而不必編寫你自己的集成層代碼。你可以在 Pub 上找到插件,這是一個 Dart 和 Flutter 的開源包倉庫。其中一些包可能會支持集成 iOS 或 Android,或兩者均可。
如果你在 Pub 上找不到符合你需求的插件,你可以自己編寫 ,并且發布在 Pub 上。
我怎么訪問 GPS 傳感器?
使用 location
社區插件。
我怎么訪問攝像頭?
image_picker
在訪問攝像頭時非常常用。
我怎么登錄 Facebook?
登錄 Facebook 可以使用 flutter_facebook_login
社區插件。
我怎么使用 Firebase 特性?
大多數 Firebase 特性被 first party plugins 包含了。這些第一方插件由 Flutter 團隊維護:
-
firebase_admob
for Firebase AdMob -
firebase_analytics
for Firebase Analytics -
firebase_auth
for Firebase Auth -
firebase_core
for Firebase’s Core package -
firebase_database
for Firebase RTDB -
firebase_storage
for Firebase Cloud Storage -
firebase_messaging
for Firebase Messaging (FCM) -
cloud_firestore
for Firebase Cloud Firestore
你也可以在 Pub 上找到 Firebase 的第三方插件。
我怎創建自己的原生集成層?
如果有一些 Flutter 和社區插件遺漏的平臺相關的特性,可以根據 developing packages and plugins 頁面構建自己的插件。
Flutter 的插件結構,簡要來說,就像 Android 中的 Event bus。你發送一個消息,并讓接受者處理并反饋結果給你。在這種情況下,接受者就是在 Android 或 iOS 上的原生代碼。
數據庫和本地存儲
我怎么在 Flutter 中訪問 UserDefaults?
在 iOS 中,你可以使用屬性列表來存儲鍵值對的集合,即我們熟悉的 UserDefaults。
在 Flutter 中,可以使用 Shared Preferences plugin 來達到相似的功能。它包裹了 UserDefaluts
以及 Android 上等價的 SharedPreferences
的功能。
CoreData 相當于 Flutter 中的什么?
在 iOS 中,你通過 CoreData 來存儲結構化的數據。這是一個 SQL 數據庫的上層封裝,讓查詢和關聯模型變得更加簡單。
在 Flutter 中,使用 SQFlite 插件來實現這個功能。
通知
我怎么推送通知?
在 iOS 中,你需要向蘋果開發者平臺中注冊來允許推送通知。
在 Flutter 中,使用 firebase_messaging
插件來實現這一功能。
更多使用 Firebase Cloud Messaging API 的信息,請參閱 firebase_messaging
插件文檔。