Flutter 学习笔记

2019-03-06

目录:

  1. Views 视图

    1.1 widgets
    1.2 动态更新 UI
    1.3 增加或者隐藏一个控件
    1.4 增加动画效果
    1.5 画布的使用
    1.6 自定义组件

  2. Intents 意图

    2.1 指定跳转路线
    2.2 直接跳到目标
    2.3 使用插件

  3. Async UI 异步更新UI

    3.1 与 Android 的区别
    3.2 async / await
    3.3 如何移动任务到后台进程
    3.4 关于网络框架
    3.5 关于进度条

  4. Project structure & resources 项目结构&资源

    差异1:图片资源
    差异2:string.xml
    差异3:lib 资源,使用框架

  5. Activities and fragments 界面

    5.1 Activities and fragments VS Widgets
    5.2 如何监听 Activities 的生命周期事件

  6. Layouts 布局

    LinearLayout
    RelativeLayout
    ScrollView

  7. Gesture detection and touch event handling 手势和触屏事件处理

    第一,如何处理手机屏幕转动的问题?
    第二,如何小部件的点击事件?

  8. Listviews & adapters
  9. Working with text 文本框

    设置字体
    设置风格

  10. Form input 输入框

    [1] 增加提示 hint
    [2] 设置允许输入字符类型

  11. Themes 主题
  12. Databases and local storage 数据库和本地存储

    [1] Shared Preferences
    [2] SQLite

  13. 总结

1. Views


在 Android 中, 视图是展示在屏幕上的所有的基础。例如 按钮、工具栏、输入框等都是一个视图。在 Flutter 中,Widget 粗略的等同于 Android 中的 View.

Widget 有不同于 Android 视图的生命周期:她们是不可改变的并且只有在需要改变的时候才存在。对比 Android, 视图只绘制一次,并且在它无效之前不会被重新绘制。

如何去理解这句话呢?
Widget 只是 UI 的描述,然后编译的时候被对应到引擎的实际视图对象中而显现出来,不需要绘制任何东西。因此,Widget 是轻量级的。
这就像是你只是根据 Flutter 的语义描述了这个界面应该是怎么样的,然后这些描述在编译解释对应到引擎下的实际视图对象。

原文描述:
In Android, the View is the foundation of everything that shows up on the screen. Buttons, toolbars, and inputs, everything is a View. In Flutter, the rough equivalent to a View is a Widget

widgets have a different lifespan: they are immutable and only exist until they need to be changed. In comparison, an Android view is drawn once and does not redraw until invalidate is called.

1.1 widgets


[1] StatelessWidgets (静态部件)

静态部件用在不需要变化的地方,例如说 app 的 logo . icon 是放在资源文件下,而不是网络请求获取的; ImageView 的显示是可以写固定的。

StatelessWidgets are useful when the part of the user interface you are describing does not depend on anything other than the configuration information in the object. == For example, in Android, this is similar to placing an ImageView with your logo. The logo is not going to change during runtime, so use a StatelessWidget in Flutter.

[2] StatefulWidget(状态部件)

状态部件用在动态变化的地方,例如说 ListView 显示服务端返回的数据。

If you want to dynamically change the UI based on data received after making an HTTP call or user interaction then you have to work with StatefulWidget and tell the Flutter framework that the widget’s State has been updated so it can update that widget.

1.2 动态更新 UI ,例如: 点击 FloatingActionButton 改变 Text 显示的文字。


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(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),
),
);
}
}

效果如下:
2019-6-1

1.3 增加或者隐藏一个组件,例如 原本显示一个 Text , FloatingActionButton 点击时显示一个 MaterialButton.


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
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: Icon(Icons.account_balance),padding: EdgeInsets.only(left: 10.0, right: 10.0));
}
}

@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),
),
);
}
}

效果如下:
2019-6-1

1.4 增加动画效果,示例:FloatingActionButton 点击后动画显示出 Icon.


In Flutter, use an AnimationController which is an Animation<double> that can pause, seek, stop and reverse the animation. It requires a Ticker that signals when vsync happens, and produces a linear interpolation between 0 and 1 on each frame while it’s running. You then create one or more Animations and attach them to the controller.

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
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();
},
),
);
}
}

效果显示如下,logo 动态淡入。
2019-6-1

1.5 画布的使用


创建一个手势区域,使用 GestureDetector 去记录触碰路径,CustomPaint 去绘制显示在屏幕上。

create a signature area using GestureDetector to record touches and CustomPaint to draw on the screen. Here are a few tips:

  • Use RenderBox.globalToLocalto convert the DragUpdateDetails provided by GestureDetector.onPanUpdate into relative coordinates
  • Use a GestureDetector.onPanEnd gesture handler to record the breaks between strokes.
  • Mutating the same List won’t automatically trigger a repaint because the CustomPainter constructor arguments are the same. You can trigger a repaint by creating a new List each time a new point is provided.
  • Use Canvas.drawLine to draw a rounded line between each of the recorded points of the signature.

–这段文字来源于Collin Jackson 在 stackoverflow 上的回答。更多的源码可以在他的 github 上找到。

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
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: SignaturePainter(_points), size: Size.infinite),
],
);
}
}

class DemoApp extends StatelessWidget {
Widget build(BuildContext context) => new Scaffold(body: new Signature());
}

void main() => runApp(new MaterialApp(home: new DemoApp()));

1.6 自定义组件
在 Android 中自定义组件是通过继承 View 或者是 TextView 这类已经拥有一些特性的组件,然后重写父类的方法或者实现接口。
在 Flutter 中,构建一个自定义组件是通过组合小部件。有点类似 Android 中的 ViewGroup.

例如,你要自定义一个 button 中含有 图片和 文字。

In Android, you typically subclass View, or use a pre-existing view, to override and implement methods that achieve the desired behavior.

In Flutter, build a custom widget by composing smaller widgets (instead of extending them). It is somewhat similar to implementing a custom ViewGroup in Android, where all the building blocks are already existing, but you provide a different behavior—for example, custom layout logic.

For example, how do you build a CustomButton that takes a label in the constructor? Create a CustomButton that composes a RaisedButton with a label, rather than by extending RaisedButton:

1
2
3
4
5
6
7
8
9
10
class CustomButton extends StatelessWidget {
final String label;

CustomButton(this.label);

@override
Widget build(BuildContext context) {
return RaisedButton(onPressed: () {}, child: Text(label));
}
}

Then use CustomButton, just as you’d use any other Flutter widget:

1
2
3
4
5
6
@override
Widget build(BuildContext context) {
return Center(
child: CustomButton("Hello"),
);
}

#2. Intents 意图

2.1 Android 中有显式 Intent 和 隐式 Intent ,作为 Activity 之间的导航仪,实现界面的跳转。但 Flutter 没有与之相对应的概念,由于 Flutter 没有很多的 Activities 和 Fragments ,需要实现 Activity 间的通信。因此,在 Flutter 中实现界面的跳转等操作是通过 Navigator 和 Routes,全都在相同的一个 Activity 中使用。
Navigator 和 Routes 的关系:
Route 是app 中页面或者界面的抽象,Navigator 是管理 Route 的组件(管理机制类似栈)。
A Route is an abstraction for a “screen” or “page” of an app, and a Navigator is a widget that manages routes

2.2 实现界面的跳转有几种方式:
[1] Specify a Map of route names.

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

[2] Directly navigate to a route.

1
Navigator.of(context).pushNamed('/b');

[3] 使用插件 android_intent 0.3.0+2
这个 插件只支持 android 系统,如果在 ios 中使用会闪退。

1
2
3
4
5
6
7
8
9
if (platform.isAndroid) {
AndroidIntent intent = AndroidIntent(
action: 'action_view',
data: 'https://play.google.com/store/apps/details?'
'id=com.google.android.apps.myapp',
arguments: {'authAccount': currentUserEmail},
);
await intent.launch();
}

[4] 对于一些常用的调用摄像头或者打开文件夹等操作,需要建立一个原生的平台去集成。具体参考

3. Async UI 异步更新UI (runOnUiThread)


3.1 与 Android 的区别:

Dart 是一个单线程执行的模型,但也支持 Isolates (一种使 Dart 代码运行在另外一个线程的方式),也支持 Event Loop (事件循环) 和 asynchronous programming(异步)。Dart 的机制是除非你开启了一个Isolates,否则代码由 Event Loop 驱动运行在主线程。Dart 的 event loop 相当于 android 的 main looper 。
不同于 android 需要一直保持主线程空闲的状态,Flutter 使用 Dart 提供的异步机制,例如 async / await 去完成异步任务。

3.2 具体用法

[1] 使用 async / await 去做一些重量级的操作,例如网络请求等。
[2] 网络请求完成后,通过调用设置状态更新 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
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(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);
});
}
}

3.3 如何移动任务到后台进程

在 Android 中为了避免阻塞主线程和 ANRs,你可能会使用 AsyncTask、LiveData、IntentService、JobScheduler job、RxJava 管道这些执行在后台线程的调度程序去完成访问网络资源工作。
由于 Flutter 是单线程模型和运行一个事件循环机制,因此作为开发人员,不需要担忧线程管理或者产生大量的后台线程。如果你要执行 IO 范围内的任务,例如磁盘访问或网络请求,那你可以用 async/await 。除此之外,如果你需要完成密集型计算的工作让 CPU 一直处于busy 的状态,那么你可以把这个任务移到 Isolate 去避免阻塞事件循环。
Isolates are separate execution threads that do not share any memory with the main execution memory heap. This means you can’t access variables from the main thread, or update your UI by calling setState(). Unlike Android threads, Isolates are true to their name, and cannot share memory (in the form of static fields, for example).

具体示例代码:

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
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;
}
}

3.4 小主,在 Flutter 可以使用 OkHttp 吗?如何使用呢?
事实上, Flutter 有自己的网络请求框架 http(等同于 android 的 OkHttp )。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[1]  pubspec.yaml
dependencies:
...
http: ^0.11.3+16

[2] 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);
});
}
}

3.5 小主,那如何添加进度条呢?
Android 中有 ProgressBar , Flutter 中有 ProgressIndicator。

1
2
3
4
5
6
7
8
9
10
11
12
13
showLoadingDialog() {
return widgets.length == 0;
}

getBody() {
if (showLoadingDialog()) {
return getProgressDialog();
}
}

getProgressDialog() {
return Center(child: CircularProgressIndicator());
}

4. Project structure & resources 项目结构&资源


新创建的 Flutter Project 的结构是如下图的。
2019-6-17

对比 Android Project 的结构:
2019-6-17

差异1: 小主,没有 res 资源包,那我的图片等资源文件应该放在哪里?怎么调用呢?
Android 会对资源(resources)和资产(assets)进行分类,但在 Flutter 中只有assets 的概念。当然,也就没有 dps 之分, 而是用像素区分。
2019-6-17
怎么添加图片?
[1] 首先在项目根目录下创建 images 文件夹,
[2] 然后把图片复制黏贴到 images 目录下,
[3] 其次在 pubspec.yaml ,引入 assets: - images/my_icon.jpeg,
[4] 最后在代码中调用 @override
Widget build(BuildContext context) {
return Image.asset(“images/my_image.png”);
}
区分不同像素的图片:

1
2
3
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

差异2 : 小主小主,还有一个问题呐, string.xml 的字符在 Flutter 中是怎么使用的呢?
Flutter 没有等同于 Android 的 string.xml , 因此对于项目的常量字符串,你可以创建一个 Strings 类,然后在代码中调用。

1
2
3
4
5
6
1.Strings 类
class Strings {
static String welcomeMessage = "Hello world !";
}
2.代码中调用
Text(Strings.welcomeMessage)

差异3: 小主小主,我前面看了你使用网络请求框架,那我以前用的视频播放、Json 解析这些开源框架都不用了,那我岂不是全部要手动造轮子???
是的,赶紧造轮子去吧(偷笑),多开源几个,为 Flutter 做贡献(一脸认真滴样子)!所有的 Flutter 开源项目 在这 Pub~

5. Activities and fragments 界面


5.1 Activities and fragments VS Widgets
我们都知道,在 Android 中,Activity 扮演者用户交互接口的角色,即用户界面。Fragment 是行为或者用户界面的碎片,用于模块化代码,组成复杂的界面。在 Flutter ,这两个概念都集中到了 Widgets 中。任何一切都是一个小部件(widget),不同的路由(Routes) 代表不同的页面。

5.2 如何监听 Activities 的生命周期事件
在 Android 中,你可以重写 Activity 的方法去捕获自身 Activity 的生命周期事件,或者在 Application 注册一个 ActivityLifecycleCallbacks。在 Flutter 中,因为没有 Activity 的概念,因此此路不通,提供的新方法是通过 WidgetsBinding 观察者和监听 didChangeAppLifecycleState() 变化事件。
可以观察到的生命周期事件:
[1] inactive不活跃的:处于不活跃状态不能接收用户输入。这种情况是工作在 IOS 系统上时产生的,Android 中没有对应的事件。
[2] paused 暂停中止的:程序对用户不可见,不响应用户操作,运行在后台,这相当于 Android 的 onPause().
[3] resumed 重新建立,恢复:应用程序对用户可见,并且可以接收用户输入。这相当于 Android 的 onResume().
[4] suspending 悬浮:应用程序暂停片刻,相当于 Android 的 onStop()。这种情况不会在 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() => _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()));
}

6. Layouts 布局


【1】 LinearLayout
水平线性布局 == Row widget(行)

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

垂直线性布局 == Column widget(列)

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

【2】 RelativeLayout
相对布局是根据组件与组件之间相对关系进行排布,这在 Flutter 中是几乎没有直接与之相对的实现方式,不过你可以通过组合 Row、Column 和 Stack 等小部件去实现,也可以为部件构造函数指定规则去规定孩子部件与父类部件的相对关系。
【3】 ScrollView
在 Flutter 中 a ListView widget 是包含 ScrollView 和 Android 中的 ListView.

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

###7. Gesture detection and touch event handling 手势和触屏事件处理

第一,如何处理手机屏幕转动的问题?
In AndroidManifest.xml

1
android:configChanges="orientation|screenSize"

第二,如何小部件的点击事件?
情况1,小部件默认有点击事件,例如 RaisedButton.

1
2
3
4
5
6
7
8
@override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () {
print("click");
},
child: Text("Button"));
}

情况2,小部件没有自带点击点击事件,在部件外层添加 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 Scaffold(
body: Center(
child: GestureDetector(
child: FlutterLogo(
size: 200.0,
),
onTap: () {
print("tap");
},
),
));
}
}

8. Listviews & adapters


在 Android 中使用 Listviews,首先创建一个 adapter ,然后将 adapter 传入 listview ,listview 的每一行与 adapter 返回的相对应。然而,你必须确保你有回收你的行,不然会出现很多的小故障和记忆问题。

在 Flutter 中,由于小部件是不可变模式,即静态模式,要动态添加数据填充,你需要增加一系列的小部件到 listview . 而 Flitter 负责保证滑动的流畅性和速度。
同样的,如果需要为 listview 添加点击事件呢,应该如何操作?
在小部件外层增加一个 GestureDetector

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(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;
}
}

9. Working with text 文本框


[1] 设置字体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1. In the pubspec.yaml file
fonts:
- family: MyCustomFont
fonts:
- asset: fonts/MyCustomFont.ttf
- style: italic
2. assign the font to your Text widget
@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'),
),
),
);
}

[2] 设置风格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
*   color
* decoration
* decorationColor
* decorationStyle
* fontFamily
* fontSize
* fontStyle
* fontWeight
* hashCode
* height
* inherit
* letterSpacing
* textBaseline
* wordSpacing

10. Form input 输入框


[1] 增加提示 hint

1
2
3
4
5
body: Center(
child: TextField(
decoration: InputDecoration(hintText: "Please Input number"),
)
)

[2] 设置允许输入字符类型

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
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);
}
}

11. Themes 主题


最钟意的环节终于来了,我真是一个有审美要求的程序媛!(隔壁穿着格子衫,牛仔裤的她投来鄙视的一眼👀)
曾经我为了让我的 app 运用 Material Design 风格的组件,把所有与之发生冲突的依赖包换成了手造轮子或者找另外的开源替换。
对于 Flutter ,开箱即用的各种 Material Design Style 小部件,先看看怎么用吧。
在 Flutter 最上层部件声明 theme ,就像这样👇

1
2
3
4
5
6
7
8
9
10
11
12
13
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
textSelectionColor: Colors.red
),
home: SampleAppPage(),
);
}
}

#12. Databases and local storage 数据库和本地存储

[1] Shared Preferences
在 Android 中, 以 key-value 的形式把小规模的数据存入。
在 Flutter 中,需要加入 shared_preferences 0.5.3+ 插件,它包括 Android 的 Shared Preferences 和 IOS 的 NSUserDefaults.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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);
}

[2] SQLite
同样的,需要使用 sqflite 1.1.5 插件,具体用法参考 sqflite 1.1.5 .

#13. 总结

文章内容来自学习 Flutter官网 的知识,仅做学习记录的用处,不做商业用途。文中的中文是自己翻译的,由于本人水平有限,故把部分经典英文原文也贴出,免得误人子弟,故若有不当之处请指出,谢谢!

二维码

作者:Emily CH
2019年6月7日