本文档适用于那些希望应用现有Android知识来使用Flutter构建移动应用的Android开发人员。如果你了解Android框架的基础知识,那么可以将此文档用作Flutter开发的快速入门。
当使用Flutter构建时,你的Android知识和技能非常有价值,因为Flutter依赖移动操作系统来实现众多功能和配置。Flutter是一个为手机构建UI界面的新方式,但是它有一个插件系统来和Android或者iOS系统进行非UI人物的交互。
在Android中,视图(Views)是屏幕上显示的所有内容的基础。按钮、工具栏和输入框,它们都是视图。在Flutter中,略等价于视图的是控件(Widget).控件并不严格对应于Android中的视图,但是当你熟悉Flutter的工作方式后,你可以将它们视为“你声明和构建UI的方式”。
然而,这些与视图有一些区别。首先,控件有一个不同的生命周期:它们是不可变的,只有在需要改变之前才存在。每当控件或其状态发生变化时,Flutter的框架层创建一个新的控件实例树。作为对比,一个Android的视图(View)只绘制一次,直达 invalidate 被调用才会重新绘制。
Flutter的控件是轻量级的,部分原因就是它们的不可变性。因为它们本身不是视图,并且不直接绘制任何东西,而是对UI及其语义的描述,这些描述被解析到引擎下的实际视图对象中。
Flutter包含 Material Components 库,它们是实现了 Material Design 准则的控件。 Material Design 是一个灵活的设计系统,适用于所有平台,包括iOS。
但Flutter具有足够的灵活性和表现力,可以实现任何设计语言。例如,在iOS上,你可以使用Cupertino控件来生成看起来像Apple的iOS设计语言的界面。
在Android中,你可以通过直接更改视图来更新你的视图。然而,在Flutter中, Widgets 是不可变的并且无法直接更新,相反,你必须操作控件的状态来更新控件。
这就是有状态(Stateful)控件和无状态(Stateless)控件构想的来源。一个无状态控件 StatelessWidget 就是它看起来的样子–一个没有状态信息的控件。
当你描述的部分用户界面不依赖于对象中的配置信息意外的任何内容时, StatelessWidget 就变的非常有用。
例如,在Android中,这类似于使用ImageView来显示你的logo。这个logo在运行时不会改变,所以在Flutter中使用无状态控件( StatelessWidget )。
如果你想基于进行网络请求后接收到的数据或者用户交互来动态改变UI,那么你必须使用有状态控件 StatefulWidget ,并且告诉Flutter框架层控件的状态发生更新,所以它需要更新这个控件。
这里需要注意的重要的一点是,无状态和有状态控件的核心行为都是一样的。它们重建每一帧,区别在于有状态控件( StatefulWidget )有一个状态( State )对象来跨帧存储状态数据并且恢复它。
如果你很疑惑,那么请记住这个规则:如果一个控件是可变的(例如由于用户的交互发生变化),那么它是有状态的。但是,如果控件对更改发生变化,则包含该控件的父控件任然可以是无状态的,如果父控件本身不对更改发生变化。
下面的例子展示了如何使用无状态控件( 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),
),
);
}
}
在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),
),
),
);
}
你可以在 widget catalog 中查看Flutter提供的布局
在Android中,你可以在父布局中动态的调用 addChild() 或者 removeChild() 方法来添加或者删除视图。在Flutter中,因为控件是不可变的,所以没有与 addChild() 相同 的方法。取而代之,你可以传入一个返回控件的方法,并且通过一个标志来控制子控件的创建。
例如:
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中,为控件设置动画要使用动画库,它通过包装控件到一个动画控件中。
在Flutter中,要使用动画控制器 AnimationController ,它是一个可以暂停,快进,停止和倒转的 Animation<double> 类型的动画。它依赖 Ticker 来指示vsync何时发生,并且在它运行时在每一帧上产生一个线性的0到1之间的插值器。然后,你创建一个或多个动画并将它们与该控制器关联起来。
例如,你可以使用 CurvedAnimation 沿插值曲线来实现一个动画。在这个场景下,控制器是动画进度的主要来源,并且由 CurvedAnimation 计算曲线来代替控制器默认的线性动画。
当构建一个控件树时,你指定 Animation 给某个控件的一个动画属性,例如 FadeTransition 的透明(opacity )属性,并且告诉控制器开始动画。
下面的例子展示了如何写一个渐变动画( 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
。
在Android中,你可能使用 Canvas 和 Drawable 来在屏幕上绘制图片和形状。Flutter同样也有一个类似的 Canvas API,因为它基于相同的底层渲染引擎Skia。这就导致,在Flutter的Canvas中绘制对Android开发者来说非常的熟悉。
Flutter有两个类来帮助你在canvas上绘制: CustomPaint 和 CustomPainter ,后者是实现了绘制到画布的算法。
要想知道在Flutter中如何实现一个签名绘制,请参考StackOverflow 中 Collin 的回答。
import 'package:flutter/material.dart';
void main() => runApp(MaterialApp(home: DemoApp()));
class DemoApp extends StatelessWidget {
Widget build(BuildContext context) => Scaffold(body: Signature());
}
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),
);
}
}
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;
}
在Android中,一般继承 View 类,或者使用已经存在的类,来覆盖或者实现方法来达到想要的功能。
在Flutter中,构建一个自定义控件通过 组合( composing )一些更小的控件(而不是继承他们)。它有点类似于在Android中实现自定义ViewGroup,其中所有构建块都已经存在,但是你提供一个不同的行为–例如,自定义布局逻辑。
例如,你如何构建一个在构造函数中使用标签的 CustomButton ?创建一个CustomButton ,它组合一个带有标签的 RaisedButton ,而不是通过继承 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"),
);
}
在Android中, Intent 有两种主要的使用场景:在Activity之间实现导航以及组件之间进行通信。在Flutter中,通过另一种方式,它没有 Intent 的概念,尽管你仍然可以使用一个插件(Android Intent Plugin for Flutter)来启动 Intent 。
Flutter没有一个与activity和fragment真正的等价物;相反,在Flutter中,你可以使用导航器( Navigator )和路由( Routes)来实现屏幕的导航,就和activity一样。
一个 Route 是app中一个屏幕或者一个页面的抽象,而 Navigator 是一个来管理routes的控件。一个route大致对应一个Activity,但它没有相同的含义。一个导航器( Navigator )可以在屏幕之间压入(push)或者弹出(pop)route。导航器( Navigator )的工作方式类似于一个堆栈,你可以 push() 一个新的route到你想要导航到的route,当你想要回退时你可以 pop() route。
在Android中,你在app的 AndroidManifest.xml 文件中声明你的所有的 activities。
在Flutter中,你有几个选项来在页面之间导航:
下面的例子就构建了一个 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'),
},
));
}
导航到一个route通过push它的名称到导航器:
Navigator.of(context).pushNamed('/b');
Intent 另一种比较受欢迎的使用场景是唤起外部的组件,例如相机或者文件选择器。为此,你需要与本地平台进行整合或者使用一个已经存在的插件(existing plugin)
要学习如何构建本地平台的整合,请参考 Developing Packages and Plugins
通过直接与Android层交互并请求共享的数据,Flutter可以处理来自Android传入的intents。
下面的例子在activity中注册一个共享文本的intent filter来运行我们的Flutter代码,所以第三方APP可以共享文本到我们的APP。
基本流程是我们首先在Android端(在我们的Activity中)处理共享文本数据,然后等待Flutter请求数据,并使用 MethodChannel 来提供数据。
首先,在 AndroidManifest.xml 中为所有intents注册一个intent filter:
<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 请求数据,它会从本地端被发送:
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;
});
}
}
}
Navigator 类处理Flutter中的路由,并且得到压入堆栈的路由返回来的结果。这是通过等待 push() 返回的Future来完成的。
例如,打开一个 location 路径,用来让用户选择他们的位置,你可以这样做:
Map coordinates = await Navigator.of(context).pushNamed('/location');
然后,在你的 location 路径中,一旦用户选择了他们的位置信息,你可以携带一个结果弹出栈:
Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});
Dart有一个单线程执行模型,支持 Isolates (一种在另一个线程执行代码的方式)、事件循环和异步编程。除非你创建一个 Isolates ,否则你的Dart代码都运行在主UI线程并且被一个事件循环驱动。Flutter的事件循环与Android中的 main Looper 等价,也就是说附加到主线程的 Looper 。
Dart的单线程模型并不意味着你需要将所有内容作为阻塞操作运行,导致UI卡顿。不想Android,他要求你始终保持主线程空闲,在Flutter中,使用Dart语言提供的异步功能,例如 async/await 来执行异步任务。如果您在C#,Javascript中使用它,或者如果您使用过Kotlin的协程,你可能熟悉async / await范例。
例如,你可以通过使用 async/await 并让Dart完成繁重的操作来运行网络代码而不会导致UI挂起。
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
一旦等待的网络调用完成,通过调用 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);
});
}
}
在Android中,当你想要访问网络资源时一般你可能让它在后台线程执行,从而使得它不会阻塞主线程并且避免 ANR。例如,你可能使用 AsyncTask 、 LiveData 、IntentService 、JobScheduler 或者使用RXJava在后台线程执行。
因为Flutter是单线程的并且运行一个事件循环(像Node.js),你不需要担心线程管理或者新建一个后台线程。如果你执行 I/O 密集型操作,例如磁盘访问或者网络调用,那么你可以安全使用 async/await 就可以了。另一方面,如果你需要执行计算密集型的任务来保持CUP繁忙,你可以将该任务移动到 Isolates 来避免阻塞事件循环,就像你不会在Android主线程中保留任意类型的任务一样。
对于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中没有与之等价的东西,因为等待长时间运行的函数时,Dart的事件循环负责其余部分。
但是,有时你可能正在处理大量数据并且UI挂起。在Flutter中,使用 Isolates 来充分利用多核CPU处理长时间运行或者计算敏感型的任务。
Isolates 是单独的执行线程,它们不共享主线程的内存堆。这意味着你不能从主线程访问变量,或者通过调用 setState() 更新你的UI。不同于Android中的线程, Isolates 与其名称相同,并不能共享内存(例如,以静态字段的形式)。
下面的例子展示了,在一个简单的 Isolates 中,如何共享数据给主线程来更新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() 是一个 Isolates ,它运行在它自己的独立执行的线程中。在这个 Isolates 中,你可以执行更多CPU密集型的处理(例如,解析一个很大的JSON串),或者执行计算密集型的数学计算,例如加密或者信号处理。
你可以运行如下完整代码:(如何添加http依赖,看下一节)
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'dart:isolate';
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List widgets = [];
@override
void initState() {
super.initState();
loadData();
}
showLoadingDialog() {
if (widgets.length == 0) {
return true;
}
return false;
}
getBody() {
if (showLoadingDialog()) {
return getProgressDialog();
} else {
return getListView();
}
}
getProgressDialog() {
return Center(child: CircularProgressIndicator());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: getBody());
}
ListView getListView() => ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
});
Widget getRow(int i) {
return Padding(padding: EdgeInsets.all(10.0), child: Text("Row ${widgets[i]["title"]}"));
}
loadData() async {
ReceivePort receivePort = ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
// The 'echo' isolate sends its SendPort as the first message
SendPort sendPort = await receivePort.first;
List msg = await sendReceive(sendPort, "https://jsonplaceholder.typicode.com/posts");
setState(() {
widgets = msg;
});
}
// the entry point for the isolate
static dataLoader(SendPort sendPort) async {
// Open the ReceivePort for incoming messages.
ReceivePort port = ReceivePort();
// Notify any other isolates what port this isolate listens to.
sendPort.send(port.sendPort);
await for (var msg in port) {
String data = msg[0];
SendPort replyTo = msg[1];
String dataURL = data;
http.Response response = await http.get(dataURL);
// Lots of JSON to parse
replyTo.send(json.decode(response.body));
}
}
Future sendReceive(SendPort port, msg) {
ReceivePort response = ReceivePort();
port.send([msg, response.sendPort]);
return response.first;
}
}
在Flutter中当你使用流行的 http 包( http package)进行网络请求时是很简单的。
虽然http包没有OkHttp中的所有功能,它抽象了你通常要自己实现的大部分网络请求代码,使得进行网络请求更简单。
要使用 http 包,在 pubspec.yaml 文件中把它添加到你的依赖中:
dependencies:
...
http: ^0.11.3+16
要发起一个网络请求,在 async 方法的 http.get() 上调用 await :
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
[...]
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
}
在Android中,当在后台执行一个长时间运行的任务时你通常显示一个 ProgressBar 视图在你的UI中。
在Flutter中,使用一个 ProgressIndicator 控件。当他渲染的时候通过控制一个boolean值来以编程的方式显示进度。在你开始执行耗时任务之前告诉Flutter去更新它的状态,并在耗时任务结束后隐藏它。
在下面的例子中,build函数被拆分成三个不同的函数。如果 showLoadingDialog() 是 true (当 widgets.length == 0 时),那么渲染 ProgressIndicator 。否则使用网络请求返回的数据来渲染 ListView 。
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List widgets = [];
@override
void initState() {
super.initState();
loadData();
}
showLoadingDialog() {
return widgets.length == 0;
}
getBody() {
if (showLoadingDialog()) {
return getProgressDialog();
} else {
return getListView();
}
}
getProgressDialog() {
return Center(child: CircularProgressIndicator());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: getBody());
}
ListView getListView() => ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
});
Widget getRow(int i) {
return Padding(padding: EdgeInsets.all(10.0), child: Text("Row ${widgets[i]["title"]}"));
}
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
}
虽然Android将 resources 和 assets 视为不同的资源,但是Flutter应用程序仅仅只有 assets 。所有在Android中放在 res/drawable-* 目录下的资源文件在Flutter中都被放在 assets 文件夹下。
Flutter遵循简单的基于密度的格式,类似iOS。Assets 可能是 1.0x,2.0x,3.0x ,或者其他的倍数。Flutter没有 dp 但是有逻辑像素( logical pixels),它基本上和与设备无关的像素相同。所谓的 devicePixelRatio 表示单个逻辑像素中的物理像素的比率。
相当于Android中的密度是:
Android密度限定符 | Flutter像素比率 |
---|---|
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 beata 2之前,定义在Flutter中的assets 在本地端是无法访问的, 反之亦然,本地端的assets和resources对Flutter也是不可见的,因为它们在不同的文件夹中。
从Flutter beta 2开始,assets 被存在本地端的 asset 文件夹中,并且在本地端使用Android中的 AssetManager 来访问它们。
val flutterAssetStream = assetManager.open("flutter_assets/assets/my_flutter_asset.png")
截止Flutter beta 2,Flutter任然无法访问本地端资源,也无法访问本地端assets。
要向我们的Flutter项目中添加新的图片 my_icon.png ,例如,并决定它应该放在 images 文件夹中。你会把基本图片(1.0x)放在 images 文件夹中,并将其他密度的文件放在子文件夹中对应的乘数因子文件夹中:
images/my_icon.png // Base: 1.0x image
images/2.0x/my_icon.png // 2.0x image
images/3.0x/my_icon.png // 3.0x image
下一步,你需要在 pubspec.yaml 文件中声明这些图片:
assets:
- images/my_icon.jpeg
然后,你可以使用 AssetImage 来访问它们:
return AssetImage("images/a_dot_burr.jpeg");
或者直接在一个 Image 控件中使用:
@override
Widget build(BuildContext context) {
return Image.asset("images/my_image.png");
}
Flutter目前没有专门的字符串资源系统。目前,最佳的做法是将复制文本作为静态字段保存在类中,并从那里访问它们。例如:
class Strings {
static String welcomeMessage = "Welcome To Flutter";
}
然后,在你的代码中,你可以这样访问你的字符串:
Text(Strings.welcomeMessage)
Flutter对Android上的访问提供了基本支持,尽管此功能正在进行中。
鼓励Flutter开发者使用 intl package 包进行国际化和本地化。
在Android中,你通过Gradle构建脚本添加依赖。Flutter使用Dart自己的构建系统和 Pub 包管理器。这些工具将本地Android和iOS包装APP的构建工作委派给相应的构建系统。
在你的Flutter项目中的android文件夹下有Gradle 文件,如果要添加集成某个平台所需的本地依赖项,那么可以使用Gradle文件。通常,使用 pubspec.yaml 来声明要在Flutter中使用的外部依赖。一个找到Flutter包的好的地方是 Pub。
在Android中, Activity 表示用户可以执行的单一操作。Fragment 表示一个行为或者用户界面的一部分。Fragments 是一个让你的代码模块化的方式,为更大的屏幕构建复杂的用户界面,并帮助扩展你的应用程序的UI。在Flutter中,这两个概念都属于 Widgets 的范畴。
要学习更多关于构建Activities和Fragments的UI的知识,看社区贡献的文章,Flutter For Android Developers : How to design an Activity UI in Flutter 。
正如Intents章节所述,Flutter中的屏幕由Widgets表示,因为在Flutter中一切都是控件。你使用导航器( Navigater )来在不同的Routes 之间切换,这代表了不同的屏幕或者页面,或者可能是相同数据的不同状态或渲染。
在Android中,你可以覆盖Activity的方法来捕获该activity本身的生命周期方法,或者在 Application 上注册 ActivityLifecycleCallbacks 。在Flutter中,你没有这样的概念,但是取而代之的是你可以通过hooking WidgetsBinding 的观察者(observer )来监听生命周期事件,并且监听 didChangeAppLifecycleState() 改变事件。
可观察的生命周期事件如下:
关于更多这些状态的详细意思,请查看:AppLifecycleStatus documentation.
就像你可能注意到的,只有少数Activity生命周期事件被保留; 而FlutterActivity 确实在内部捕获几乎所有的activity生命周期事件,并将它们发送到Flutter引擎,它们大多数都被屏蔽了。Flutter负责为你启动和停止引擎,在大多数情况下在Flutter侧没有理由需要观察activity的生命周期。如果你需要观察生命周期以获取或释放任何本地资源,无论如何你应该在本地侧做这些事。
这里有一个如何观察包含的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()));
}
在Android中,一个LinearLayout用来线性的放置控件 – 要么水平要么垂直。在Flutter中,使用 Row 控件或者 Colum 控件来达到相同的目的。
你注意到两个代码示例除了“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控件的组合来实现RelativeLayout的结果。你可以为控件构造函数指定有关子级相对于父级的布局方式的规则。
有关在Flutter中构建RelativeLayout的一个很好的示例,请参阅Collin在StackOverflow 上的答案。
在Android中,使用ScrollView来放置控件 – 如果用户的设备的屏幕比要展示的内容小,那么可以滑动。
在Flutter中,最简单的方法是使用 ListView 控件。在Flutter中,ListView控件既是ScrollView又是Android中的 ListView。
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
如果AndroidManifest.xml包含以下内容,FlutterView将处理配置更改:
android:configChanges="orientation|screenSize"
在Android中,你可以给控件关联一个onClick,例如一个按钮通过调用“setOnClickListener”方法。
在Flutter中,有两种方法添加触摸监听:
@override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () {
print("click");
},
child: Text("Button"));
}
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
child: FlutterLogo(
size: 200.0,
),
onTap: () {
print("tap");
},
),
));
}
}
使用 GestureDetector ,你可以监听各种各样的手势,例如:
下面的例子展示了一个 GestureDetector 在双击后旋转Flutter的logo。
AnimationController controller;
CurvedAnimation curve;
@override
void initState() {
controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
}
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
child: RotationTransition(
turns: curve,
child: FlutterLogo(
size: 200.0,
)),
onDoubleTap: () {
if (controller.isCompleted) {
controller.reverse();
} else {
controller.forward();
}
},
),
));
}
}
在Flutter中与ListView等价的还是ListView!
在Android中的ListView,你创建一个适配器并把它传给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中,ListView有一个方法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;
}
}
在Android中,你通过调用notifyDataSetChanged来更新适配器。
在Flutter中,如果你在setState()中更新一个控件的列表,你很快就会发现你的数据没有直观的改变。这是因为当setState()被调用时,Flutter的渲染引擎会观察控件树是否有任何改变。当它处理你的ListView时,它执行==检查,并确定两个ListView是相同的。没有发生任何变化,所以不需要更新。
一个更新你的ListView的简单的方法是在setState()中创建一个新的List,并且从旧的List复制数据到新的List。虽然这种方法很简单,但是不建议用于大型数据集合,如下一个例子所示。
import 'package:flutter/material.dart';
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List widgets = <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。当你有一个动态列表或者大量数据的列表时这个方法非常有用。这基本上相当于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.builder ,而不是一个“ListView”,它接收两个关键参数:列表的初始长度和itemBuilder函数。
itemBuilder函数类似于Android中适配器的getView 函数;它有一个参数position,并返回你在当前position想要呈现的行。
最后,最重要的是,请注意onTap() 函数不是重新创建列表,而是添加add 。
在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控件的样式参数使用TextStyle 对象,你可以在其中自定义许多参数,例如:
有关使用表单的更多信息,请参考Retrieve the value of a text field ,Flutter Cookbook
在Flutter中,你可以方便的让输入框显示一个“hint”或者占位符文本,通过给输入框控件添加一个 InputDecoration 对象来装饰构造函数参数。
body: Center(
child: TextField(
decoration: InputDecoration(hintText: "This is a hint"),
)
)
正如设置“hint”那样,给输入框控件的构造函数传入一个InputDecoration 对象即可。
然而,你不想一开始就显示一个错误。相反,当用户输入了无效数据时,更新状态,并传入一个新的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);
}
}
使用 geolocator 社区插件。
使用非常受欢迎的 image_picker 插件访问相机。
如果存在特定于平台的功能,并且Flutter或者它的社区都没有该插件,你可以按照 Developing packages & plugins 构建你自己的插件。
Flutter插件的架构,简而言之,非常类似于在Android中使用事件总线(Event Bus):你发射一条消息,让接收者处理并将结果发回给你。在这种情况下,接收器是在Android或者iOS侧运行的代码。
如果你在你的当前的Android应用中使用了NDK,并且想让你的Flutter应用充分利用你的本地库,那么可以通过构建自定义插件来实现。
你的自定义插件首先告诉你的Android应用,你通过JNI调用本地函数的地方。一旦响应准备就绪后,将消息发送回Flutter并呈现结果。
目前不支持直接从Flutter调用本地代码。
开箱即用,Flutter带有完美的Material Design实现,它会负责你通常会做的很多样式和主题需求。不像Android,你需要在XML中声明主题,然后使用AndroidManifest.xml将其指定给你的应用,在Flutter中,你在顶级控件中声明主题。
要在你的应用中充分利用Material组件,你可以声明一个顶级控件MaterialApp 作为你的应用的入口。MaterialApp 是一个很方便的控件,它包含许多实现了Material Design的应用通常需要使用的控件。它通过添加特定的Material功能构建在WidgetsApp上。
你还可以使用WidgetApp作为app widget,它提供了一些相同的功能,但不如MaterialApp丰富。
要自定义任何子组件的颜色和样式,请将ThemeData对象传递给MaterialApp控件。例如,在下面的代码中,主样本设置为蓝色,文本选择颜色为红色。
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
textSelectionColor: Colors.red
),
home: SampleAppPage(),
);
}
}
在Android中,你可以使用SharedPreferences API存储少量的 key-value 集合。
在Flutter中,访问这个功能使用Shared_Preferences plugin。这个插件封装了Shared Preferences 和 NSUserDefaults (the iOS equivalent) 二者的功能。
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);
}
在Android中,你使用SQLite存储结构化数据,你可以使用SQL来查询。
在Flutter中,访问这个功能要使用插件:SQFlite 。
在Android中,您可以使用Firebase Cloud Messaging为您的应用设置推送通知。
在Flutter中,使用 Firebase_Messaging 插件访问此功能。有关使用Firebase Cloud Messaging API的更多信息,请参阅 firebase_messaging 插件文档。