UI 异步
runOnUiThread() 对应什么?
Dart 是一种支持 Isolate(多线程)、事件循环和异步编程的单线程模型。除非使用 Isolate,否则 Dart 代码将由事件循环器驱动运行在主线程中。事件循环等同于 Android 中的 main Looper 。
单线程模型并不意味着需要将所有代码都以阻塞 UI 的方式进行操作。可以使用 Dart 提供的异步工具,如 async/await
。
示例:使用 async/await
请求网络且不阻塞 UI
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
示例:加载数据,并显示到 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 {
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 中可能会用到 AsyncTask、LiveData、IntentService、JobSchedule、 RxJava
。
由于 Flutter 是单线程(类似 Node.js),执行事件循环的,所以不必担心如何创建和管理线程。
- async/await:IO 操作,如磁盘存储、网络请求等;
- Isolate:实现并发,类似线程,但不共享内存,是独立的程序执行环境,无法直接访问主线程变量或更新 UI。利用 CPU 多核的性质处理事务。默认环境 main isolate。
//示例:async/await
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
//示例: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 中。
完整示例
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 {
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;
}
}
OkHttp 对应什么?
使用 http package 请求网络。
虽然 http 扩展库并未实现 OkHttp 的所有功能,但抽象出了很多常用的功能。
//在 pubspec.yaml 中添加依赖
dependencies:
...
http: ^0.11.3+16
//使用
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);
});
}
}
如何显示耗时任务进度?
使用 ProgressIndicator。通过布尔型的标志来控制进度显示。
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 {
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);
});
}
}
项目结构与资源
分辨率相关的图片资源应该存储在哪里?
存储在 assets 资产文件夹下。
Flutter 遵循像 iOS 一样简单的分辨率格式,如 1.0x、2.0x、3.0x,或其它倍数。
没有单位 dp(device-independent pixels, dip
),但有和 dp 类似的逻辑像素(logical pixels
),称作 设备像素比例 devicePixelRatio —— 单个逻辑像素中物理像素的比例。
Android | Flutter |
---|---|
ldpi | 0.75x |
mdpi | 1.0x |
hdpi | 1.5x |
xhdpi | 2.0x |
xxhdpi | 3.0x |
xxxhdpi | 4.0x |
Flutter 没有预加的文件结构,所以资产文件(Assets)可以放在任意文件夹下。在 pubspec.yaml
中声明资产文件的路径后便可以使用。
示例
- 创建 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
- 使用图片资源
//1. AssetImage
return AssetImage("images/a_dot_burr.jpeg");
//2. Image Widget
@override
Widget build(BuildContext context) {
return Image.asset("images/my_image.png");
}
如何存储字符串,如何处多语言?
目前最佳方案:在类中添加静态字段。
class Strings {
static String welcomeMessage = "Welcome To Flutter";
}
//使用
Text(Strings.welcomeMessage)
未来会支持 Flutter 访问 Android 。
鼓励开发人员使用 Intl 包进行国际化和本地化操作。
Gradle 文件对应什么,如何添加依赖?
Flutter 通过 Dart 语言独自构建系统,并通过 Pub 包管理。这些工具将原生 Android 和 iOS 的构建托付给了各自的构建系统。
虽然在 Flutter 项目中的 Android 文件夹下有 Gradle 文件,但只有添加平台集成所需的依赖时才能使用(意思是需要单独添加?)。一般在pubspec.yaml
中声明外部依赖项。更多依赖参阅 Pub 。
Activity 与 Fragment
在 Flutter 中,此两种概念均属于 Widget 的范畴。
了解更多,参阅: Flutter For Android Developers : How to design an Activity UI in Flutter.
如何监听 Android Activity 的生命周期?
可以通过绑定 WidgetsBinding 观察者,并监听 didChangeAppLifecycleState() 事件来监听生命周期事件。
可监听生命周期事件:
- resumed:应用当前可见,可响应用户操作,等同于 Android 的 onResume();
- paused:应用当前对用户不可见,无法响应用户操作,后台运行,等同于 Android 的 onPause();
- suspending:应用暂停,仅适用 Android, 等同 onStop ();
- inactive:应用处于非活跃状态,未接收到用户操作,仅适用iOS;
更多关于生命状态的信息,参阅: AppLifecycleStatus 文档
FlutterActivity 几乎捕获了所有生命周期事件,并交由 Flutter 系统处理,所以大多数情况下,没有必要去监听处理。如果需要监听生命周期来获取或释放本地资源,那么应该在本地添加监听。
//示例
import 'package:flutter/widgets.dart';
class LifecycleWatcher extends StatefulWidget {
@override
_LifecycleWatcherState createState() => _LifecycleWatcherState();
}
class _LifecycleWatcherState extends State 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 对应什么?
Flutter 中使用 Row 或 Column 可以达到同样的效果。
//row
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
//column
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Column One'),
Text('Column Two'),
Text('Column Three'),
Text('Column Four'),
],
);
}
了解更多,参阅: Flutter For Android Developers : How to design LinearLayout in Flutter?
RelativeLayout 对应什么?
实现方式:
- 通过组合 Column、Row、Stack;
- 指定子部件相对于父部件的布局规则。
一个很好的示例: StackOverflow 。
ScrollView 对应什么?
最简单的方式是使用 ListView ,Flutter 中的 ListView 等同于 Scroll View 和 Android 中的 ListView。
@override
Widget build(BuildContext context) {
return ListView(
children: [
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
如何处理横屏切换?
当 AndroidManifest.xml 添加如下内容,FlutterView 将会处理配置更改:
android:configChanges="orientation|screenSize"
手势检测与触摸事件处理
Widget 如何添加点击事件?
两种方式:
- Widget 支持事件检测,则可直接传递函数并处理:
@override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () {
print("click");
},
child: Text("Button"));
}
- Widget 不支持事件检测,则使用
GestureDetector
包装 Widget,并将函数传递给参数onTab
:
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
child: FlutterLogo(
size: 200.0,
),
onTap: () {
print("tap");
},
),
));
}
}
如何处理 Widget 的其他手势?
使用 GestureDetector 可以监听到很多手势:
- 点击 Tab
-
onTabDown
: 按下 -
onTabUp
: 抬起 -
onTab
: 点击 -
onTabCancel
: 触发 onTabDown ,但未触发 onTabUp
-
- 双击 Double tab
-
onDoubleTab
: 快速点击同一位置两次
-
- 长按 Long press
-
onLongPress
:长时间按住某一位置
-
- 垂直拖动 Vertical drag
-
onVerticalDragStart
: 手势按下,开始垂直移动 -
onVerticalDragUpdate
: 手势滑动,屏幕跟随垂直移动 -
onVerticalDragEnd
: 手势抬起,屏幕按照特定速度垂直滚动
-
- 水平拖动 Horizontal drag
-
onHorizontalDragStart
:手势按下,开始水平移动 -
onHorizontalDragUpdate
: 手势滑动,屏幕跟随水平移动 -
onHorizontalDragEnd
:手势抬起,屏幕按照特定速度水平滚动
-
示例:双击旋转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
ListView 对应什么?
对应 ListView。
由于 Flutter Widget 是不可变的,只需要将一个 List 传递给 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 {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: ListView(children: _getListData()),
);
}
_getListData() {
List widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(Padding(padding: EdgeInsets.all(10.0), child: Text("Row $i")));
}
return widgets;
}
}
如何知道点击了哪个子项?
通过传入的子项 Widget 的手势事件进行处理。
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 {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: ListView(children: _getListData()),
);
}
_getListData() {
List 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 ?
此时,使用 setState() 方法并不会更新。因为调用 setState() 时,Flutter 渲染引擎会遍历 Widget 树(所有 Widget )查看是否有改变。当遍历到 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 {
List widgets = [];
@override
void initState() {
super.initState();
for (int i = 0; i < 100; i++) {
widgets.add(getRow(i));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: ListView(children: widgets),
);
}
Widget getRow(int i) {
return GestureDetector(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text("Row $i")),
onTap: () {
setState(() {
widgets = List.from(widgets);
widgets.add(getRow(widgets.length + 1));
print('row $i');
});
},
);
}
}
推荐方式
推荐使用 ListView.Builder,十分适用于大数据量的动态更新。本质上与 Android 中的 RecyclerView 相同——会自动复用表元素。
两个参数:
- itemCount:列表初始长度
- itemBuilder:类似 Android 适配器中的 getView 方法
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 {
List widgets = [];
@override
void initState() {
super.initState();
for (int i = 0; i < 100; i++) {
widgets.add(getRow(i));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
}));
}
Widget getRow(int i) {
return GestureDetector(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text("Row $i")),
onTap: () {
setState(() {
widgets.add(getRow(widgets.length + 1));
print('row $i');
});
},
);
}
}
注意:onTab()
方法中并没有重新创建列表,而是向列表中添加了新元素。
文本使用
Text Widget 如何自定义字体?
将字体文件存入项目文件夹,并在 pubspec.yaml
中声明,类似使用图片文件。
//声明
fonts:
- family: MyCustomFont
fonts:
- asset: fonts/MyCustomFont.ttf
- style: italic
//使用
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: Center(
child: Text(
'This is a custom font text',
style: TextStyle(fontFamily: 'MyCustomFont'),
),
),
);
}
Text Widget 如何设置样式?
通过 Text Widget 的 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 ?
body: Center(
child: TextField(
decoration: InputDecoration(hintText: "This is a hint"),
)
)
如何显示验证错误信息 ?
通过 setState 更新,并传递新的 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 {
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);
}
}