导引
本文原文在这里:Flutter for Android developers
本文属于半翻译半笔记注解,按照自己的描述整合了原文,如若读者读过原文会发现有些许不一样不要惊讶,但仍然完整表达了原文的意思,文中斜体加粗部分是我自己的注解。
前言
Android的知识和技能对Flutter开发很有价值,因为Flutter依赖于移动系统的众多功能和配置。Flutter构建App的UI的一种新方式,它有一个插件系统去连接Android/IOS原生系统来完成非UI的任务,如果你精通Android,你可以不用学习任何东西直接使用Flutter(当然需要看完这篇指南)
Views
在Flutter中什么东西相当于View
在Android里,View
是所有布局的基础,所有的控件都继承于View。在Flutter中,大致相当于View
的是Widget
。Widget不能和View划上等号,当你熟悉了Flutter的开发后你可以将它们视为你声明和构建UI的方式。
Widget与View主要有这几点不同:
- Widgets一经创建,是不可改变的一直存活到他们必须改变为止。当Widgets或者他们的状态发生改变,Flutter会创建新的Widget树的实例,而View则是绘制完成后不会再重绘直到
invalidate
被调用。 - Widgets是轻量级的,部分原因在于他们的不变性。他们不能直接绘制任何东西,而是对UI及其语义的描述,会通过"inflated"生成真实的视图对象中去。
Flutter包括 Material Components library.这些Widgets实现了Material Design guidelines.Material Design 是一个包括iOS在内的所有平台进行优化的灵活的设计。
但是Flutter的灵活性和表现力足以实现任何设计语言。例如,在iOS上,您可以使用 Cupertino widgets生成一个类似于 Apple’s iOS design language的界面。
怎样更新Widgets
在Android中可以直接改变View,但是在Flutter里,Widgets 是不可以直接更新的,取而代之的你需要去改变Widgets的状态。
这就是有状态和无状态Widgets的概念来源。 StatelessWidget
听起来就像是一个没有状态信息的Widget.
当你描述的UI里有一些部分不依赖于对象中的配置信息时, StatelessWidget
非常有用。
例如,在Android中,这类似于放置Logo的ImageView。Logo在运行时不会更改,所以在Flutter中可以使用 StatelessWidget
.
如果你想根据HTTP请求或用户交互后接收到的数据动态更改UI,那么你必须使用StatefulWidget
,并告诉Flutter widget的状态已经更新,以便它可以更新该widget。
这里需要注意的重要一点是,无状态和有状态widget的行为都是相同的,每一帧都会重建,区别在于StatefulWidget
有一个State
对象,它跨越帧时序,并且恢复StatefulWidget
的状态。
如果你有疑问,那么请始终记住这条规则:如果widget发生更改(例如,由于用户交互),那么它是有状态的。但是,如果widget对更改作出反应,但它的父widget仍然可以是无状态的,如果它不需要更改作出反应。
下面的例子使用了一个StatelessWidget
.一个常见的StatelessWidget
是Text
.如果你去看Text
的实现你会发现它是Statelesswidget
的子类。
Text(
'I like Flutter!',
style: TextStyle(fontWeight: FontWeight.bold),
);
如你所见,Text没有与之关联的状态,它只呈现在其构造函数中传递的内容。
但是,如果您想要动态地更改“I Like Flutter”,例如在单击FloatingActionButton时?
要实现这一点,请将Text包装在一个有StatefulWidget
中,并在用户单击按钮时更新它。
例如:
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中,使用控件树编写布局。
以下示例显示如何使用一个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
怎么添加或者删除一个布局中的控件
在Android中,可以对父布局调用addChild()
或removeChild()
来动态添加或删除子布局。在Flutter中,因为控件是不可变的,所以没有与addChild()
直接等价的方式。相反,可以将一个函数传递给返回小部件的父控件,并用一个boolean控制子控件的创建。
例如这里,你可以点击FloatingActionButton
来切换两个控件:
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),
),
);
}
}
怎么给控件添加动画呢
在Android中,可以使用XML创建动画,也可以在View
上调用animate()
方法。在Flutter中,实现动画的方式是使用animated library
中的animated widget
来包装控件。
在Flutter中,使用AnimationController
它是一个Animation<double>
,可以实现暂停、搜索、停止、反转的动画。它需要一个Ticker
作为vsync的信号发生器,并且在每一帧发出一个0-1之间的线性插值。你可以创建多个Animation
使用同一个AnimationController
你可以使用CurvedAnimation
去实现插值曲线的动画,从这个意义上来说,控制器是实现动画变换的主要部分,而使用CurvedAnimation
可以实现曲线插值替换默认的线性运动,就像Widget
一样,Flutter的动画也可以互相组合。
当创建控件树的时候可以将Animation
赋给控件的动画属性,例如实现透明的FadeTransition
,同时告诉控制器开始动画。
下面这个例子使用FadeTransition
是实现了渐进显示Logo的一个效果:
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来绘制?
在Android中,您可以使用Canvas
和Drawable
在屏幕上绘制图像和形状。Flutter也有类似的Canvas
API,因为它基于相同的底层渲染引擎,Skia。因此,对于Android开发人员来说,在Flutter中绘制到Canvas
是一项非常熟悉的工作。
Flutter有两个类来帮助你绘制Canvas:CustomPaint
和CustomPainter
,后者可以实现你的算法去绘制Canvas.
想了解如何使用Flutter实现一个签名绘画区,可以参考Collin的回答: StackOverflow.
这里需要注意的是Collin原回答不能实现绘画,需要使用下面的代码需要替换掉源码中这一行,修改CustomPaint
的构造参数
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()));
怎么创建自定义控件
在Android中,通过继承于View或者继承于一个已经存在的View,通过覆盖或者实现接口来完成期望的效果。
在Flutter中,通过组合更小控件(而不是继承)来构建一个自定义的控件。这有点类似于在Android中实现自定义ViewGroup,其中所有构建块都已经存在,但是你提供了不同的行为—例如,自定义布局逻辑。
例如,如何构建一个带有标签的CustomButton
呢,通过组合RaisedButton
包含一个lablel
更好,而不是通过继承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相对应的是什么?
在Android
中,Intent
有两个主要用途:用于activity
间的跳转、用于组件间的通信。而在Flutter
中,没有Intent
这个概念,虽然你依然可以通过本地集成(native integrations
(使用插件))来启动Intent
。
Route
可以看做是应用屏幕或者页面的抽象,而Navigator
是一个管理Route
的控件。可以粗略的将Route
看成Activity
,但是它们含义不同。Navigator
通过push
和pop
操作Route
在页面之间切换,Navigator
就像是一个栈,push
表示切换到新的页面,pop
表示返回。
在Android
中,需要在AndroidManifest.xml
中声明activity,而在Flutter中,你有以下页面切换选择:
- 指定一个包含所有Route名字的
Map
(MaterialApp) - 直接切换到Route(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'),
},
));
}
通过压入对应的名字到Navigator
来切换到对应的Route
:
Navigator.of(context).pushNamed('/b');
另一种Intent
常见的使用场景是调用外部组件比如相机、文件选择器,对于这种情况你需要创建一个原生平台集成(或者使用现成插件 existing plugin)
关于构建原生平台集成,请查看 Developing Packages and Plugins.
Flutter 如何处理来自外部的Intent?
Flutter可以通过直接访问Android layer来处理来自Android的Intent,或者请求共享数据。
下面的实例注册了一个文本共享的Intent过滤器在运行我们Flutter代码的原生activity上,所以其他应用就能共享文本给我们的Flutter 应用。
基本流程就是先在Android 原生层(即Activity)先处理这些共享数据,然后等待Flutter请求,通过MethodChannel
可以将这些数据提供给Flutter.
首先,在AndroidManifest.xml
中注册Intent过滤器:
<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中获取共享的数据,然后先暂时持有这些数据,当Flutter准备好处理时,它会通过平台通道(platform channel)进行请求,然后从原生Android层发送这些数据。
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);
}
}
最后,当 Flutter的控件渲染完成时请求数据:
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()
等价的是什么?
Flutter中Navigator
除了用于处理Route
,也可以获得已入栈Route的返回结果。只需要等push()
返回的Future
执行完即可。
Map coordinates = await Navigator.of(context).pushNamed('/location');
然后,在定位Route
里,当用户选择完位置后,你就可以通过pop
获取结果了:
Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});
异步UI
runOnUiThread 在Flutter中等价于什么
Dart是单线程执行模型,支持Isolates
(在另一个线程上运行Dart代码的方式)、事件循环和异步编程。 除非您启动一个Isolate
,否则您的Dart代码将在主UI线程中运行,并由事件循环驱动(和JavaScript一样)。Flutter的事件驱动循环等价于Android的主线程/UI线程的Looper。
Dart虽然是单线程模型但是并不意味着你需要用一种阻塞式的方式来执行其他所有的代码造成UI线程被冻结。不像Android,你需要保持主线程一直空闲不被阻塞,在Flutter中,你可以使用Dart语言提供的异步工具,如async/await
来执行异步操作。如果你用过C#,Javascript,或者你使用过Kotlin的协程的话,你可能会熟悉async/await
这个范例。
例如,你可以使用async/await
来执行网络请求代码同时不会引起UI线程挂起,让Dart完成繁重的操作:
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
一旦等到网络请求结束,就会调用setState()
方法以更新UI,接着触发控件子树的重建并更新数据。
下面的示例描述了如何异步加载数据,然后填充到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(下文中也有提到,这里是为了方便顺序查看本文时验证demo所以先提到),可以访问http 0.12.0+2(当前版本)查看对应版本号在pubspec.yaml
中引入:
dependencies:
http: ^0.12.0+2
如何将任务放入后台线程?
在Android中,访问网络资源的一种典型方式就是在后台线程中去执行任务,从而避免在主线程中执行造成ANR。例如,你可以使用AsyncTask
, LiveData
, IntentService
, JobScheduler
或者RxJava Scheduler
进行后台处理。
由于Flutter是一个单线程模型运行着一个事件循环(像Node.js一样),你不需要替线程管理和创建后台线程而操心。如果你进行I/O操作,例如硬盘访问或者网络请求,你可以简单的使用async/await
来完成所有的操作,反过来说,如果你需要执行计算密集型的任务,捏可以把它放入一个Isolate
中从而避免阻塞事件循环,就像Android中,你不会在主线程执行任何除UI更新相关的其他任何类型的任务。
I/O类的任务,只需要声明函数是一个async
函数,然后在函数中,用await
修饰在耗时任务之前:
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
这就是网络请求、数据库操作等的典型做法,它们都是I/O
操作。
在Android中,当你继承AsyncTask
,那么通常你需要重写三个方法,onPreExecute()
、doInBackground()
和onPostExecute()
,而在Flutter中则没有与之等价的方式,因为await
修饰的耗时任务函数,剩余的工作都交给Dart的事件循环去处理。
然而当你处理大量数据时,你的UI会挂起,所以在Flutter中使用Isolate
来充分利用多核CPU去执行耗时的计算密集型任务。
Isolate
是独立的执行线程,它不会与主线程共享内存堆,这就意味着你不能在Isolate中直接访问主线程的变量,或者调用setState更新UI。不像Android中的线程,Isolate是名副其实的隔离区,不能共享内存(比如不能以静态字段的方式共享等)。
下面示例展示了一个简单的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中与OkHttp等价的是什么?
在Flutter中使用流行的 httppackage调用一个网络请求是很简单的。
虽然在http包中没有OkHttp所有的功能,它抽象了大多你通常需要自己实现的网络功能,使得网络请求更加简单。
要使用http包,需要在pubspec.yaml
中添加如下依赖:
dependencies:
...
http: ^0.11.3+16
发起一个网络请求可以使用await
调用一个async
函数例如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);
});
}
}
如何显示耗时任务的进度?
在Android中,当在后台线程中执行一个耗时任务时,通常显示一个ProgressBar
在UI上。
在Flutter中,则是使用ProgressIndicator
控件。通过boolean标记位来控制何时开始渲染,然后在耗时任务开始之前显示它,并在任务结束时隐藏掉。
下面示例中,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);
});
}
}
项目结构&资源文件
在哪里存放不同分辨率的图片文件?
在Android
中,resources
与assets
是两个独立的文件夹,而在Flutter
中,只有assets
,所有放在Android
的res/drawable-*
文件夹中的文件全都放在Flutter
中的assets
文件夹中。
Flutter
跟ios
一样遵循简单的基于密度(density-base
)的格式,assets
包含1.0x
、2.0x
、3.0x
或者更高乘数,Flutter
中并没有dp
这一说,而是使用与设备无关的逻辑像素,在devicePixelRatio 中描述了单个逻辑像素与物理像素的比例。
对应于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
可以存在任意的文件夹,Flutter没有预定义的文件夹结构。需要在pubspec.yaml
声明assets
对应的物理位置,Flutter就能正确的读取到。
注意,在Flutter 1.0 beta 2之前,Flutter定义的assets
无法被原生端访问,反之亦然,原生定义的assets
和resources
也不能被Flutter访问,因为他们位于不同的文件夹中。
而从Flutter 1.0 beta2开始,assets
存储于本地层的assets
文件夹中,且可以被本地层通过AssetManager
访问,但是beta2版本中,Flutter依然不能访问本地层的resources
和assets
:
val flutterAssetStream = assetManager.open("flutter_assets/assets/my_flutter_asset.png")
例如,要将名为my_icon.png
的新图像资源添加到我们的Flutter项目中,比方说,把它放到一个我们任意命名为images
的文件夹中,你需要把1.0x
图片资源放在该文件的根目录,然后把其他放在对应乘数比例命名的子文件夹中:
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
控件中写入路径:
@override
Widget build(BuildContext context) {
return Image.asset("images/my_image.png");
}
在哪里存放strings
字符串资源,怎么实现本地化?
目前Flutter
没有像strings
这样的专用资源系统,所以目前来说,最佳的方法就是把你的字符串以static字段的方式存放在一个类中,然后从这个类去访问它们。如下示例:
class Strings {
static String welcomeMessage = "Welcome To Flutter";
}
然后在代码中你可以这样调用你的字符串:
Text(Strings.welcomeMessage)
Flutter对Android上的辅助功能提供了基本支持,目前工作正在进行中。官方鼓励Flutter开发者使用intl package来实现国际化和本地化。
与Gradle文件等价的是什么?该怎么添加依赖?
在Android中,使用Gradle
构建脚本添加依赖。在Flutter中,使用Dart
自己的构建系统和Pub
包管理器。该工具会把原生的Android和IOS的包装应用的构建过程委托给各自的构建系统。
gradle
文件在Flutter
工程目录的android
文件夹下,只有需要针对单个平台添加本地依赖时才使用gradle
,通常来说在Flutter中直接在pubspec.yaml
声明外部依赖即可。找Flutter包的好地方就是 Pub.
Activities和fragments
Flutter中与activities
和fragments
相对应的是啥?
在Android中,activity
是用户可以操作的一个焦点物,而Fragment
则代表着一个行为表现或者是UI的一部分。Fragment可以模块化你的代码,可以给大屏设备组合出复杂的UI部分,帮助你拓展你的应用UI.在Flutter中,这两个概念都落在了Widget
的范畴里。
要了解有关使用Activity
和Fragment
构建UI的更多信息,可以看社区里的文章,Flutter For Android Developers : How to design an Activity UI in Flutter.
正如在Intent
部分所提到的,在Flutter中,Widget
就代表着屏幕,因为在Flutter中万物皆Widget
。我们使用Navigator
来切换Route
,这代表着不同屏幕或页面,亦或是不同状态或是渲染相同的数据。
如何监听Android中activity
的生命周期事件
在Android中, 通过重写Activity
中的方法来捕获生命周期函数的回调,或者实现ActivityLifecycleCallbacks
在Application
上。而在Flutter
中,没有这个概念,但你可以通过hookWidgetsBinding
并且监听didChangeAppLifecycleState()
的变化事件来代替监听生命周期。
可观察的生命周期事件有:
-
inactive
— 应用处于非活动状态,不接受用户输入。这个事件只在IOS中有效,因为在Android中没有与此等价的事件。 -
paused
— 当前应用对用户不可见,不响应用户输入,且运行在后台。等同于Android中的onPause。(如果等同于Android中onPause的话这里应该是可见但是不在顶层可见) -
resumed
— 此时应用可见且响应用户输入。等同于Android中的onPostResume()
-
suspending
— 此时应用暂时挂起。等同于Android中的onStop;不会触发IOS上的事件,IOS中没有等价的事件。
想要了解更多关于这些状态的信息,可以参见 AppLifecycleStatus documentation.
正如你注意到的,只有少数的Activity
生命周期可用,而FlutterActivity
确实在内部捕获了几乎所有的Activity
生命周期事件然后把它们发送到Flutter的引擎中去,然而很多事件都替你屏蔽了。Flutter会替你管理引擎的启动和关闭,所以大多数情况下我们几乎不需要去观察activity
的生命周期在Flutter中。如果你一定要在生命周期里获取或者释放本地资源,你至少可以在原生端做这些事。
以下示例描述了如何监听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
等价的是什么
在Android中,LinearLayout
用于横向和纵向布局控件,而在Flutter中则是使用Row
或Column
控件来实现相同的行为。
你可能注意到两段示例代码实现相同,仅仅Row
和Column
控件不同。子控件是完全相同的,这样的特性可以被用来开发具有相同子控件但是随着时间推移会变化的富布局。
@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'),
],
);
}
如果想了解更多关于构建线性布局的内容,可以参见社区文章:Flutter For Android Developers : How to design LinearLayout in Flutter?.
与RelativeLayout
等价的是什么
使用相对布局可以将你的子控件按照彼此的位置相互放置。在Flutter中,有很多方法可以达到同样的效果。
你可以组合Column
,Row
和Stack
控件来实现与相对布局一样的效果。你可以在widgets
的构造函数中指定子控件相对于父布局的规则。
有关在FLutter中构建RelativeLayout
的好例子,可以参考Collin's的回答: StackOverflow.
与ScrollView
等价的是什么
在Android中,当你的内容超出设备屏幕时,使用ScrollView
来布局。
在Flutter中,最简单的实现方式就是使用ListView
控件。这与Android看起来仿佛有点矫枉过正,但是在Flutter中一个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中如何处理横屏反转
只要在AndroidManifest.xml
中包含下列内容FlutterView
就会处理这些配置变化:
android:configChanges="orientation|screenSize"
手势检测和触摸事件处理
Flutter中怎么给一个空间添加点击事件
在Android中你可以通过setOnClickListener
来给一个View,例如Button添加点击事件。
在Flutter中,有下面两种方式添加触摸事件监听:
- 如果控件支持事件检测,那可以给它传入一个函数去处理这个事件。比如,
RaisedButton
包含一个onPressd
参数:
@override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () {
print("click");
},
child: Text("Button"));
}
- 如果空间不支持事件检测,可以把它包装在
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");
},
),
));
}
}
如何处理控件的其他手势?
使用GestureDetector
可以监听广泛的手势,例如:
- 列表第一项
- 列表第二项
- 列表第二项
- 列表第二项
- 单击(Tab)
-
onTabDown
: 触发单击事件的指针已经与屏幕上特定点联系在一起 -
onTapUp
: 触发单击事件的指针停止与屏幕在特定点上的联系 -
onTap
: 单击事件发生 -
onTapCancel
: 触发时会导致之前触发onTabDown
的指针无法形成单击事件
-
- 双击(Double Tab)
-
onDoubleTab
: 用户在屏幕的同一个点上连续快速点击了两次
-
- 长按Long Press
-
onLongPress
: 指针和屏幕上同一个位置发生一段较长事件的联系
-
- 垂直拖动(Vertical drag)
-
onVerticalDragStart
: 指针已经和屏幕联系,并且可能开始垂直移动。 -
onVerticalDragUpdate
: 正在和屏幕联系的指针已经开始在垂直方向上进行移动。 -
onVerticalDragEnd
: 之前与屏幕进行联系且在垂直方向移动的指针,现在已经不需要再与屏幕联系了,并且在停止联系的瞬间,指针依然以一定的速度移动。
-
- 水平拖动(Horizontal drag)
-
onHorizontalDragStart
: 指针已经和屏幕联系,并且可能开始水平移动。 -
onHorizontalDragUpdate
: 正在和屏幕联系的指针已经开始在水平方向上进行移动。 -
onHorizontalDragEnd
: 之前与屏幕进行联系且在水平方向移动的指针,现在已经不需要再与屏幕联系了,并且在停止联系的瞬间,指针依然以一定的速度移动。
-
下面示例展示了使用GestureDetector
监听双击Flutter logo时,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();
}
},
),
));
}
}
ListView 与 Adapter
在Flutter中替代ListView的是什么
在Flutter中与之对应的就是ListView
。
在Android中,ListView使用一个适配器模式,每条数据都由你的适配器来渲染返回。然而你必须确保每条数据都被你回收,否则你可能会遇到各种显示错乱和内存问题。
而在Flutter中控件不可变,你提供一个控件的列表给你的ListView
,然后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;
}
}
如何知道哪一条列表项被点击
在Android中,onItemClickListener
会找出哪一条列表项被点击。在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;
}
}
如何动态更新ListView
在Android中是通过适配器调用notifyDataSetChanged
来更新。
而在Flutter中,如果你是在setState()
方法中更新控件列表,那么你很快会发现你的数据没有更新视图,这是因为当setState()
被调用时,Flutter渲染引擎会在控件树中搜索是否存在发生改变的东西,而当它找到了你的ListView,它会进行==
判断,然后判定这两个ListView是相同的,因而不会发生任何改变,也就不会请求更新。
一个简单更新ListView的方法就是,在setState()
方法中创建一个新的List
,然后将旧集合的数据拷贝过来。这个途径比较简单,但是不推荐在数据量大的时候使用,如下示例:
import 'package:flutter/material.dart';
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List widgets = <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');
});
},
);
}
}
一个推荐的高效且有效的方式是通过ListView.Builder
来建立ListView,这种方式在你的数据量巨大或者需要动态改变数据的情况下非常有用,这实质上等价于Android中的RecyclerView
,因为它会自动循环复用列表元素:
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');
});
},
);
}
}
与创建ListView不同的是,创建ListView.Builder
需要传入两个命名参数:初始列表的长度和ItemBuilder
函数。
ItemBuilder
与Android适配器中的getView()
方法类似,它给你一个位置,然后返回你想要在对应位置上渲染的行。
最后,且最重要的一点是, onTab
函数已经不需要在重新创建数据列表了,取而代之的是通过.add()
来添加到控件列表中。
文字处理
如何给文字控件设置自定义字体
在Android SDK(Android O),可以创建一个Font
资源,然后作为FontFamily
参数传入TextView。而在Flutter中,则是将字体文件放到一个文件夹中,然后在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
控件自定义风格
除了字体,我们还可以定义Text
控件的其他风格属性,Text
控件的风格参数中包含一个TextStyle
对象,我们可以定义它的很多参数,例如:
- color
- decoration
- decorationColor
- decorationStyle
- fontFamily
- fontSize
- fontStyle
- fontWeight
- hashCode
- height
- inherit
- letterSpacing
- textBaseline
- wordSpacing
表单输入
有关表单的更多信息请查阅Flutter cookbook 中的Retrieve the value of a text field.
与输入框中"hint"等价的是什么
在Flutter中,可以通过给Text
控件传入一个InputDecoration
对象来显示“hint”或者占位字符,如下示例:
body: Center(
child: TextField(
decoration: InputDecoration(hintText: "This is a hint"),
)
)
如何展示验证错误?
和显示hint一样,传一个InputDecoration
对象给Text控件的构造函数即可。
然而,你肯定不想一开始就显示错误,而是当用户键入一个非法值才显示,并传入一个新的InputDecoration
对象。
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
如何访问GPS传感器
使用 geolocator
社区插件。
如何访问相机
流行的方式是使用 image_picker
插件。
如何登录Facebook
flutter_facebook_login
插件可以用来登录到FaceBook.
如何使用Firebase
大部分Firebase
函数转换自 first party plugins,以下是第一批集成的插件,由Flutter
团队维护:
- 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
如何构建自定义的 native integrations(本地集成)
如果没有Flutter或者社区插件没有提供那些平台特定的功能,你可以构建自己的,参考developing packages and plugins页面。
Flutter
的插件架构,简而言之,类似于在Android中使用EventBus
:发出消息给接收器处理,接收器处理完成后将结果发射回给你。在这里,接收器代码运行在本地层,也就是Android
或IOS
.
如何在Flutter应用中使用NDK
如果你已经在Android应用中使用了NDK,并且想要让Flutter也能利用到这些类库,那么就需要构建自定义插件了。
你的自定义插件先和Android应用交互,即在Android应用中通过JNI调用native
函数,当响应准备好时,就发送给Flutter,然后渲染结果。
目前不支持直接从Flutter中调用native代码。
主题
如何给应用定制主题
直接使用的情况下(Out of the box),Flutter自带漂亮的Material Design
实现,它可以满足你通常所需的大量样式和主题需求。不像Android在XML文件中声明主题,然后在AndroidManifest.xml
中使用。在Flutter中是在顶层控件中声明主题的。
为了能充分利用Material
组件,你可以声明一个顶层控件MaterialApp
作为应用的入口。MaterialApp
是一个很便携的控件,他包装了实现了Material Design
且应用需要的控件。他建立在WidgetsApp
之上,添加了Material
特定的功能。
同样也可以使用WidgetsApp
作为应用控件,它提供了一些相同的功能,但不如MaterialApp
丰富。
想要在任意子组件上自定义颜色和风格的话,那么给MaterialApp
传入一个ThemeData
对象,例如,在下面代码中,初始样本设置为蓝色,而文字选择后显示为红色。
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
textSelectionColor: Colors.red
),
home: SampleAppPage(),
);
}
}
数据库与本地存储
怎样访问Share Preferences
在Android中,你可以使用SharePreferences
来存储一些小的键值对。
在Flutter中,使用Shared_Preferences plugin来访问这一功能。这个插件包装了Shared Preferences
和NSUserDefaults
(IOS中等价的功能)两个功能。
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);
}
如何访问SQLite数据库
在Flutter中使用SQFlite插件实现此功能。
通知
如何推送通知
(国内环境中firebase基本也没有用)
在Flutter
中,通过Firebase_Messaging插件可以使用该功能,更多关于使用Firebase Cloud Messaging API
的信息,请查看firebase_messaging插件文档。