2018-03-03 | learn

[翻译] Flutter for Android part 2

https://flutter.io/flutter-for-android/#async-ui

[toc]

Async UI

Flutter 中的 runOnUiThread 是啥

Dart 是单线程模型的,提供 Isolates (一种在其他线程运行 dart 的方式),Event Loop,异步编程。除了创建 Isolate,你的代码都将运行在主 UI 线程,并由 Event Loop 驱动。

比如你可以在 UI 线程进行网络请求而不阻塞 UI

1
2
3
4
5
6
7
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = JSON.decode(response.body);
});
}

使用 setState 触发 build 方法,更新数据,更新 UI。
先面是个 异步加载数据并显示 ListView 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
runApp(new SampleApp());
}

class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Sample App',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new SampleAppPage(),
);
}
}

class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);

@override
_SampleAppPageState createState() => new _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
List widgets = [];

@override
void initState() {
super.initState();

loadData();
}

@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Sample App"),
),
body: new ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
}));
}

Widget getRow(int i) {
return new Padding(
padding: new EdgeInsets.all(10.0),
child: new 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 或是 IntentService

Android 在访问网络数据时可以创建一个 AsyncTask 在 UI 线程外执行,避免阻塞主线程。AsyncTask 有个线程池管理 thread。

Flutter 是单线程的啊,只有 event loop (比如 Node.js),用不着操心线程什么乱七八糟的。

想异步执行代码,只要把函数声明成 async 就行,await 等待结果返回。

1
2
3
4
5
6
7
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = JSON.decode(response.body);
});
}

这就是常用的网络或是数据库请求了。

Android 继承 AsyncTask 要重写好多方法,我们 Flutter 根本用不着,直接 await ,剩下的交给 Dart 的 event loop 就行了。

不过当你处理大量数据时 UI 仍有被阻塞的可能。

这种场景,Flutter 有机会用到多核处理器的优势执行耗时操作,或是密集计算型 task。轮到 Isolates 出场了。

Isolates 是单独执行的线程,不和主线程共享数据。在这你不能访问主线程的变量,也不能调用 setState 更新 UI。

看代码,我们将如何将数据返回给主线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
loadData() async {
ReceivePort receivePort = new ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);

// The 'echo' isolate sends it's 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 = new 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 = new ReceivePort();
port.send([msg, response.sendPort]);
return response.first;
}

dataLoader 将执行在 Isolate 线程,可以做更多 CPU 密集的操作。

完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'dart:isolate';

void main() {
runApp(new SampleApp());
}

class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Sample App',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new SampleAppPage(),
);
}
}

class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);

@override
_SampleAppPageState createState() => new _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 new Center(child: new CircularProgressIndicator());
}

@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Sample App"),
),
body: getBody());
}

ListView getListView() => new ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
});

Widget getRow(int i) {
return new Padding(padding: new EdgeInsets.all(10.0), child: new Text("Row ${widgets[i]["title"]}"));
}

loadData() async {
ReceivePort receivePort = new ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);

// The 'echo' isolate sends it's 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 = new 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 = new ReceivePort();
port.send([msg, response.sendPort]);
return response.first;
}

}

Flutter 可以用啥替代 OkHttp

普通点,直接用 http package 就行。

尽管 http 包和 OkHttp 差了点 feature 蛤,但他也抽象了很多常用的方法,让网络请求更简单。

https://pub.dartlang.org/packages/http

把依赖加到 pubspec.yaml 就能用了

1
2
3
dependencies:
...
http: '>=0.11.3+12'

跑个网络请求吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
});
}
}

拿到数据咱们直接就可以用 setState 更新 UI

task 执行时如何显示进度条

一般执行耗时操作都会显示一个进度条。

Flutter 可以渲染一个 Progress Indicator widget。你可以在执行耗时操作前通过代码控制何时渲染一个 Progress Indicator。

下边的例子,把 build function 分成三部分。if showLoadingDialog == true (当 widgets.length == 0)就渲染 ProgressIndicator,否则渲染 ListView。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
runApp(new SampleApp());
}

class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Sample App',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new SampleAppPage(),
);
}
}

class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);

@override
_SampleAppPageState createState() => new _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 new Center(child: new CircularProgressIndicator());
}

@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Sample App"),
),
body: getBody());
}

ListView getListView() => new ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
});

Widget getRow(int i) {
return new Padding(padding: new EdgeInsets.all(10.0), child: new 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);
});
}
}

Project Structure & Resources

如何存放多分辨率资源分拣 HDPI/XXHDPI

Flutter 资源文件和 iOS 规则相似。1x, 2x, and 3x.

如下存放文件即可:

  • …/my_icon.png
  • …/2.0x/my_icon.png
  • …/3.0x/my_icon.png

并在 pubspec.yaml 文件中声明:

1
2
3
4
```yaml
assets:
- images/a_dot_burr.jpeg
- images/a_dot_ham.jpeg

最后就可以使用了:

1
return new AssetImage("images/a_dot_burr.jpeg");

如何保存字符串并处理本地化

当前的最佳实践是创建一个 String 类。

1
2
3
class Strings{
static String welcomeMessage = "Welcome To Flutter";
}

并使用:
1
new Text(Strings.welcomeMessage)

Flutter 对 Android 提供基本的访问,不过正在开发进程中。
我们推荐使用 intl package 开发国际化相关内容。

什么等价于 gradle 配置文件,管理依赖

Flutter 项目的 Android 路径下有 gradle 的配置文件,可以添加 Android 平台的依赖。Flutter 的依赖声明在 pubspec.yaml 文件。
可以到 Pub 这里找到更多 flutter 的 package。

Activities and Fragments

什么等价于 activities and fragments

在 Flutter 里这两个概念都由 widget 承载。

如何监听 Android 生命周期

可以 hook 一个 WidgetsBinding observer,监听 didChangeAppLifecycleState 事件。

生命周期相关的事件有:

  • resumed - 应用处于可见状态,并可以响应用户输入。对应 Android 的 onResume
  • inactive - 应用处于非活跃状态,不相应用户输入。 只用于 iOS 。
  • paused - 可见,不相应输入,运行在后台。 对应 Android 的 onPause
  • suspending - 应用暂时被挂起。 不用于 iOS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import 'package:flutter/widgets.dart';

class LifecycleWatcher extends StatefulWidget {
@override
_LifecycleWatcherState createState() => new _LifecycleWatcherState();
}

class _LifecycleWatcherState extends State<LifecycleWatcher> with WidgetsBindingObserver {
AppLifecycleState _lastLifecyleState;

@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}

@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
setState(() {
_lastLifecyleState = state;
});
}

@override
Widget build(BuildContext context) {
if (_lastLifecyleState == null)
return new Text('This widget has not observed any lifecycle changes.', textDirection: TextDirection.ltr);
return new Text('The most recent lifecycle state this widget observed was: $_lastLifecyleState.',
textDirection: TextDirection.ltr);
}
}

void main() {
runApp(new Center(child: new LifecycleWatcher()));
}

Layouts

LinearLayout 有没有

Android 使用 LinearLayout 垂直或水平布局 widget,Flutter 里使用 Row widget or Column widget 。

观察下面两段代码,除了 “Row” 和 “Column” 这两个 widget 不同,剩下的都一样。 利用这一特性可以对同一子节点进行额外的变化从而开发出多样的布局。

1
2
3
4
5
6
7
8
9
10
11
12
@override
Widget build(BuildContext context) {
return new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text('Row One'),
new Text('Row Two'),
new Text('Row Three'),
new Text('Row Four'),
],
);
}
1
2
3
4
5
6
7
8
9
10
11
12
@override
Widget build(BuildContext context) {
return new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text('Column One'),
new Text('Column Two'),
new Text('Column Three'),
new Text('Column Four'),
],
);
}

RelativeLayout 怎么说

相对布局,Flutter 可以有很多方式完成同样的效果。
你可以将 Column, Row, and Stack widgets 这些组件组合,也可以在 widget 的构造函数处声明 child widget 相对于其 parent widget 如何布局。

可以看看这个 StackOverflow 里的问题,不错的例子 https://stackoverflow.com/questions/44396075/equivalent-of-relativelayout-in -flutter

ScrollView 怎么写

Android 开发中我们用 ScrollView 承载一个屏幕装不下的内容。
Flutter 里最简单的就是用 ListView 去做。从 Android 开发的角度看,有些杀鸡焉用牛刀了,不过 Flutter 的 ListView 就是用于这个场景的,它同时对应 Android 里的 ScrollView 和 ListView 这两个控件。

手势识别和触摸事件响应

怎么给 Flutter 里的 widget 添加点击事件

Android 使用 view 的 setOnClickListener 添加点击事件
Flutter 有两种方式:

  1. widget 提供事件响应,我们只需要传入处理函数即可。比如 RaisedButton 提供 onPressed 属性。
    1
    2
    3
    4
    5
    6
    7
    8
    @overrid	e
    Widget build(BuildContext context) {
    return new RaisedButton(
    onPressed: () {
    print("click");
    },
    child: new Text("Button"));
    }
  2. 当widget 不提供事件时,你可以用 GestureDetector 把它封装一下,再向 onTap 参数传入处理函数即可:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class SampleApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return new Scaffold(
    body: new Center(
    child: new GestureDetector(
    child: new FlutterLogo(
    size: 200.0,
    ),
    onTap: () {
    print("tap");
    },
    ),
    ));
    }
    }

除了点击之外事件呢?

GestureDetector 为我们提供了丰富的事件类型:

  • Tap

    • onTapDown 类似 MotionEvent.ACTION_DOWN
    • onTapUp MotionEvent.ACTION_UP
    • onTap onClick
    • onTapCancel MotionEvent.ACTION_CANCEL
  • Double tap

    • onDoubleTap The user has tapped the screen at the same location twice in quick succession.
  • Long press

    • onLongPress A pointer has remained in contact with the screen at the same location for a long period of time.
  • Vertical drag

    • onVerticalDragStart A pointer has contacted the screen and might begin to move vertically.
    • onVerticalDragUpdate A pointer that is in contact with the screen and moving vertically has moved in the vertical direction.
    • onVerticalDragEnd A pointer that was previously in contact with the screen and moving vertically is no longer in contact with the screen and was moving at a specific velocity when it stopped contacting the screen.
  • Horizontal drag

    • onHorizontalDragStart A pointer has contacted the screen and might begin to move horizontally.
    • onHorizontalDragUpdate A pointer that is in contact with the screen and moving horizontally has moved in the horizontal direction.
    • onHorizontalDragEnd A pointer that was previously in contact with the screen and moving horizontally is no longer in contact with the screen and was moving at a specific velocity when it stopped contacting the screen.

一个双击旋转的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
AnimationController controller;
CurvedAnimation curve;

@override
void initState() {
controller = new AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
curve = new CurvedAnimation(parent: controller, curve: Curves.easeIn);
}

class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new GestureDetector(
child: new RotationTransition(
turns: curve,
child: new FlutterLogo(
size: 200.0,
)),
onDoubleTap: () {
if (controller.isCompleted) {
controller.reverse();
} else {
controller.forward();
}
},
),
));
}
}

Listviews & Adapters

Flutter 用啥替代 ListView

对的,用 ListView…
Android 里我们创建 adapter 控制如何渲染每个 child,同时我们还要负责复用 view,避免浪费大量的内存和附带的卡顿问题。

由于 Flutter 的 widget 是 immutable 的,你只负责生成 ListView , Flutter 来确视图的顺畅平滑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import 'package:flutter/material.dart';

void main() {
runApp(new SampleApp());
}

class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Sample App',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new SampleAppPage(),
);
}
}

class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);

@override
_SampleAppPageState createState() => new _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Sample App"),
),
body: new ListView(children: _getListData()),
);
}

_getListData() {
List<Widget> widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(new Padding(padding: new EdgeInsets.all(10.0), child: new Text("Row $i")));
}
return widgets;
}
}

如何给 list 的 item 设置事件

Android 提供 onItemClickListener。Flutter 里可以直接单独为每一个 child 设置 handler。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import 'package:flutter/material.dart';

void main() {
runApp(new SampleApp());
}

class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Sample App',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new SampleAppPage(),
);
}
}

class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);

@override
_SampleAppPageState createState() => new _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Sample App"),
),
body: new ListView(children: _getListData()),
);
}

_getListData() {
List<Widget> widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(new GestureDetector(
child: new Padding(
padding: new EdgeInsets.all(10.0),
child: new Text("Row $i")),
onTap: () {
print('row tapped');
},
));
}
return widgets;
}
}

如何动态更新 ListView

Android 使用 notifyDataSetChanged 通知数据变化。Flutter 通过 setState() 方法设置数据后,视图并没有随之变化。

setState() 调用后,Flutter 会遍历所有 widget 检查是否发生变动。到你的 ListView 时,会执行 == 操作,发现前后两个 ListView 是一样的,所以不会有视图更新。

想让视图更新你得在 setState() 的时候创建一个新的 List,并且自己手动吧老数据拷过来。例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import 'package:flutter/material.dart';

void main() {
runApp(new SampleApp());
}

class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Sample App',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new SampleAppPage(),
);
}
}

class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);

@override
_SampleAppPageState createState() => new _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
List widgets = [];

@override
void initState() {
super.initState();
for (int i = 0; i < 100; i++) {
widgets.add(getRow(i));
}
}

@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Sample App"),
),
body: new ListView(children: widgets),
);
}

Widget getRow(int i) {
return new GestureDetector(
child: new Padding(
padding: new EdgeInsets.all(10.0),
child: new Text("Row $i")),
onTap: () {
setState(() {
widgets = new List.from(widgets);
widgets.add(getRow(widgets.length + 1));
print('row $i');
});
},
);
}
}

不过,推荐还是使用 ListView.Builder,更高效,实用。当你处理大量数据或是一个动态 List 时很有用。 等同于 Andorid 里的 RecyclerView。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import 'package:flutter/material.dart';

void main() {
runApp(new SampleApp());
}

class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Sample App',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new SampleAppPage(),
);
}
}

class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);

@override
_SampleAppPageState createState() => new _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
List widgets = [];

@override
void initState() {
super.initState();
for (int i = 0; i < 100; i++) {
widgets.add(getRow(i));
}
}

@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Sample App"),
),
body: new ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
}));
}

Widget getRow(int i) {
return new GestureDetector(
child: new Padding(
padding: new EdgeInsets.all(10.0),
child: new Text("Row $i")),
onTap: () {
setState(() {
widgets.add(getRow(widgets.length + 1));
print('row $i');
});
},
);
}
}

我们用 new ListView.builder 代替了直接创建新的 ListView。build 接受两个参数,初始长度和一个 ItemBuilder 函数。
功能上 ItemBuilder 和 Android 中 Adapter 的 getView() 一样,返回对应位置 child 样式。
并且,最重要的是我们没有在 onTap 函数里重新创建 List,只调用了一个 add 方法。