使用Flutter開發(fā)iOS APP
Flutter for iOS 開發(fā)者
本文檔適用那些希望將現(xiàn)有 iOS 經(jīng)驗(yàn)應(yīng)用于 Flutter 的開發(fā)者。如果你擁有 iOS 開發(fā)基礎(chǔ),那么你可以使用這篇文檔開始學(xué)習(xí) Flutter 的開發(fā)。
開發(fā) Flutter 時(shí),你的 iOS 經(jīng)驗(yàn)和技能將會(huì)大有裨益,因?yàn)?Flutter 依賴于移動(dòng)操作系統(tǒng)的眾多功能和配置。Flutter 是用于為移動(dòng)設(shè)備構(gòu)建用戶界面的全新方式,但它也有一個(gè)插件系統(tǒng)用于和 iOS(及 Android)進(jìn)行非 UI 任務(wù)的通信。如果你是 iOS 開發(fā)專家,則你不必將 Flutter 徹底重新學(xué)習(xí)一遍。
你可以將此文檔作為 cookbook,通過跳轉(zhuǎn)并查找與你的需求最相關(guān)的問題。
UIView 相當(dāng)于 Flutter 中的什么?
在 iOS 中,構(gòu)建 UI 的過程中將大量使用 view 對(duì)象。這些對(duì)象都是 UIView 的實(shí)例。它們可以用作容器來承載其他的 UIView,最終構(gòu)成你的界面布局。
在 Flutter 中,你可以粗略地認(rèn)為 Widget 相當(dāng)于 UIView 。Widget 和 iOS 中的控件并不完全等價(jià),但當(dāng)你試圖去理解 Flutter 是如何工作的時(shí)候,你可以認(rèn)為它們是“聲明和構(gòu)建 UI 的方法”。
然而,Widget 和 UIView 還是有些區(qū)別的。首先,widgets 擁有不同的生存時(shí)間:它們一直存在且保持不變,直到當(dāng)它們需要被改變。當(dāng) widgets 和它們的狀態(tài)被改變時(shí),F(xiàn)lutter 會(huì)構(gòu)建一顆新的 widgets 樹。作為對(duì)比,iOS 中的 views 在改變時(shí)并不會(huì)被重新創(chuàng)建。但是與其說 views 是可變的實(shí)例,不如說它們被繪制了一次,并且直到使用 setNeedsDisplay() 之后才會(huì)被重新繪制。
此外,不像 UIView,由于不可變性,F(xiàn)lutter 的 widgets 非常輕量。這是因?yàn)樗鼈儽旧聿⒉皇鞘裁纯丶?,也不?huì)被直接繪制出什么,而只是 UI 的描述。
Flutter 包含了 Material 組件庫。這些 widgets 遵循了 Material 設(shè)計(jì)規(guī)范。MD 是一個(gè)靈活的設(shè)計(jì)系統(tǒng),并且為包括 iOS 在內(nèi)的所有系統(tǒng)進(jìn)行了優(yōu)化。
但是用 Flutter 實(shí)現(xiàn)任何的設(shè)計(jì)語言都非常的靈活和富有表現(xiàn)力。在 iOS 平臺(tái),你可以使用 Cupertino widgets 來構(gòu)建遵循了 Apple’s iOS design language 的界面。
我怎么來更新 Widgets?
在 iOS 上更新 views,只需要直接改變它們就可以了。在 Flutter 中,widgets 是不可變的,而且不能被直接更新。你需要去操縱 widget 的 state。
這也正是有狀態(tài)的和無狀態(tài)的 widget 這一概念的來源。一個(gè) StatelessWidget 正如它聽起來一樣,是一個(gè)沒有附加狀態(tài)的 widget。
StatelessWidget 在你構(gòu)建初始化后不再進(jìn)行改變的界面時(shí)非常有用。
舉個(gè)例子,你可能會(huì)用一個(gè) UIImageView 來展示你的 logo image 。如果這個(gè) logo 在運(yùn)行時(shí)不會(huì)改變,那么你就可以在 Flutter 中使用 StatelessWidget 。
如果你希望在發(fā)起 HTTP 請(qǐng)求時(shí),依托接收到的數(shù)據(jù)動(dòng)態(tài)的改變 UI,請(qǐng)使用 StatefulWidget。當(dāng) HTTP 請(qǐng)求結(jié)束后,通知 Flutter 框架 widget 的 State 更新了,好讓系統(tǒng)來更新 UI。
有狀態(tài)和無狀態(tài)的 widget 之間一個(gè)非常重要的區(qū)別是,StatefulWidget 擁有一個(gè) State 對(duì)象來存儲(chǔ)它的狀態(tài)數(shù)據(jù),并在 widget 樹重建時(shí)攜帶著它,因此狀態(tài)不會(huì)丟失。
如果你有疑惑,請(qǐng)記住以下規(guī)則:如果一個(gè) widget 在它的 build 方法之外改變(例如,在運(yùn)行時(shí)由于用戶的操作而改變),它就是有狀態(tài)的。如果一個(gè) widget 在一次 build 之后永遠(yuǎn)不變,那它就是無狀態(tài)的。但是,即便一個(gè) widget 是有狀態(tài)的,包含它的父親 widget 也可以是無狀態(tài)的,只要父 widget 本身不響應(yīng)這些變化。
下面的例子展示了如何使用一個(gè) StatelessWidget 。一個(gè)常見的 StatelessWidget 是 Text widget。如果你查看 Text 的實(shí)現(xiàn),你會(huì)發(fā)現(xiàn)它是 StatelessWidget 的子類。
Text(
'I like Flutter!',
style: TextStyle(fontWeight: FontWeight.bold),);
閱讀上面的代碼,你可能會(huì)注意到 Text widget 并不顯示地?cái)y帶任何狀態(tài)。它通過傳入給它的構(gòu)造器的數(shù)據(jù)來渲染,除此之外再無其他。
但是,如果你希望 I like Flutter 在點(diǎn)擊 FloatingActionButton 時(shí)動(dòng)態(tài)的改變呢?
為了實(shí)現(xiàn)這個(gè),用 StatefulWidget 包裹 Text widget,并在用戶點(diǎn)擊按鈕時(shí)更新它。
舉個(gè)例子:
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),
),
);
}}
我怎么對(duì) widget 布局?我的 Storyboard 在哪?
在 iOS 中,你可能會(huì)用 Storyboard 文件來組織 views,并對(duì)它們?cè)O(shè)置約束,或者,你可能在 view controller 中使用代碼來設(shè)置約束。在 Flutter 中,你通過編寫一個(gè) widget 樹來聲明你的布局。
下面這個(gè)例子展示了如何展示一個(gè)帶有 padding 的簡單 widget:
@overrideWidget 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 中調(diào)用 addSubview() 或在子 view 中調(diào)用 removeFromSuperview() 來動(dòng)態(tài)地添加或移除子 views。在 Flutter 中,由于 widget 不可變,所以沒有和 addSubview() 直接等價(jià)的東西。作為替代,你可以向 parent 傳入一個(gè)返回 widget 的函數(shù),并用一個(gè)布爾值來控制子 widget 的創(chuàng)建。
下面這個(gè)例子展示了在點(diǎn)擊 FloatingActionButton 時(shí)如何動(dòng)態(tài)地切換兩個(gè) 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),
),
);
}}
我怎么對(duì) widget 做動(dòng)畫?
在 iOS 中,你通過調(diào)用 animate(withDuration:animations:) 方法來給一個(gè) view 創(chuàng)建動(dòng)畫。在 Flutter 中,使用動(dòng)畫庫來包裹 widgets,而不是創(chuàng)建一個(gè)動(dòng)畫 widget。
在 Flutter 中,使用 AnimationController 。這是一個(gè)可以暫停、尋找、停止、反轉(zhuǎn)動(dòng)畫的 Animation<double> 類型。它需要一個(gè) Ticker 當(dāng) vsync 發(fā)生時(shí)來發(fā)送信號(hào),并且在每幀運(yùn)行時(shí)創(chuàng)建一個(gè)介于 0 和 1 之間的線性插值(interpolation)。你可以創(chuàng)建一個(gè)或多個(gè)的 Animation 并附加給一個(gè) controller。
例如,你可能會(huì)用 CurvedAnimation 來實(shí)現(xiàn)一個(gè) interpolated 曲線。在這個(gè)場景中,controller 是動(dòng)畫過程的“主人”,而 CurvedAnimation 計(jì)算曲線,并替代 controller 默認(rèn)的線性模式。
當(dāng)構(gòu)建 widget 樹時(shí),你會(huì)把 Animation 指定給一個(gè) widget 的動(dòng)畫屬性,比如 FadeTransition 的 opacity,并告訴控制器開始動(dòng)畫。
下面這個(gè)例子展示了在點(diǎn)擊 FloatingActionButton 之后,如何使用 FadeTransition 來讓 widget 淡出到 logo 圖標(biāo):
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();
}}
更多信息,請(qǐng)參閱 Animation & Motion widgets, Animations tutorial 以及 Animations overview。
我該怎么繪圖?
在 iOS 上,你通過 CoreGraphics 來在屏幕上繪制線條和形狀。Flutter 有一套基于 Canvas 類的不同的 API,還有 CustomPaint 和 CustomPainter 這兩個(gè)類來幫助你繪圖。后者實(shí)現(xiàn)你在 canvas 上的繪圖算法。
想要學(xué)習(xí)如何實(shí)現(xiàn)一個(gè)筆跡畫筆,請(qǐng)參考 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 中,什么東西都會(huì)有一個(gè) .opacity 或是 .alpha 的屬性。在 Flutter 中,你需要給 widget 包裹一個(gè) Opacity widget 來做到這一點(diǎn)。
我怎么創(chuàng)建自定義的 widgets?
在 iOS 中,你編寫 UIView 的子類,或使用已經(jīng)存在的 view 來重載并實(shí)現(xiàn)方法,以達(dá)到特定的功能。在 Flutter 中,你會(huì)組合(composing)多個(gè)小的 widgets 來構(gòu)建一個(gè)自定義的 widget(而不是擴(kuò)展它)。
舉個(gè)例子,如果你要構(gòu)建一個(gè) CustomButton ,并在構(gòu)造器中傳入它的 label?那就組合 RaisedButton 和 label,而不是擴(kuò)展 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:
@overrideWidget build(BuildContext context) {
return Center(
child: CustomButton("Hello"),
);}
我怎么在不同頁面之間跳轉(zhuǎn)?
在 iOS 中,你可以使用管理了 view controller 棧的 UINavigationController 來在不同的 view controller 之間跳轉(zhuǎn)。
Flutter 也有類似的實(shí)現(xiàn),使用了 Navigator 和 Routes。一個(gè)路由是 App 中“屏幕”或“頁面”的抽象,而一個(gè) Navigator 是管理多個(gè)路由的 widget 。你可以粗略地把一個(gè)路由對(duì)應(yīng)到一個(gè) UIViewController。Navigator 的工作原理和 iOS 中 UINavigationController 非常相似,當(dāng)你想跳轉(zhuǎn)到新頁面或者從新頁面返回時(shí),它可以 push() 和 pop() 路由。
在頁面之間跳轉(zhuǎn),你有幾個(gè)選擇:
具體指定一個(gè)由路由名構(gòu)成的 Map。(MaterialApp)
直接跳轉(zhuǎn)到一個(gè)路由。(WidgetApp)
下面是構(gòu)建一個(gè) 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 給一個(gè) Navigator 來跳轉(zhuǎn):
Navigator.of(context).pushNamed('/b');
Navigator 類不僅用來處理 Flutter 中的路由,還被用來獲取你剛 push 到棧中的路由返回的結(jié)果。通過 await等待路由返回的結(jié)果來達(dá)到這點(diǎn)。
舉個(gè)例子,要跳轉(zhuǎn)到“位置”路由來讓用戶選擇一個(gè)地點(diǎn),你可能要這么做:
Map coordinates = await Navigator.of(context).pushNamed('/location');
之后,在 location 路由中,一旦用戶選擇了地點(diǎn),攜帶結(jié)果一起 pop() 出棧:
Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});
我怎么跳轉(zhuǎn)到其他 App?
在 iOS 中,要跳轉(zhuǎn)到其他 App,你需要一個(gè)特定的 URL Scheme。對(duì)系統(tǒng)級(jí)別的 App 來說,這個(gè) scheme 取決于 App。為了在 Flutter 中實(shí)現(xiàn)這個(gè)功能,你可以創(chuàng)建一個(gè)原生平臺(tái)的整合層,或者使用現(xiàn)有的 plugin,例如 url_launcher。
我怎么編寫異步的代碼?
Dart 是單線程執(zhí)行模型,但是它支持 Isolate(一種讓 Dart 代碼運(yùn)行在其他線程的方式)、事件循環(huán)和異步編程。除非你自己創(chuàng)建一個(gè) Isolate ,否則你的 Dart 代碼永遠(yuǎn)運(yùn)行在 UI 線程,并由 event loop 驅(qū)動(dòng)。Flutter 的 event loop 和 iOS 中的 main loop 相似——Looper 是附加在主線程上的。
Dart 的單線程模型并不意味著你寫的代碼一定是阻塞操作,從而卡住 UI。相反,使用 Dart 語言提供的異步工具,例如 async / await ,來實(shí)現(xiàn)異步操作。
舉個(gè)例子,你可以使用 async / await 來讓 Dart 幫你做一些繁重的工作,編寫網(wǎng)絡(luò)請(qǐng)求代碼而不會(huì)掛起 UI:
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});}
一旦 await 到網(wǎng)絡(luò)請(qǐng)求完成,通過調(diào)用 setState() 來更新 UI,這會(huì)觸發(fā) widget 子樹的重建,并更新相關(guān)數(shù)據(jù)。
下面的例子展示了異步加載數(shù)據(jù),并用 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);
});
}}
你是怎么把工作放到后臺(tái)線程的?
由于 Flutter 是單線程并且跑著一個(gè) event loop 的(就像 Node.js 那樣),你不必為線程管理或是開啟后臺(tái)線程而操心。如果你正在做 I/O 操作,如訪問磁盤或網(wǎng)絡(luò)請(qǐng)求,安全地使用 async / await 就完事了。如果,在另外的情況下,你需要做讓 CPU 執(zhí)行繁忙的計(jì)算密集型任務(wù),你需要使用 Isolate 來避免阻塞 event loop。
對(duì)于 I/O 操作,通過關(guān)鍵字 async,把方法聲明為異步方法,然后通過await關(guān)鍵字等待該異步方法執(zhí)行完成(譯者語:這和javascript中是相同的):
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});}
這就是對(duì)諸如網(wǎng)絡(luò)請(qǐng)求或數(shù)據(jù)庫訪問等 I/O 操作的典型做法。
然而,有時(shí)候你需要處理大量的數(shù)據(jù),這會(huì)導(dǎo)致你的 UI 掛起。在 Flutter 中,使用 Isolate 來發(fā)揮多核心 CPU 的優(yōu)勢來處理那些長期運(yùn)行或是計(jì)算密集型的任務(wù)。
Isolates 是分離的運(yùn)行線程,并且不和主線程的內(nèi)存堆共享內(nèi)存。這意味著你不能訪問主線程中的變量,或者使用 setState() 來更新 UI。正如它們的名字一樣,Isolates 不能共享內(nèi)存。
下面的例子展示了一個(gè)簡單的 isolate,是如何把數(shù)據(jù)返回給主線程來更新 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 isolatestatic 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() 是一個(gè)運(yùn)行于自己獨(dú)立執(zhí)行線程上的 Isolate。在 isolate 里,你可以執(zhí)行 CPU 密集型任務(wù)(例如解析一個(gè)龐大的 json),或是計(jì)算密集型的數(shù)學(xué)操作,如加密或信號(hào)處理等。
你可以運(yùn)行下面的完整例子:
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;
}}
我怎么發(fā)起網(wǎng)絡(luò)請(qǐng)求?
在 Flutter 中,使用流行的 http package 做網(wǎng)絡(luò)請(qǐng)求非常簡單。它把你可能需要自己做的網(wǎng)絡(luò)請(qǐng)求操作抽象了出來,讓發(fā)起請(qǐng)求變得簡單。
要使用 http 包,在 pubspec.yaml 中把它添加為依賴:
dependencies:
...
http: ^0.11.3+16
發(fā)起網(wǎng)絡(luò)請(qǐng)求,在 http.get() 這個(gè) 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);
});
}}
我怎么展示一個(gè)長時(shí)間運(yùn)行的任務(wù)的進(jìn)度?
在 iOS 中,在后臺(tái)運(yùn)行耗時(shí)任務(wù)時(shí)你會(huì)使用 UIProgressView。
在 Flutter 中,使用一個(gè) ProgressIndicator widget。通過一個(gè)布爾 flag 來控制是否展示進(jìn)度。在任務(wù)開始時(shí),告訴 Flutter 更新狀態(tài),并在結(jié)束后隱去。
在下面的例子中,build 函數(shù)被拆分成三個(gè)函數(shù)。如果 showLoadingDialog() 是 true (當(dāng) widgets.length == 0 時(shí)),則渲染 ProgressIndicator。否則,當(dāng)數(shù)據(jù)從網(wǎng)絡(luò)請(qǐng)求中返回時(shí),渲染 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');}
對(duì)于圖片,F(xiàn)lutter 像 iOS 一樣,遵循了一個(gè)簡單的基于像素密度的格式。Image assets 可能是 1.0x 2.0x 3.0x 或是其他的任何倍數(shù)。這些所謂的 devicePixelRatio 傳達(dá)了物理像素到單個(gè)邏輯像素的比率。
Assets 可以被放置到任何屬性文件夾中——Flutter 并沒有預(yù)先定義的文件結(jié)構(gòu)。在 pubspec.yaml 文件中聲明 assets (和位置),然后 Flutter 會(huì)把他們識(shí)別出來。
舉個(gè)例子,要把一個(gè)叫 my_icon.png 的圖片放到 Flutter 工程中,你可能想要把存儲(chǔ)它的文件夾叫做 images。把基礎(chǔ)圖片(1.0x)放置到 images 文件夾中,并把其他變體放置在子文件夾中,并接上合適的比例系數(shù):
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 中直接使用:
@overrideWidget build(BuildContext context) {
return Image.asset("images/my_image.png");}
更多細(xì)節(jié),參見 Adding Assets and Images in Flutter。
我在哪里放置字符串?我怎么做本地化?
不像 iOS 擁有一個(gè) Localizable.strings 文件,F(xiàn)lutter 目前并沒有一個(gè)用于處理字符串的系統(tǒng)。目前,最佳實(shí)踐是把你的文本拷貝到靜態(tài)區(qū),并在這里訪問。例如:
class Strings {
static String welcomeMessage = "Welcome To Flutter";}
并且這樣訪問你的字符串:
Text(Strings.welcomeMessage)
默認(rèn)情況下,F(xiàn)lutter 只支持美式英語字符串。如果你要支持其他語言,請(qǐng)引入 flutter_localizations 包。你可能也要引入 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 定義了 App 支持哪些地區(qū)。上面的例子使用了一個(gè) MaterialApp ,所以它既有 GlobalWidgetsLocalizations 用于基礎(chǔ) widgets,也有 MaterialWidgetsLocalizations 用于 Material wigets 的本地化。如果你使用 WidgetsApp ,則無需包括后者。注意,這兩個(gè)代理雖然包括了“默認(rèn)”值,但如果你想讓你的 App 本地化,你仍需要提供一或多個(gè)代理作為你的 App 本地化副本。
當(dāng)初始化時(shí),WidgetsApp 或 MaterialApp 會(huì)使用你指定的代理為你創(chuàng)建一個(gè) Localizations widget。Localizations widget 可以隨時(shí)從當(dāng)前上下文中訪問設(shè)備的地點(diǎn),或者使用 Window.locale。
要訪問本地化文件,使用 Localizations.of() 方法來訪問提供代理的特定本地化類。如需翻譯,使用 intl_translation 包來取出翻譯副本到 arb 文件中。把它們引入 App 中,并用 intl 來使用它們。
更多 Flutter 中國際化和本地化的細(xì)節(jié),請(qǐng)?jiān)L問 internationalization guide ,那里有不使用 intl 包的示例代碼。
注意,在 Flutter 1.0 beta 2 之前,在 Flutter 中定義的 assets 不能在原生一側(cè)被訪問。原生定義的資源在 Flutter 中也不可用,因?yàn)樗鼈冊(cè)讵?dú)立的文件夾中。
Cocoapods 相當(dāng)于什么?我該如何添加依賴?
在 iOS 中,你把依賴添加到 Podfile 中。Flutter 使用 Dart 構(gòu)建系統(tǒng)和 Pub 包管理器來處理依賴。這些工具將本機(jī) Android 和 iOS 包裝應(yīng)用程序的構(gòu)建委派給相應(yīng)的構(gòu)建系統(tǒng)。
如果你的 Flutter 工程中的 iOS 文件夾中擁有 Podfile,請(qǐng)僅在你為每個(gè)平臺(tái)集成時(shí)使用它。總體來說,使用 pubspec.yaml 來在 Flutter 中聲明外部依賴。一個(gè)可以找到優(yōu)秀 Flutter 包的地方是 Pub。
ViewController 相當(dāng)于 Flutter 中的什么?
在 iOS 中,一個(gè) ViewController 代表了用戶界面的一部分,最常用于一個(gè)屏幕,或是其中一部分。它們被組合在一起用于構(gòu)建復(fù)雜的用戶界面,并幫助你拆分 App 的 UI。在 Flutter 中,這一任務(wù)回落到了 widgets 中。就像在界面導(dǎo)航部分提到的一樣,一個(gè)屏幕也是被 widgets 來表示的,因?yàn)椤叭f物皆 widget!”。使用 Navigator 在 Route 之間跳轉(zhuǎn),或者渲染相同數(shù)據(jù)的不同狀態(tài)。
我該怎么監(jiān)聽 iOS 中的生命周期事件?
在 iOS 中,你可以重寫 ViewController 中的方法來捕獲它的視圖的生命周期,或者在 AppDelegate 中注冊(cè)生命周期的回調(diào)函數(shù)。在 Flutter 中沒有這兩個(gè)概念,但你可以通過 hook WidgetsBinding 觀察者來監(jiān)聽生命周期事件,并監(jiān)聽 didChangeAppLifecycleState() 的變化事件。
可觀察的生命周期事件有:
inactive - 應(yīng)用處于不活躍的狀態(tài),并且不會(huì)接受用戶的輸入。這個(gè)事件僅工作在 iOS 平臺(tái),在 Android 上沒有等價(jià)的事件。
paused - 應(yīng)用暫時(shí)對(duì)用戶不可見,雖然不接受用戶輸入,但是是在后臺(tái)運(yùn)行的。
resumed - 應(yīng)用可見,也響應(yīng)用戶的輸入。
suspending - 應(yīng)用暫時(shí)被掛起,在 iOS 上沒有這一事件。
UITableView 和 UICollectionView 相當(dāng)于 Flutter 中的什么?
在 iOS 中,你可能用 UITableView 或 UICollectionView 來展示一個(gè)列表。在 Flutter 中,你可以用 ListView 來達(dá)到相似的實(shí)現(xiàn)。在 iOS 中,你通過代理方法來確定行數(shù),每一個(gè) index path 的單元格,以及單元格的尺寸。
由于 Flutter 中 widget 的不可變特性,你需要向 ListView 傳遞一個(gè) widget 列表,F(xiàn)lutter 會(huì)確保滾動(dòng)是快速且流暢的。
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;
}}
我怎么知道列表的哪個(gè)元素被點(diǎn)擊了?
iOS 中,你通過 tableView:didSelectRowAtIndexPath: 代理方法來實(shí)現(xiàn)。在 Flutter 中,使用傳遞進(jìn)來的 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;
}}
我怎么動(dòng)態(tài)地更新 ListView?
在 iOS 中,你改變列表的數(shù)據(jù),并通過 reloadData() 方法來通知 table 或是 collection view。
在 Flutter 中,如果你想通過 setState() 方法來更新 widget 列表,你會(huì)很快發(fā)現(xiàn)你的數(shù)據(jù)展示并沒有變化。這是因?yàn)楫?dāng) setState() 被調(diào)用時(shí),F(xiàn)lutter 渲染引擎會(huì)去檢查 widget 樹來查看是否有什么地方被改變了。當(dāng)它得到你的 ListView 時(shí),它會(huì)使用一個(gè) == 判斷,并且發(fā)現(xiàn)兩個(gè) ListView 是相同的。沒有什么東西是變了的,因此更新不是必須的。
一個(gè)更新 ListView 的簡單方法是,在 setState() 中創(chuàng)建一個(gè)新的 list,并把舊 list 的數(shù)據(jù)拷貝給新的 list。雖然這樣很簡單,但當(dāng)數(shù)據(jù)集很大時(shí),并不推薦這樣做:
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');
});
},
);
}}
一個(gè)推薦的、高效的且有效的做法是,使用 ListView.Builder 來構(gòu)建列表。這個(gè)方法在你想要構(gòu)建動(dòng)態(tài)列表,或是列表擁有大量數(shù)據(jù)時(shí)會(huì)非常好用。
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');
});
},
);
}}
與創(chuàng)建一個(gè) “ListView” 不同,創(chuàng)建一個(gè) ListView.builder 接受兩個(gè)主要參數(shù):列表的初始長度,和一個(gè) ItemBuilder 方法。
ItemBuilder 方法和 cellForItemAt 代理方法非常類似,它接受一個(gè)位置,并且返回在這個(gè)位置上你希望渲染的 cell。
最后,也是最重要的,注意 onTap() 函數(shù)里并沒有重新創(chuàng)建一個(gè) list,而是 .add 了一個(gè) widget。
ScrollView 相當(dāng)于 Flutter 里的什么?
在 iOS 中,你給 view 包裹上 ScrollView 來允許用戶在需要時(shí)滾動(dòng)你的內(nèi)容。
在 Flutter 中,最簡單的方法是使用 ListView widget。它表現(xiàn)得既和 iOS 中的 ScrollView 一致,也能和 TableView 一致,因?yàn)槟憧梢越o它的 widget 做垂直排布:
@overrideWidget build(BuildContext context) {
return ListView(
children: <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);}
我怎么給 Flutter 的 widget 添加一個(gè)點(diǎn)擊監(jiān)聽者?
在 iOS 中,你給一個(gè) view 添加 GestureRecognizer 來處理點(diǎn)擊事件。在 Flutter 中,有兩種方法來添加點(diǎn)擊監(jiān)聽者:
如果 widget 本身支持事件監(jiān)測,直接傳遞給它一個(gè)函數(shù),并在這個(gè)函數(shù)里實(shí)現(xiàn)響應(yīng)方法。例如,RaisedButton widget 擁有一個(gè) RaisedButton 參數(shù):
@overrideWidget build(BuildContext context) {
return RaisedButton(
onPressed: () {
print("click");
},
child: Text("Button"),
);}
如果 widget 本身不支持事件監(jiān)測,則在外面包裹一個(gè) GestureDetector,并給它的 onTap 屬性傳遞一個(gè)函數(shù):
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 你可以監(jiān)聽更廣闊范圍內(nèi)的手勢,比如:
Tapping
onTapDown — 在特定位置輕觸手勢接觸了屏幕。
onTapUp — 在特定位置產(chǎn)生了一個(gè)輕觸手勢,并停止接觸屏幕。
onTap — 產(chǎn)生了一個(gè)輕觸手勢。
onTapCancel — 觸發(fā)了 onTapDown 但沒能觸發(fā) tap。
Double tapping
onDoubleTap — 用戶在同一個(gè)位置快速點(diǎn)擊了兩下屏幕。
Long pressing
onLongPress — 用戶在同一個(gè)位置長時(shí)間接觸屏幕。
Vertical dragging
onVerticalDragStart — 接觸了屏幕,并且可能會(huì)垂直移動(dòng)。
onVerticalDragUpdate — 接觸了屏幕,并繼續(xù)在垂直方向移動(dòng)。
onVerticalDragEnd — 之前接觸了屏幕并垂直移動(dòng),并在停止接觸屏幕前以某個(gè)垂直的速度移動(dòng)。
Horizontal dragging
onHorizontalDragStart — 接觸了屏幕,并且可能會(huì)水平移動(dòng)。
onHorizontalDragUpdate — 接觸了屏幕,并繼續(xù)在水平方向移動(dòng)。
onHorizontalDragEnd — 之前接觸屏幕并水平移動(dòng)的觸摸點(diǎn)與屏幕分離。
下面這個(gè)例子展示了一個(gè) GestureDetector 是如何在雙擊時(shí)旋轉(zhuǎn) Flutter 的 logo 的:
AnimationController controller;CurvedAnimation curve;@overridevoid 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 設(shè)置主題?
Flutter 實(shí)現(xiàn)了一套漂亮的 MD 組件,并且開箱可用。它接管了一大堆你需要的樣式和主題。
為了充分發(fā)揮你的 App 中 MD 組件的優(yōu)勢,聲明一個(gè)頂級(jí) widget,MaterialApp,用作你的 App 入口。MaterialApp 是一個(gè)便利組件,包含了許多 App 通常需要的 MD 風(fēng)格組件。它通過一個(gè) WidgetsApp 添加了 MD 功能來實(shí)現(xiàn)。
但是 Flutter 足夠地靈活和富有表現(xiàn)力來實(shí)現(xiàn)任何其他的設(shè)計(jì)語言。在 iOS 上,你可以用 Cupertino library 來制作遵守 Human Interface Guidelines 的界面。查看這些 widget 的集合,請(qǐng)參閱 Cupertino widgets gallery。
你也可以在你的 App 中使用 WidgetApp,它提供了許多相似的功能,但不如 MaterialApp 那樣強(qiáng)大。
對(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 設(shè)置自定義字體?
在 iOS 中,你在項(xiàng)目中引入任意的 ttf 文件,并在 info.plist 中設(shè)置引用。在 Flutter 中,在文件夾中放置字體文件,并在 pubspec.yaml 中引用它,就像添加圖片那樣。
fonts:
- family: MyCustomFont
fonts:
- asset: fonts/MyCustomFont.ttf
- style: italic
然后在你的 Text widget 中指定字體:
@overrideWidget 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 設(shè)置樣式?
除了字體以外,你也可以給 Text widget 的樣式元素設(shè)置自定義值。Text widget 接受一個(gè) TextStyle 對(duì)象,你可以指定許多參數(shù),比如:
color
decoration
decorationColor
decorationStyle
fontFamily
fontSize
fontStyle
fontWeight
hashCode
height
inherit
letterSpacing
textBaseline
wordSpacing
Flutter 中表單怎么工作?我怎么拿到用戶的輸入?
我們已經(jīng)提到 Flutter 使用不可變的 widget,并且狀態(tài)是分離的,你可能會(huì)好奇在這種情境下怎么處理用戶的輸入。在 iOS 中,你經(jīng)常在需要提交數(shù)據(jù)時(shí)查詢組件當(dāng)前的狀態(tài)或動(dòng)作,但這在 Flutter 中是怎么工作的呢?
在表單處理的實(shí)踐中,就像在 Flutter 中任何其他的地方一樣,要通過特定的 widgets。如果你有一個(gè) 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 相當(dāng)于什么?
在 Flutter 中,你可以輕易地通過向 Text widget 的裝飾構(gòu)造器參數(shù)重傳遞 InputDecoration 來展示“小提示”,或是占位符文字:
body: Center(
child: TextField(
decoration: InputDecoration(hintText: "This is a hint"),
),)
我怎么展示驗(yàn)證錯(cuò)誤信息?
就像展示“小提示”一樣,向 Text widget 的裝飾器構(gòu)造器參數(shù)中傳遞一個(gè) InputDecoration。
然而,你并不想在一開始就顯示錯(cuò)誤信息。相反,當(dāng)用戶輸入了驗(yàn)證信息,更新狀態(tài),并傳入一個(gè)新的 InputDecoration 對(duì)象:
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);
}}
我怎么和平臺(tái),以及平臺(tái)的原生代碼交互?
Flutter 的代碼并不直接在平臺(tái)之下運(yùn)行,相反,Dart 代碼構(gòu)建的 Flutter 應(yīng)用在設(shè)備上以原生的方式運(yùn)行,卻“側(cè)步躲開了”平臺(tái)提供的 SDK。這意味著,例如,你在 Dart 中發(fā)起一個(gè)網(wǎng)絡(luò)請(qǐng)求,它就直接在 Dart 的上下文中運(yùn)行。你并不會(huì)用上平常在 iOS 或 Android 上使用的原生 API。你的 Flutter 程序仍然被原生平臺(tái)的 ViewController 管理作一個(gè) view,但是你并不會(huì)直接訪問 ViewController 自身,或是原生框架。
但這并不意味著 Flutter 不能和原生 API,或任何你編寫的原生代碼交互。Flutter 提供了 platform channels ,來和管理你的 Flutter view 的 ViewController 通信和交互數(shù)據(jù)。平臺(tái)管道本質(zhì)上是一個(gè)異步通信機(jī)制,橋接了 Dart 代碼和宿主 ViewController,以及它運(yùn)行于的 iOS 框架。你可以用平臺(tái)管道來執(zhí)行一個(gè)原生的函數(shù),或者是從設(shè)備的傳感器中獲取數(shù)據(jù)。
除了直接使用平臺(tái)管道之外,你還可以使用一系列預(yù)先制作好的 plugins。例如,你可以直接使用插件來訪問相機(jī)膠卷或是設(shè)備的攝像頭,而不必編寫你自己的集成層代碼。你可以在 Pub 上找到插件,這是一個(gè) Dart 和 Flutter 的開源包倉庫。其中一些包可能會(huì)支持集成 iOS 或 Android,或兩者均可。
如果你在 Pub 上找不到符合你需求的插件,你可以自己編寫 ,并且發(fā)布在 Pub 上。
我怎么訪問 GPS 傳感器?
使用 location 社區(qū)插件。
我怎么訪問攝像頭?
image_picker 在訪問攝像頭時(shí)非常常用。
我怎么登錄 Facebook?
登錄 Facebook 可以使用 flutter_facebook_login 社區(qū)插件。
我怎么使用 Firebase 特性?
大多數(shù) Firebase 特性被 first party plugins 包含了。這些第一方插件由 Flutter 團(tuán)隊(duì)維護(hù):
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 的第三方插件。
我怎創(chuàng)建自己的原生集成層?
如果有一些 Flutter 和社區(qū)插件遺漏的平臺(tái)相關(guān)的特性,可以根據(jù) developing packages and plugins 頁面構(gòu)建自己的插件。
Flutter 的插件結(jié)構(gòu),簡要來說,就像 Android 中的 Event bus。你發(fā)送一個(gè)消息,并讓接受者處理并反饋結(jié)果給你。在這種情況下,接受者就是在 Android 或 iOS 上的原生代碼。
我怎么在 Flutter 中訪問 UserDefaults?
在 iOS 中,你可以使用屬性列表來存儲(chǔ)鍵值對(duì)的集合,即我們熟悉的 UserDefaults。
在 Flutter 中,可以使用 Shared Preferences plugin 來達(dá)到相似的功能。它包裹了 UserDefaluts 以及 Android 上等價(jià)的 SharedPreferences 的功能。
CoreData 相當(dāng)于 Flutter 中的什么?
在 iOS 中,你通過 CoreData 來存儲(chǔ)結(jié)構(gòu)化的數(shù)據(jù)。這是一個(gè) SQL 數(shù)據(jù)庫的上層封裝,讓查詢和關(guān)聯(lián)模型變得更加簡單。
在 Flutter 中,使用 SQFlite 插件來實(shí)現(xiàn)這個(gè)功能。
我怎么推送通知?
在 iOS 中,你需要向蘋果開發(fā)者平臺(tái)中注冊(cè)來允許推送通知。
在 Flutter 中,使用 firebase_messaging 插件來實(shí)現(xiàn)這一功能。
