2018-03-01 | learn

[翻译] Flutter for Android part 1

https://flutter.io/flutter-for-android/

[toc]

View

在 Flutter 里什么等价于 View

View 是一切 Android 控件的基石。无论是 Button, Toolbar 或者 Input 都是 View。Flutter 中的 View 是 Widget,但有几点不同。首先,widget 的生命周期只有一帧,flutter 框架会为每一帧都创建一个 widget 实例树。相比之下, View 绘制之后只有 invalidate 方法被调用才会再次绘制。

如何更新 Widgets

Android 中,我们直接修改 view 即可更新。但是 Flutter 的 Widget 是不可变的,不能直接修改,只能修改 Widget 的状态来更新。
这也是 Stateful 和 Stateless Widget 这两个概念的由来。StatelessWidget 就是一个没有状态信息的 widget 。

当你所需的用户界面不依赖对象里的任何配置信息时,可以选择使用 StatelessWidget。

比如 Android 里的这样一个场景,一个只显示 logo 的 ImageView ,运行时 logo 也不变这种的。

再比如,要是你想根据一个 http 请求的返回结果动态的对 UI 进行修改,你就得用 StatefulWidget 了,然后告诉 Flutter 框架状态更新了,他就会自己处理后面的事了。

需要注意,本质上 Stateless 和 Stateful 还是会在每一帧重建的。不同的是 StatefulWidget 有一个 State 对象保存状态数据并在新的一帧中恢复。

如果你对此还有疑惑,请记住:如果 widget 可以改变(比如响应用户操作) 就是 stateful 的。child widget 是 Stateful 并不意味他的 parent widget 就是 Stateful 的,一切都要看当前 widget 是否响应变化。

看看具体代码,Text widget 一个普通的 StatelessWidget 。看源码就知道 Text Widget 是 StatelessWidget 的子类。

1
2
3
4
new Text(
'I like Flutter!',
style: new TextStyle(fontWeight: FontWeight.bold),
);

可以看到 Text 并没有相关的状态信息,除了构造函数传入的参数就啥都没有了。

那你想了啊,我要是想点个按钮让那段字变一下怎么整?

用 StatelessWidget 包一下 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(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> {
// Default placeholder text
String textToShow = "I Like Flutter";

void _updateText() {
setState(() {
// update the text
textToShow = "Flutter is Awesome!";
});
}

@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Sample App"),
),
body: new Center(child: new Text(textToShow)),
floatingActionButton: new FloatingActionButton(
onPressed: _updateText,
tooltip: 'Update Text',
child: new Icon(Icons.update),
),
);
}
}

怎么写布局?还用 xml 嘛?

没有 xml,我们直接写 widget tree。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Sample App"),
),
body: new Center(
child: new MaterialButton(
onPressed: () {},
child: new Text('Hello'),
padding: new EdgeInsets.only(left: 10.0, right: 10.0),
),
),
);
}

You can view all the layouts that Flutter has to offer here: https://flutter.io/widgets/layout/

如何对 layout 组件进行增删

Android 里用 parent 的addChildremoveChild方法对child 进行增删。但是 Flutter 的 widget 是不可变的,这些方法都没有,创建 widget 树时你可以用一个函数返回 widget,再通过传入的参数判断怎么显示 widget。

栗子:

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
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> {
// Default value for toggle
bool toggle = true;
void _toggle() {
setState(() {
toggle = !toggle;
});
}

_getToggleChild() {
if (toggle) {
return new Text('Toggle One');
} else {
return new MaterialButton(onPressed: () {}, child: new Text('Toggle Two'));
}
}

@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Sample App"),
),
body: new Center(
child: _getToggleChild(),
),
floatingActionButton: new FloatingActionButton(
onPressed: _toggle,
tooltip: 'Update Text',
child: new Icon(Icons.update),
),
);
}
}

怎么用 widget 写动画啊,像 View.animate() 那样吗

Flutter 提供了 animation library 实现动画。

Andorid 中,我们通过 xml 或是直接调用 View 的 animate() 方法创建动画,而 Flutter 是将 widget 包装到一个 Animation 之中。

和 Android 一样, Flutter 也提供 AnimationController 和一个Interpolator (继承自 Animation 类),比如 CurvedAnimation。使用时将 controller 和 Animation 传入 AnimationWidget ,之后调用 controller 启动动画。

一个 fade 动画:

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
import 'package:flutter/material.dart';

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

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

class MyFadeTest extends StatefulWidget {
MyFadeTest({Key key, this.title}) : super(key: key);
final String title;
@override
_MyFadeTest createState() => new _MyFadeTest();
}

class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {
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);
}

@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Container(
child: new FadeTransition(
opacity: curve,
child: new FlutterLogo(
size: 100.0,
)))),
floatingActionButton: new FloatingActionButton(
tooltip: 'Fade',
child: new Icon(Icons.brush),
onPressed: () {
controller.forward();
},
),
);
}
}

See https://flutter.io/widgets/animation/ and https://flutter.io/tutorials/animation for more specific details.

如何使用 Canvas 进行 draw/paint

Android 中,使用 Canvas 即可绘制自定义图形。
Flutter 提供两个类 CustomPaint 和 CustomPainter 帮助你进行绘制,

来自 StackOverFlow 的热门问题,实现一个画板:
https://stackoverflow.com/questions/46241071/create-signature-area-for-mobile-app-in-dart-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
import 'package:flutter/material.dart';
class SignaturePainter extends CustomPainter {
SignaturePainter(this.points);
final List<Offset> points;
void paint(Canvas canvas, Size size) {
var 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 GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
setState(() {
RenderBox referenceBox = context.findRenderObject();
Offset localPosition =
referenceBox.globalToLocal(details.globalPosition);
_points = new List.from(_points)..add(localPosition);
});
},
onPanEnd: (DragEndDetails details) => _points.add(null),
child: new CustomPaint(painter: new SignaturePainter(_points)),
);
}
}
class DemoApp extends StatelessWidget {
Widget build(BuildContext context) => new Scaffold(body: new Signature());
}
void main() => runApp(new MaterialApp(home: new DemoApp()));

如何自定义 Widgets

安卓怎么自定义 View 大家都很熟了,不多说。
Flutter 里自定义一个 widget 往往不是直接继承,而是通过组合小的 widget。

我们来实现一个 CustomButton ,直接在构造函数传入一个 label。这里将其与一个 RaisedButton 组合,而不是继承一个 RaisedButton,再重写实现新的方法。

1
2
3
4
5
6
7
8
9
class CustomButton extends StatelessWidget {
final String label;
CustomButton(this.label);

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

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

Intents

Flutter 里什么等价于 Intent

Android 使用 Intent 主要是用来切换 Activity,和调用外部组件。 Flutter 里没有 Intent 的概念,但必要时仍可以通过 Flutter 提供的方法调用 native 层发送 Intent。

Flutter 使用 router 绘制新的 widget 从而达到切换 screen 的需求。这里有两个核心概念,同时也是 Flutter 提供用来管理多个 screen 的类:Router 和 Navigator。Router 是一个应用中对应 screen 或 page 的抽象(想想 Activity),Navigator 是一个用来管理 router 的 widget。Navigator 通过对 router 进行 push/pop 操作帮助用户在 screen 之间切换。

Flutter 中,可以将一个 Mapname:String,router:Router 传给顶级 MaterialApp 实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main() {
runApp(new MaterialApp(
home: new MyAppHome(), // becomes the route named '/'
routes: <String, WidgetBuilder> {
'/a': (BuildContext context) => new MyPage(title: 'page A'),
'/b': (BuildContext context) => new MyPage(title: 'page B'),
'/c': (BuildContext context) => new MyPage(title: 'page C'),
},
));
}
```

就可以通过 Navigator 根据 router 的名字对 router 进行操作。
```dart
Navigator.of(context).pushNamed('/b');

Intent 的其他使用场景(比如启动相机,选择文件),则需要创建中间层与 native 进行交互(或是用别人写好的库)。

See [Flutter Plugins] to learn how to build a native platform integration.

Flutter 如何处理外部程序发来的 Intent

Flutter 可以直接与 Android 层交互,获取共享的数据,从而处理外部发来的 Intent。

看例子:
基本流程就是先在 Android 端接收 text,然后等待 Flutter 通过 MethodChannel 请求将数据发送给它。

我们先在 AndroidManifest.xml 注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<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">
<!-- This keeps the window background of the activity showing
until Flutter renders its first frame. It can be removed if
there is no splash screen (such as the default splash screen
defined in @style/LaunchTheme). -->
<meta-data
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:value="true" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<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 中取出数据,等待 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
package com.yourcompany.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.plugins.GeneratedPluginRegistrant;

public class MainActivity extends FlutterActivity {
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 MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
if (methodCall.method.contentEquals("getSharedText")) {
result.success(sharedText);
sharedText = null;
}
}
});
}


void handleSendText(Intent intent) {
sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
}
}

最后,当 Flutter 渲染 view 时请求数据

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
import 'package:flutter/material.dart';
import 'package:flutter/services.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 Shared App Handler',
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> {
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 new Scaffold(body: new Center(child: new Text(dataShared)));
}

getSharedText() async {
var sharedData = await platform.invokeMethod("getSharedText");
if (sharedData != null) {
setState(() {
dataShared = sharedData;
});
}
}
}

那么 startActivityForResult?

Flutter 可以使用 Navigator 获取一个 router 返回的数据。直接 await 一个 push 的返回值。如下:

1
Map coordinates = await Navigator.of(context).pushNamed('/location');

在位置选择 router 里,当用户操作完毕,就可以带着返回值 pop
1
Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});