導(dǎo)引
本文原文在這里:Flutter for Android developers
本文屬于半翻譯半筆記注解,按照自己的描述整合了原文,如若讀者讀過(guò)原文會(huì)發(fā)現(xiàn)有些許不一樣不要驚訝,但仍然完整表達(dá)了原文的意思,文中斜體加粗部分是我自己的注解。
前言
Android的知識(shí)和技能對(duì)Flutter開(kāi)發(fā)很有價(jià)值,因?yàn)镕lutter依賴于移動(dòng)系統(tǒng)的眾多功能和配置。Flutter構(gòu)建App的UI的一種新方式,它有一個(gè)插件系統(tǒng)去連接Android/IOS原生系統(tǒng)來(lái)完成非UI的任務(wù),如果你精通Android,你可以不用學(xué)習(xí)任何東西直接使用Flutter(當(dāng)然需要看完這篇指南)
Views
在Flutter中什么東西相當(dāng)于View
在Android里,View是所有布局的基礎(chǔ),所有的控件都繼承于View。在Flutter中,大致相當(dāng)于View的是Widget。Widget不能和View劃上等號(hào),當(dāng)你熟悉了Flutter的開(kāi)發(fā)后你可以將它們視為你聲明和構(gòu)建UI的方式。
Widget與View主要有這幾點(diǎn)不同:
- Widgets一經(jīng)創(chuàng)建,是不可改變的一直存活到他們必須改變?yōu)橹?。?dāng)Widgets或者他們的狀態(tài)發(fā)生改變,F(xiàn)lutter會(huì)創(chuàng)建新的Widget樹(shù)的實(shí)例,而View則是繪制完成后不會(huì)再重繪直到
invalidate被調(diào)用。 - Widgets是輕量級(jí)的,部分原因在于他們的不變性。他們不能直接繪制任何東西,而是對(duì)UI及其語(yǔ)義的描述,會(huì)通過(guò)"inflated"生成真實(shí)的視圖對(duì)象中去。
Flutter包括 Material Components library.這些Widgets實(shí)現(xiàn)了Material Design guidelines.Material Design 是一個(gè)包括iOS在內(nèi)的所有平臺(tái)進(jìn)行優(yōu)化的靈活的設(shè)計(jì)。
但是Flutter的靈活性和表現(xiàn)力足以實(shí)現(xiàn)任何設(shè)計(jì)語(yǔ)言。例如,在iOS上,您可以使用 Cupertino widgets生成一個(gè)類似于 Apple’s iOS design language的界面。
怎樣更新Widgets
在Android中可以直接改變View,但是在Flutter里,Widgets 是不可以直接更新的,取而代之的你需要去改變Widgets的狀態(tài)。
這就是有狀態(tài)和無(wú)狀態(tài)Widgets的概念來(lái)源。 StatelessWidget聽(tīng)起來(lái)就像是一個(gè)沒(méi)有狀態(tài)信息的Widget.
當(dāng)你描述的UI里有一些部分不依賴于對(duì)象中的配置信息時(shí), StatelessWidget非常有用。
例如,在Android中,這類似于放置Logo的ImageView。Logo在運(yùn)行時(shí)不會(huì)更改,所以在Flutter中可以使用 StatelessWidget.
如果你想根據(jù)HTTP請(qǐng)求或用戶交互后接收到的數(shù)據(jù)動(dòng)態(tài)更改UI,那么你必須使用StatefulWidget,并告訴Flutter widget的狀態(tài)已經(jīng)更新,以便它可以更新該widget。
這里需要注意的重要一點(diǎn)是,無(wú)狀態(tài)和有狀態(tài)widget的行為都是相同的,每一幀都會(huì)重建,區(qū)別在于StatefulWidget有一個(gè)State對(duì)象,它跨越幀時(shí)序,并且恢復(fù)StatefulWidget的狀態(tài)。
如果你有疑問(wèn),那么請(qǐng)始終記住這條規(guī)則:如果widget發(fā)生更改(例如,由于用戶交互),那么它是有狀態(tài)的。但是,如果widget對(duì)更改作出反應(yīng),但它的父widget仍然可以是無(wú)狀態(tài)的,如果它不需要更改作出反應(yīng)。
下面的例子使用了一個(gè)StatelessWidget.一個(gè)常見(jiàn)的StatelessWidget是Text.如果你去看Text的實(shí)現(xiàn)你會(huì)發(fā)現(xiàn)它是Statelesswidget的子類。
Text(
'I like Flutter!',
style: TextStyle(fontWeight: FontWeight.bold),
);
如你所見(jiàn),Text沒(méi)有與之關(guān)聯(lián)的狀態(tài),它只呈現(xiàn)在其構(gòu)造函數(shù)中傳遞的內(nèi)容。
但是,如果您想要?jiǎng)討B(tài)地更改“I Like Flutter”,例如在單擊FloatingActionButton時(shí)?
要實(shí)現(xiàn)這一點(diǎn),請(qǐng)將Text包裝在一個(gè)有StatefulWidget中,并在用戶單擊按鈕時(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> {
// 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),
),
);
}
}
怎么布局以及XML在哪里
在Android中,使用XML編寫布局,但在Flutter中,使用控件樹(shù)編寫布局。
以下示例顯示如何使用一個(gè)padding部件:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: Center(
child: MaterialButton(
onPressed: () {},
child: Text('Hello'),
padding: EdgeInsets.only(left: 10.0, right: 10.0),
),
),
);
}
你可以查看Flutter用作布局的控件類型:widget catalog
怎么添加或者刪除一個(gè)布局中的控件
在Android中,可以對(duì)父布局調(diào)用addChild()或removeChild()來(lái)動(dòng)態(tài)添加或刪除子布局。在Flutter中,因?yàn)榭丶遣豢勺兊模詻](méi)有與addChild()直接等價(jià)的方式。相反,可以將一個(gè)函數(shù)傳遞給返回小部件的父控件,并用一個(gè)boolean控制子控件的創(chuàng)建。
例如這里,你可以點(diǎn)擊FloatingActionButton來(lái)切換兩個(gè)控件:
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> {
// Default value for toggle
bool toggle = true;
void _toggle() {
setState(() {
toggle = !toggle;
});
}
_getToggleChild() {
if (toggle) {
return Text('Toggle One');
} else {
return MaterialButton(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),
),
);
}
}
怎么給控件添加動(dòng)畫呢
在Android中,可以使用XML創(chuàng)建動(dòng)畫,也可以在View上調(diào)用animate()方法。在Flutter中,實(shí)現(xiàn)動(dòng)畫的方式是使用animated library中的animated widget來(lái)包裝控件。
在Flutter中,使用AnimationController它是一個(gè)Animation<double>,可以實(shí)現(xiàn)暫停、搜索、停止、反轉(zhuǎn)的動(dòng)畫。它需要一個(gè)Ticker作為vsync的信號(hào)發(fā)生器,并且在每一幀發(fā)出一個(gè)0-1之間的線性插值。你可以創(chuàng)建多個(gè)Animation使用同一個(gè)AnimationController
你可以使用CurvedAnimation去實(shí)現(xiàn)插值曲線的動(dòng)畫,從這個(gè)意義上來(lái)說(shuō),控制器是實(shí)現(xiàn)動(dòng)畫變換的主要部分,而使用CurvedAnimation可以實(shí)現(xiàn)曲線插值替換默認(rèn)的線性運(yùn)動(dòng),就像Widget一樣,F(xiàn)lutter的動(dòng)畫也可以互相組合。
當(dāng)創(chuàng)建控件樹(shù)的時(shí)候可以將Animation賦給控件的動(dòng)畫屬性,例如實(shí)現(xiàn)透明的FadeTransition,同時(shí)告訴控制器開(kāi)始動(dòng)畫。
下面這個(gè)例子使用FadeTransition是實(shí)現(xiàn)了漸進(jìn)顯示Logo的一個(gè)效果:
import 'package:flutter/material.dart';
void main() {
runApp(FadeAppTest());
}
class FadeAppTest 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() {
super.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();
},
),
);
}
}
更多信息, 參考 Animation & Motion widgets, Animations tutorial, 和 Animations overview.
怎么使用Canvas來(lái)繪制?
在Android中,您可以使用Canvas和Drawable在屏幕上繪制圖像和形狀。Flutter也有類似的Canvas API,因?yàn)樗谙嗤牡讓愉秩疽?,Skia。因此,對(duì)于Android開(kāi)發(fā)人員來(lái)說(shuō),在Flutter中繪制到Canvas是一項(xiàng)非常熟悉的工作。
Flutter有兩個(gè)類來(lái)幫助你繪制Canvas:CustomPaint和CustomPainter,后者可以實(shí)現(xiàn)你的算法去繪制Canvas.
想了解如何使用Flutter實(shí)現(xiàn)一個(gè)簽名繪畫區(qū),可以參考Collin的回答: StackOverflow.
這里需要注意的是Collin原回答不能實(shí)現(xiàn)繪畫,需要使用下面的代碼需要替換掉源碼中這一行,修改CustomPaint的構(gòu)造參數(shù)
child: CustomPaint(painter: SignaturePainter(_points), size: Size.infinite),
import 'package:flutter/material.dart';
class SignaturePainter extends CustomPainter {
SignaturePainter(this.points);
final List<Offset> points;
void paint(Canvas canvas, Size size) {
Paint paint = new 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() => new SignatureState();
}
class SignatureState extends State<Signature> {
List<Offset> _points = <Offset>[];
Widget build(BuildContext context) {
return new Stack(
children: [
GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
RenderBox referenceBox = context.findRenderObject();
Offset localPosition =
referenceBox.globalToLocal(details.globalPosition);
setState(() {
_points = new List.from(_points)..add(localPosition);
});
},
onPanEnd: (DragEndDetails details) => _points.add(null),
),
CustomPaint(painter: new SignaturePainter(_points)),
],
);
}
}
class DemoApp extends StatelessWidget {
Widget build(BuildContext context) => new Scaffold(body: new Signature());
}
void main() => runApp(new MaterialApp(home: new DemoApp()));
怎么創(chuàng)建自定義控件
在Android中,通過(guò)繼承于View或者繼承于一個(gè)已經(jīng)存在的View,通過(guò)覆蓋或者實(shí)現(xiàn)接口來(lái)完成期望的效果。
在Flutter中,通過(guò)組合更小控件(而不是繼承)來(lái)構(gòu)建一個(gè)自定義的控件。這有點(diǎn)類似于在Android中實(shí)現(xiàn)自定義ViewGroup,其中所有構(gòu)建塊都已經(jīng)存在,但是你提供了不同的行為—例如,自定義布局邏輯。
例如,如何構(gòu)建一個(gè)帶有標(biāo)簽的CustomButton呢,通過(guò)組合RaisedButton包含一個(gè)lablel更好,而不是通過(guò)繼承RaisedButton。
class CustomButton extends StatelessWidget {
final String label;
CustomButton(this.label);
@override
Widget build(BuildContext context) {
return RaisedButton(onPressed: () {}, child: Text(label));
}
}
然后可以使用CustomButton就像你使用其他的Flutter的控件一樣:
@override
Widget build(BuildContext context) {
return Center(
child: CustomButton("Hello"),
);
}
Intents
Flutter中與Intent相對(duì)應(yīng)的是什么?
在Android中,Intent有兩個(gè)主要用途:用于activity間的跳轉(zhuǎn)、用于組件間的通信。而在Flutter中,沒(méi)有Intent這個(gè)概念,雖然你依然可以通過(guò)本地集成(native integrations(使用插件))來(lái)啟動(dòng)Intent。
Route可以看做是應(yīng)用屏幕或者頁(yè)面的抽象,而Navigator是一個(gè)管理Route的控件??梢源致缘膶?code>Route看成Activity,但是它們含義不同。Navigator通過(guò)push和pop操作Route在頁(yè)面之間切換,Navigator就像是一個(gè)棧,push表示切換到新的頁(yè)面,pop表示返回。
在Android中,需要在AndroidManifest.xml中聲明activity,而在Flutter中,你有以下頁(yè)面切換選擇:
- 指定一個(gè)包含所有Route名字的
Map(MaterialApp) - 直接切換到Route(WidgetApp)
下面的實(shí)例里建立了一個(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'),
},
));
}
通過(guò)壓入對(duì)應(yīng)的名字到Navigator來(lái)切換到對(duì)應(yīng)的Route:
Navigator.of(context).pushNamed('/b');
另一種Intent常見(jiàn)的使用場(chǎng)景是調(diào)用外部組件比如相機(jī)、文件選擇器,對(duì)于這種情況你需要?jiǎng)?chuàng)建一個(gè)原生平臺(tái)集成(或者使用現(xiàn)成插件 existing plugin)
關(guān)于構(gòu)建原生平臺(tái)集成,請(qǐng)查看 Developing Packages and Plugins.
Flutter 如何處理來(lái)自外部的Intent?
Flutter可以通過(guò)直接訪問(wèn)Android layer來(lái)處理來(lái)自Android的Intent,或者請(qǐng)求共享數(shù)據(jù)。
下面的實(shí)例注冊(cè)了一個(gè)文本共享的Intent過(guò)濾器在運(yùn)行我們Flutter代碼的原生activity上,所以其他應(yīng)用就能共享文本給我們的Flutter 應(yīng)用。
基本流程就是先在Android 原生層(即Activity)先處理這些共享數(shù)據(jù),然后等待Flutter請(qǐng)求,通過(guò)MethodChannel可以將這些數(shù)據(jù)提供給Flutter.
首先,在AndroidManifest.xml中注冊(cè)Intent過(guò)濾器:
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- ... -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
接著在MainActivity中處理Intent,從Intent中獲取共享的數(shù)據(jù),然后先暫時(shí)持有這些數(shù)據(jù),當(dāng)Flutter準(zhǔn)備好處理時(shí),它會(huì)通過(guò)平臺(tái)通道(platform channel)進(jìn)行請(qǐng)求,然后從原生Android層發(fā)送這些數(shù)據(jù)。
package com.example.shared;
import android.content.Intent;
import android.os.Bundle;
import java.nio.ByteBuffer;
import io.flutter.app.FlutterActivity;
import io.flutter.plugin.common.ActivityLifecycleListener;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugins.GeneratedPluginRegistrant;
public class MainActivity extends FlutterActivity {
private String sharedText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
Intent intent = getIntent();
String action = intent.getAction();
String type = intent.getType();
if (Intent.ACTION_SEND.equals(action) && type != null) {
if ("text/plain".equals(type)) {
handleSendText(intent); // Handle text being sent
}
}
new MethodChannel(getFlutterView(), "app.channel.shared.data").setMethodCallHandler(
new MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
if (call.method.contentEquals("getSharedText")) {
result.success(sharedText);
sharedText = null;
}
}
});
}
void handleSendText(Intent intent) {
sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
}
}
最后,當(dāng) Flutter的控件渲染完成時(shí)請(qǐng)求數(shù)據(jù):
import 'package:flutter/material.dart';
import 'package:flutter/services.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 Shared App Handler',
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> {
static const platform = const MethodChannel('app.channel.shared.data');
String dataShared = "No data";
@override
void initState() {
super.initState();
getSharedText();
}
@override
Widget build(BuildContext context) {
return Scaffold(body: Center(child: Text(dataShared)));
}
getSharedText() async {
var sharedData = await platform.invokeMethod("getSharedText");
if (sharedData != null) {
setState(() {
dataShared = sharedData;
});
}
}
}
與startActivityForResult()等價(jià)的是什么?
Flutter中Navigator除了用于處理Route,也可以獲得已入棧Route的返回結(jié)果。只需要等push()返回的Future執(zhí)行完即可。
Map coordinates = await Navigator.of(context).pushNamed('/location');
然后,在定位Route里,當(dāng)用戶選擇完位置后,你就可以通過(guò)pop獲取結(jié)果了:
Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});
異步UI
runOnUiThread 在Flutter中等價(jià)于什么
Dart是單線程執(zhí)行模型,支持Isolates(在另一個(gè)線程上運(yùn)行Dart代碼的方式)、事件循環(huán)和異步編程。 除非您啟動(dòng)一個(gè)Isolate,否則您的Dart代碼將在主UI線程中運(yùn)行,并由事件循環(huán)驅(qū)動(dòng)(和JavaScript一樣)。Flutter的事件驅(qū)動(dòng)循環(huán)等價(jià)于Android的主線程/UI線程的Looper。
Dart雖然是單線程模型但是并不意味著你需要用一種阻塞式的方式來(lái)執(zhí)行其他所有的代碼造成UI線程被凍結(jié)。不像Android,你需要保持主線程一直空閑不被阻塞,在Flutter中,你可以使用Dart語(yǔ)言提供的異步工具,如async/await來(lái)執(zhí)行異步操作。如果你用過(guò)C#,Javascript,或者你使用過(guò)Kotlin的協(xié)程的話,你可能會(huì)熟悉async/await這個(gè)范例。
例如,你可以使用async/await來(lái)執(zhí)行網(wǎng)絡(luò)請(qǐng)求代碼同時(shí)不會(huì)引起UI線程掛起,讓Dart完成繁重的操作:
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
一旦等到網(wǎng)絡(luò)請(qǐng)求結(jié)束,就會(huì)調(diào)用setState()方法以更新UI,接著觸發(fā)控件子樹(shù)的重建并更新數(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);
});
}
}
上述代碼需要引入http.dart(下文中也有提到,這里是為了方便順序查看本文時(shí)驗(yàn)證demo所以先提到),可以訪問(wèn)http 0.12.0+2(當(dāng)前版本)查看對(duì)應(yīng)版本號(hào)在pubspec.yaml中引入:
dependencies:
http: ^0.12.0+2
如何將任務(wù)放入后臺(tái)線程?
在Android中,訪問(wèn)網(wǎng)絡(luò)資源的一種典型方式就是在后臺(tái)線程中去執(zhí)行任務(wù),從而避免在主線程中執(zhí)行造成ANR。例如,你可以使用AsyncTask, LiveData, IntentService, JobScheduler或者RxJava Scheduler進(jìn)行后臺(tái)處理。
由于Flutter是一個(gè)單線程模型運(yùn)行著一個(gè)事件循環(huán)(像Node.js一樣),你不需要替線程管理和創(chuàng)建后臺(tái)線程而操心。如果你進(jìn)行I/O操作,例如硬盤訪問(wèn)或者網(wǎng)絡(luò)請(qǐng)求,你可以簡(jiǎn)單的使用async/await來(lái)完成所有的操作,反過(guò)來(lái)說(shuō),如果你需要執(zhí)行計(jì)算密集型的任務(wù),捏可以把它放入一個(gè)Isolate中從而避免阻塞事件循環(huán),就像Android中,你不會(huì)在主線程執(zhí)行任何除UI更新相關(guān)的其他任何類型的任務(wù)。
I/O類的任務(wù),只需要聲明函數(shù)是一個(gè)async函數(shù),然后在函數(shù)中,用await修飾在耗時(shí)任務(wù)之前:
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
這就是網(wǎng)絡(luò)請(qǐng)求、數(shù)據(jù)庫(kù)操作等的典型做法,它們都是I/O操作。
在Android中,當(dāng)你繼承AsyncTask,那么通常你需要重寫三個(gè)方法,onPreExecute()、doInBackground()和onPostExecute(),而在Flutter中則沒(méi)有與之等價(jià)的方式,因?yàn)?code>await修飾的耗時(shí)任務(wù)函數(shù),剩余的工作都交給Dart的事件循環(huán)去處理。
然而當(dāng)你處理大量數(shù)據(jù)時(shí),你的UI會(huì)掛起,所以在Flutter中使用Isolate來(lái)充分利用多核CPU去執(zhí)行耗時(shí)的計(jì)算密集型任務(wù)。
Isolate是獨(dú)立的執(zhí)行線程,它不會(huì)與主線程共享內(nèi)存堆,這就意味著你不能在Isolate中直接訪問(wèn)主線程的變量,或者調(diào)用setState更新UI。不像Android中的線程,Isolate是名副其實(shí)的隔離區(qū),不能共享內(nèi)存(比如不能以靜態(tài)字段的方式共享等)。
下面示例展示了一個(gè)簡(jiǎn)單的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 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執(zhí)行在它擁有的獨(dú)立執(zhí)行線程中。在Isolate中,可以執(zhí)行CPU密集型任務(wù)(比如解析超大的Json數(shù)據(jù)),或者執(zhí)行計(jì)算密集型的數(shù)學(xué)運(yùn)算,比如加密或者信號(hào)處理等。
你可以跑一下下面這個(gè)完整的示例:
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中與OkHttp等價(jià)的是什么?
在Flutter中使用流行的 httppackage調(diào)用一個(gè)網(wǎng)絡(luò)請(qǐng)求是很簡(jiǎn)單的。
雖然在http包中沒(méi)有OkHttp所有的功能,它抽象了大多你通常需要自己實(shí)現(xiàn)的網(wǎng)絡(luò)功能,使得網(wǎng)絡(luò)請(qǐng)求更加簡(jiǎn)單。
要使用http包,需要在pubspec.yaml中添加如下依賴:
dependencies:
...
http: ^0.11.3+16
發(fā)起一個(gè)網(wǎng)絡(luò)請(qǐng)求可以使用await調(diào)用一個(gè)async函數(shù)例如http.get():
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
[...]
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
}
如何顯示耗時(shí)任務(wù)的進(jìn)度?
在Android中,當(dāng)在后臺(tái)線程中執(zhí)行一個(gè)耗時(shí)任務(wù)時(shí),通常顯示一個(gè)ProgressBar在UI上。
在Flutter中,則是使用ProgressIndicator控件。通過(guò)boolean標(biāo)記位來(lái)控制何時(shí)開(kāi)始渲染,然后在耗時(shí)任務(wù)開(kāi)始之前顯示它,并在任務(wù)結(jié)束時(shí)隱藏掉。
下面示例中,build函數(shù)分割成了三個(gè)不同的子函數(shù),如果showLoadingDialog()返回true(當(dāng)widgets.length == 0),則渲染ProgressIndicator,否則就將網(wǎng)絡(luò)請(qǐng)求返回的數(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();
}
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);
});
}
}
項(xiàng)目結(jié)構(gòu)&資源文件
在哪里存放不同分辨率的圖片文件?
在Android中,resources與assets是兩個(gè)獨(dú)立的文件夾,而在Flutter中,只有assets,所有放在Android的res/drawable-*文件夾中的文件全都放在Flutter中的assets文件夾中。
Flutter跟ios一樣遵循簡(jiǎn)單的基于密度(density-base)的格式,assets包含1.0x、2.0x、3.0x或者更高乘數(shù),Flutter中并沒(méi)有dp這一說(shuō),而是使用與設(shè)備無(wú)關(guān)的邏輯像素,在devicePixelRatio 中描述了單個(gè)邏輯像素與物理像素的比例。
對(duì)應(yīng)于Android密度清單如下:
| Android density qualifier | Flutter pixel ratio |
|---|---|
| ldpi | 0.75x |
| mdpi | 1.0x |
| hdpi | 1.5x |
| xhdpi | 2.0x |
| xxhdpi | 3.0x |
| xxxhdpi | 4.0x |
Assets可以存在任意的文件夾,F(xiàn)lutter沒(méi)有預(yù)定義的文件夾結(jié)構(gòu)。需要在pubspec.yaml聲明assets對(duì)應(yīng)的物理位置,F(xiàn)lutter就能正確的讀取到。
注意,在Flutter 1.0 beta 2之前,F(xiàn)lutter定義的assets無(wú)法被原生端訪問(wèn),反之亦然,原生定義的assets和resources也不能被Flutter訪問(wèn),因?yàn)樗麄兾挥诓煌奈募A中。
而從Flutter 1.0 beta2開(kāi)始,assets存儲(chǔ)于本地層的assets文件夾中,且可以被本地層通過(guò)AssetManager訪問(wèn),但是beta2版本中,F(xiàn)lutter依然不能訪問(wèn)本地層的resources和assets:
val flutterAssetStream = assetManager.open("flutter_assets/assets/my_flutter_asset.png")
例如,要將名為my_icon.png的新圖像資源添加到我們的Flutter項(xiàng)目中,比方說(shuō),把它放到一個(gè)我們?nèi)我饷麨?code>images的文件夾中,你需要把1.0x圖片資源放在該文件的根目錄,然后把其他放在對(duì)應(yīng)乘數(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
接下來(lái)你需要去聲明這些圖片在pubspec.yaml文件中:
assets:
- images/my_icon.jpeg
你可以用AssetImage去訪問(wèn)這些圖片:
return AssetImage("images/a_dot_burr.jpeg");
或者直接可以在Image控件中寫入路徑:
@override
Widget build(BuildContext context) {
return Image.asset("images/my_image.png");
}
在哪里存放strings字符串資源,怎么實(shí)現(xiàn)本地化?
目前Flutter沒(méi)有像strings這樣的專用資源系統(tǒng),所以目前來(lái)說(shuō),最佳的方法就是把你的字符串以static字段的方式存放在一個(gè)類中,然后從這個(gè)類去訪問(wèn)它們。如下示例:
class Strings {
static String welcomeMessage = "Welcome To Flutter";
}
然后在代碼中你可以這樣調(diào)用你的字符串:
Text(Strings.welcomeMessage)
Flutter對(duì)Android上的輔助功能提供了基本支持,目前工作正在進(jìn)行中。官方鼓勵(lì)Flutter開(kāi)發(fā)者使用intl package來(lái)實(shí)現(xiàn)國(guó)際化和本地化。
與Gradle文件等價(jià)的是什么?該怎么添加依賴?
在Android中,使用Gradle構(gòu)建腳本添加依賴。在Flutter中,使用Dart自己的構(gòu)建系統(tǒng)和Pub包管理器。該工具會(huì)把原生的Android和IOS的包裝應(yīng)用的構(gòu)建過(guò)程委托給各自的構(gòu)建系統(tǒng)。
gradle文件在Flutter工程目錄的android文件夾下,只有需要針對(duì)單個(gè)平臺(tái)添加本地依賴時(shí)才使用gradle,通常來(lái)說(shuō)在Flutter中直接在pubspec.yaml聲明外部依賴即可。找Flutter包的好地方就是 Pub.
Activities和fragments
Flutter中與activities和fragments相對(duì)應(yīng)的是啥?
在Android中,activity是用戶可以操作的一個(gè)焦點(diǎn)物,而Fragment則代表著一個(gè)行為表現(xiàn)或者是UI的一部分。Fragment可以模塊化你的代碼,可以給大屏設(shè)備組合出復(fù)雜的UI部分,幫助你拓展你的應(yīng)用UI.在Flutter中,這兩個(gè)概念都落在了Widget的范疇里。
要了解有關(guān)使用Activity和Fragment構(gòu)建UI的更多信息,可以看社區(qū)里的文章,Flutter For Android Developers : How to design an Activity UI in Flutter.
正如在Intent部分所提到的,在Flutter中,Widget就代表著屏幕,因?yàn)樵贔lutter中萬(wàn)物皆Widget。我們使用Navigator來(lái)切換Route,這代表著不同屏幕或頁(yè)面,亦或是不同狀態(tài)或是渲染相同的數(shù)據(jù)。
如何監(jiān)聽(tīng)Android中activity的生命周期事件
在Android中, 通過(guò)重寫Activity中的方法來(lái)捕獲生命周期函數(shù)的回調(diào),或者實(shí)現(xiàn)ActivityLifecycleCallbacks在Application上。而在Flutter中,沒(méi)有這個(gè)概念,但你可以通過(guò)hookWidgetsBinding并且監(jiān)聽(tīng)didChangeAppLifecycleState()的變化事件來(lái)代替監(jiān)聽(tīng)生命周期。
可觀察的生命周期事件有:
-
inactive— 應(yīng)用處于非活動(dòng)狀態(tài),不接受用戶輸入。這個(gè)事件只在IOS中有效,因?yàn)樵贏ndroid中沒(méi)有與此等價(jià)的事件。 -
paused— 當(dāng)前應(yīng)用對(duì)用戶不可見(jiàn),不響應(yīng)用戶輸入,且運(yùn)行在后臺(tái)。等同于Android中的onPause。(如果等同于Android中onPause的話這里應(yīng)該是可見(jiàn)但是不在頂層可見(jiàn)) -
resumed— 此時(shí)應(yīng)用可見(jiàn)且響應(yīng)用戶輸入。等同于Android中的onPostResume() -
suspending— 此時(shí)應(yīng)用暫時(shí)掛起。等同于Android中的onStop;不會(huì)觸發(fā)IOS上的事件,IOS中沒(méi)有等價(jià)的事件。
想要了解更多關(guān)于這些狀態(tài)的信息,可以參見(jiàn) AppLifecycleStatus documentation.
正如你注意到的,只有少數(shù)的Activity生命周期可用,而FlutterActivity確實(shí)在內(nèi)部捕獲了幾乎所有的Activity生命周期事件然后把它們發(fā)送到Flutter的引擎中去,然而很多事件都替你屏蔽了。Flutter會(huì)替你管理引擎的啟動(dòng)和關(guān)閉,所以大多數(shù)情況下我們幾乎不需要去觀察activity的生命周期在Flutter中。如果你一定要在生命周期里獲取或者釋放本地資源,你至少可以在原生端做這些事。
以下示例描述了如何監(jiān)聽(tīng)Activity中的生命周期事件:
import 'package:flutter/widgets.dart';
class LifecycleWatcher extends StatefulWidget {
@override
_LifecycleWatcherState createState() => _LifecycleWatcherState();
}
class _LifecycleWatcherState extends State<LifecycleWatcher> with WidgetsBindingObserver {
AppLifecycleState _lastLifecycleState;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
setState(() {
_lastLifecycleState = state;
});
}
@override
Widget build(BuildContext context) {
if (_lastLifecycleState == null)
return Text('This widget has not observed any lifecycle changes.', textDirection: TextDirection.ltr);
return Text('The most recent lifecycle state this widget observed was: $_lastLifecycleState.',
textDirection: TextDirection.ltr);
}
}
void main() {
runApp(Center(child: LifecycleWatcher()));
}
布局
與LinearLayout等價(jià)的是什么
在Android中,LinearLayout用于橫向和縱向布局控件,而在Flutter中則是使用Row或Column控件來(lái)實(shí)現(xiàn)相同的行為。
你可能注意到兩段示例代碼實(shí)現(xiàn)相同,僅僅Row和Column控件不同。子控件是完全相同的,這樣的特性可以被用來(lái)開(kāi)發(fā)具有相同子控件但是隨著時(shí)間推移會(huì)變化的富布局。
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Column One'),
Text('Column Two'),
Text('Column Three'),
Text('Column Four'),
],
);
}
如果想了解更多關(guān)于構(gòu)建線性布局的內(nèi)容,可以參見(jiàn)社區(qū)文章:Flutter For Android Developers : How to design LinearLayout in Flutter?.
與RelativeLayout等價(jià)的是什么
使用相對(duì)布局可以將你的子控件按照彼此的位置相互放置。在Flutter中,有很多方法可以達(dá)到同樣的效果。
你可以組合Column,Row和Stack控件來(lái)實(shí)現(xiàn)與相對(duì)布局一樣的效果。你可以在widgets的構(gòu)造函數(shù)中指定子控件相對(duì)于父布局的規(guī)則。
有關(guān)在FLutter中構(gòu)建RelativeLayout的好例子,可以參考Collin's的回答: StackOverflow.
與ScrollView等價(jià)的是什么
在Android中,當(dāng)你的內(nèi)容超出設(shè)備屏幕時(shí),使用ScrollView來(lái)布局。
在Flutter中,最簡(jiǎn)單的實(shí)現(xiàn)方式就是使用ListView控件。這與Android看起來(lái)仿佛有點(diǎn)矯枉過(guò)正,但是在Flutter中一個(gè)ListView即是Android中的ScrollView也是ListView。
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
在Flutter中如何處理橫屏反轉(zhuǎn)
只要在AndroidManifest.xml中包含下列內(nèi)容FlutterView就會(huì)處理這些配置變化:
android:configChanges="orientation|screenSize"
手勢(shì)檢測(cè)和觸摸事件處理
Flutter中怎么給一個(gè)空間添加點(diǎn)擊事件
在Android中你可以通過(guò)setOnClickListener來(lái)給一個(gè)View,例如Button添加點(diǎn)擊事件。
在Flutter中,有下面兩種方式添加觸摸事件監(jiān)聽(tīng):
- 如果控件支持事件檢測(cè),那可以給它傳入一個(gè)函數(shù)去處理這個(gè)事件。比如,
RaisedButton包含一個(gè)onPressd參數(shù):
@override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () {
print("click");
},
child: Text("Button"));
}
- 如果空間不支持事件檢測(cè),可以把它包裝在
GestureDetector中通過(guò)onTap參數(shù)傳遞一個(gè)監(jiān)聽(tīng)函數(shù)。
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
child: FlutterLogo(
size: 200.0,
),
onTap: () {
print("tap");
},
),
));
}
}
如何處理控件的其他手勢(shì)?
使用GestureDetector可以監(jiān)聽(tīng)廣泛的手勢(shì),例如:
- 列表第一項(xiàng)
- 列表第二項(xiàng)
- 列表第二項(xiàng)
- 列表第二項(xiàng)
- 單擊(Tab)
-
onTabDown: 觸發(fā)單擊事件的指針已經(jīng)與屏幕上特定點(diǎn)聯(lián)系在一起 -
onTapUp: 觸發(fā)單擊事件的指針停止與屏幕在特定點(diǎn)上的聯(lián)系 -
onTap: 單擊事件發(fā)生 -
onTapCancel: 觸發(fā)時(shí)會(huì)導(dǎo)致之前觸發(fā)onTabDown的指針無(wú)法形成單擊事件
-
- 雙擊(Double Tab)
-
onDoubleTab: 用戶在屏幕的同一個(gè)點(diǎn)上連續(xù)快速點(diǎn)擊了兩次
-
- 長(zhǎng)按Long Press
-
onLongPress: 指針和屏幕上同一個(gè)位置發(fā)生一段較長(zhǎng)事件的聯(lián)系
-
- 垂直拖動(dòng)(Vertical drag)
-
onVerticalDragStart: 指針已經(jīng)和屏幕聯(lián)系,并且可能開(kāi)始垂直移動(dòng)。 -
onVerticalDragUpdate: 正在和屏幕聯(lián)系的指針已經(jīng)開(kāi)始在垂直方向上進(jìn)行移動(dòng)。 -
onVerticalDragEnd: 之前與屏幕進(jìn)行聯(lián)系且在垂直方向移動(dòng)的指針,現(xiàn)在已經(jīng)不需要再與屏幕聯(lián)系了,并且在停止聯(lián)系的瞬間,指針依然以一定的速度移動(dòng)。
-
- 水平拖動(dòng)(Horizontal drag)
-
onHorizontalDragStart: 指針已經(jīng)和屏幕聯(lián)系,并且可能開(kāi)始水平移動(dòng)。 -
onHorizontalDragUpdate: 正在和屏幕聯(lián)系的指針已經(jīng)開(kāi)始在水平方向上進(jìn)行移動(dòng)。 -
onHorizontalDragEnd: 之前與屏幕進(jìn)行聯(lián)系且在水平方向移動(dòng)的指針,現(xiàn)在已經(jīng)不需要再與屏幕聯(lián)系了,并且在停止聯(lián)系的瞬間,指針依然以一定的速度移動(dòng)。
-
下面示例展示了使用GestureDetector監(jiān)聽(tīng)雙擊Flutter logo時(shí),logo旋轉(zhuǎn)的效果:
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();
}
},
),
));
}
}
ListView 與 Adapter
在Flutter中替代ListView的是什么
在Flutter中與之對(duì)應(yīng)的就是ListView。
在Android中,ListView使用一個(gè)適配器模式,每條數(shù)據(jù)都由你的適配器來(lái)渲染返回。然而你必須確保每條數(shù)據(jù)都被你回收,否則你可能會(huì)遇到各種顯示錯(cuò)亂和內(nèi)存問(wèn)題。
而在Flutter中控件不可變,你提供一個(gè)控件的列表給你的ListView,然后Flutter去處理確?;瑒?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;
}
}
如何知道哪一條列表項(xiàng)被點(diǎn)擊
在Android中,onItemClickListener會(huì)找出哪一條列表項(xiàng)被點(diǎn)擊。在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(GestureDetector(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text("Row $i")),
onTap: () {
print('row tapped');
},
));
}
return widgets;
}
}
如何動(dòng)態(tài)更新ListView
在Android中是通過(guò)適配器調(diào)用notifyDataSetChanged來(lái)更新。
而在Flutter中,如果你是在setState()方法中更新控件列表,那么你很快會(huì)發(fā)現(xiàn)你的數(shù)據(jù)沒(méi)有更新視圖,這是因?yàn)楫?dāng)setState()被調(diào)用時(shí),F(xiàn)lutter渲染引擎會(huì)在控件樹(shù)中搜索是否存在發(fā)生改變的東西,而當(dāng)它找到了你的ListView,它會(huì)進(jìn)行==判斷,然后判定這兩個(gè)ListView是相同的,因而不會(huì)發(fā)生任何改變,也就不會(huì)請(qǐng)求更新。
一個(gè)簡(jiǎn)單更新ListView的方法就是,在setState()方法中創(chuàng)建一個(gè)新的List,然后將舊集合的數(shù)據(jù)拷貝過(guò)來(lái)。這個(gè)途徑比較簡(jiǎn)單,但是不推薦在數(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 = <Widget>[];
@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è)推薦的高效且有效的方式是通過(guò)ListView.Builder來(lái)建立ListView,這種方式在你的數(shù)據(jù)量巨大或者需要?jiǎng)討B(tài)改變數(shù)據(jù)的情況下非常有用,這實(shí)質(zhì)上等價(jià)于Android中的RecyclerView,因?yàn)樗鼤?huì)自動(dòng)循環(huán)復(fù)用列表元素:
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 = <Widget>[];
@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)建ListView不同的是,創(chuàng)建ListView.Builder需要傳入兩個(gè)命名參數(shù):初始列表的長(zhǎng)度和ItemBuilder函數(shù)。
ItemBuilder與Android適配器中的getView()方法類似,它給你一個(gè)位置,然后返回你想要在對(duì)應(yīng)位置上渲染的行。
最后,且最重要的一點(diǎn)是, onTab函數(shù)已經(jīng)不需要在重新創(chuàng)建數(shù)據(jù)列表了,取而代之的是通過(guò).add()來(lái)添加到控件列表中。
文字處理
如何給文字控件設(shè)置自定義字體
在Android SDK(Android O),可以創(chuàng)建一個(gè)Font資源,然后作為FontFamily參數(shù)傳入TextView。而在Flutter中,則是將字體文件放到一個(gè)文件夾中,然后在pubspec.yaml中引用即可,跟引入圖片是類似的。
fonts:
- family: MyCustomFont
fonts:
- asset: fonts/MyCustomFont.ttf
- style: italic
然后給你的Text指定字體:
@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控件自定義風(fēng)格
除了字體,我們還可以定義Text控件的其他風(fēng)格屬性,Text控件的風(fēng)格參數(shù)中包含一個(gè)TextStyle對(duì)象,我們可以定義它的很多參數(shù),例如:
- color
- decoration
- decorationColor
- decorationStyle
- fontFamily
- fontSize
- fontStyle
- fontWeight
- hashCode
- height
- inherit
- letterSpacing
- textBaseline
- wordSpacing
表單輸入
有關(guān)表單的更多信息請(qǐng)查閱Flutter cookbook 中的Retrieve the value of a text field.
與輸入框中"hint"等價(jià)的是什么
在Flutter中,可以通過(guò)給Text控件傳入一個(gè)InputDecoration對(duì)象來(lái)顯示“hint”或者占位字符,如下示例:
body: Center(
child: TextField(
decoration: InputDecoration(hintText: "This is a hint"),
)
)
如何展示驗(yàn)證錯(cuò)誤?
和顯示hint一樣,傳一個(gè)InputDecoration對(duì)象給Text控件的構(gòu)造函數(shù)即可。
然而,你肯定不想一開(kāi)始就顯示錯(cuò)誤,而是當(dāng)用戶鍵入一個(gè)非法值才顯示,并傳入一個(gè)新的InputDecoration對(duì)象。
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> {
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(emailRegexp);
return regExp.hasMatch(em);
}
}
Flutter Plugins
如何訪問(wèn)GPS傳感器
使用 geolocator社區(qū)插件。
如何訪問(wèn)相機(jī)
流行的方式是使用 image_picker插件。
如何登錄Facebook
flutter_facebook_login插件可以用來(lái)登錄到FaceBook.
如何使用Firebase
大部分Firebase函數(shù)轉(zhuǎn)換自 first party plugins,以下是第一批集成的插件,由Flutter團(tuán)隊(duì)維護(hù):
- firebase_admob for Firebase AdMob
- firebase_analytics for Firebase Analytics
- firebase_auth for Firebase Auth
- firebase_database for Firebase RTDB
- firebase_storage for Firebase Cloud Storage
- firebase_messaging for Firebase Messaging (FCM)
- flutter_firebase_ui for quick Firebase Auth integrations (Facebook, Google, Twitter and email)
- cloud_firestore for Firebase Cloud Firestore
如何構(gòu)建自定義的 native integrations(本地集成)
如果沒(méi)有Flutter或者社區(qū)插件沒(méi)有提供那些平臺(tái)特定的功能,你可以構(gòu)建自己的,參考developing packages and plugins頁(yè)面。
Flutter的插件架構(gòu),簡(jiǎn)而言之,類似于在Android中使用EventBus:發(fā)出消息給接收器處理,接收器處理完成后將結(jié)果發(fā)射回給你。在這里,接收器代碼運(yùn)行在本地層,也就是Android或IOS.
如何在Flutter應(yīng)用中使用NDK
如果你已經(jīng)在Android應(yīng)用中使用了NDK,并且想要讓Flutter也能利用到這些類庫(kù),那么就需要構(gòu)建自定義插件了。
你的自定義插件先和Android應(yīng)用交互,即在Android應(yīng)用中通過(guò)JNI調(diào)用native函數(shù),當(dāng)響應(yīng)準(zhǔn)備好時(shí),就發(fā)送給Flutter,然后渲染結(jié)果。
目前不支持直接從Flutter中調(diào)用native代碼。
主題
如何給應(yīng)用定制主題
直接使用的情況下(Out of the box),F(xiàn)lutter自帶漂亮的Material Design實(shí)現(xiàn),它可以滿足你通常所需的大量樣式和主題需求。不像Android在XML文件中聲明主題,然后在AndroidManifest.xml中使用。在Flutter中是在頂層控件中聲明主題的。
為了能充分利用Material組件,你可以聲明一個(gè)頂層控件MaterialApp作為應(yīng)用的入口。MaterialApp是一個(gè)很便攜的控件,他包裝了實(shí)現(xiàn)了Material Design且應(yīng)用需要的控件。他建立在WidgetsApp之上,添加了Material特定的功能。
同樣也可以使用WidgetsApp作為應(yīng)用控件,它提供了一些相同的功能,但不如MaterialApp豐富。
想要在任意子組件上自定義顏色和風(fēng)格的話,那么給MaterialApp傳入一個(gè)ThemeData對(duì)象,例如,在下面代碼中,初始樣本設(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(),
);
}
}
數(shù)據(jù)庫(kù)與本地存儲(chǔ)
怎樣訪問(wèn)Share Preferences
在Android中,你可以使用SharePreferences來(lái)存儲(chǔ)一些小的鍵值對(duì)。
在Flutter中,使用Shared_Preferences plugin來(lái)訪問(wèn)這一功能。這個(gè)插件包裝了Shared Preferences和NSUserDefaults(IOS中等價(jià)的功能)兩個(gè)功能。
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
runApp(
MaterialApp(
home: Scaffold(
body: Center(
child: RaisedButton(
onPressed: _incrementCounter,
child: Text('Increment Counter'),
),
),
),
),
);
}
_incrementCounter() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
int counter = (prefs.getInt('counter') ?? 0) + 1;
print('Pressed $counter times.');
prefs.setInt('counter', counter);
}
如何訪問(wèn)SQLite數(shù)據(jù)庫(kù)
在Flutter中使用SQFlite插件實(shí)現(xiàn)此功能。
通知
如何推送通知
(國(guó)內(nèi)環(huán)境中firebase基本也沒(méi)有用)
在Flutter中,通過(guò)Firebase_Messaging插件可以使用該功能,更多關(guān)于使用Firebase Cloud Messaging API的信息,請(qǐng)查看firebase_messaging插件文檔。