通常我们需要衡量是否值得花精力去学习一个新的东西,但如果你是一个面向全栈(或面向人民币)的有志青年,Flutter作为一个跨端框架是完全值得你去尝试的。在吸收各种前辈的经验(React Native / Weex)后,Flutter提供了一个简洁,有力的形式让你去组织你的跨端App。

安装

下载好SDK配置好环境变量后,你可以用Android Studio,VS Code安装Flutter的插件来获得开发支持。

创建项目后这里你可能遇到的坑可能会有:

gradle在国内下载很慢的问题。这时候只需要修改项目android/build.gradle 里的repositories为阿里云的源即可

1
2
3
maven { url'https://maven.aliyun.com/repository/google' }
maven { url'https://maven.aliyun.com/repository/jcenter' }
maven { url'http://maven.aliyun.com/nexus/content/groups/public' }

Android NDK环境变量没有配置好。

概览

框架概览

可以看到项目代码主要是用Dart来写的。Dart这个语言很早就出来了,我大概在13年的时候尝试过。Dart2后属于一个强类型的语言,很容易入手,学习难度不会很大。

控件 / Widget

StatelessWidgetAssetImageWidgetTextScrollableAnimatableStatefulWidget

在Flutter中Widget属于构建UI的最小基本元素。

Widget分Stateless / Sateful:

  • Stateless
    这个概念很好理解,有些组件可能初始化之后不会有任何更新。例如一个按钮,一张图表。

  • Sateful:
    当你的UI可能需要根据用户的某个交互(点击 / 长按)做出状态变换的时候。

基础控件

Flutter有几个特别常用的基础控件

  • Text:创建一段有样式的文本
  • Row,Colmun:这两个控件可以让你用web Flex布局模型来构建UI
  • Stack: 可以让控件脱离上下文,相对于屏幕上下左右,等同于web里的绝对定位模型
  • Container:创建一个矩形元素,等同于web里的div,他有外边距内边距。

一个简单的例子:

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

class MyAppBar extends StatelessWidget {
MyAppBar({this.title});
// Fields in a Widget subclass are always marked "final".
final Widget title;
@override
Widget build(BuildContext context) {
return Container(
height: 56.0, // in logical pixels
padding: const EdgeInsets.symmetric(horizontal: 8.0),
decoration: BoxDecoration(color: Colors.blue[500]),
// Row is a horizontal, linear layout.
child: Row(
// <Widget> is the type of items in the list.
children: <Widget>[
IconButton(
icon: Icon(Icons.menu),
tooltip: 'Navigation menu',
onPressed: null, // null disables the button
),
// Expanded expands its child to fill the available space.
Expanded(
child: title,
),
IconButton(
icon: Icon(Icons.search),
tooltip: 'Search',
onPressed: null,
),
],
),
);
}
}

class MyScaffold extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Material is a conceptual piece of paper on which the UI appears.
return Material(
// Column is a vertical, linear layout.
child: Column(
children: <Widget>[
MyAppBar(
title: Text(
'Example title',
style: Theme.of(context).primaryTextTheme.title,
),
),
Expanded(
child: Center(
child: Text('Hello, world!'),
),
),
],
),
);
}
}

void main() {
runApp(MaterialApp(
title: 'My app', // used by the OS task switcher
home: MyScaffold(),
));
}

基础布局

  • 布局一个控件

创建一个内容控件

1
Text('Hello World', style: TextStyle(fontSize: 32.0))

所有布局控件都有一个属性叫child(Center / Container)的单个子控件,或者 children(Row,Column, ListView, Stack)拥有多个子控件。

添加一个Text到Center布局控件中

1
2
Center(
child: Text('Hello World', style: TextStyle(fontSize: 32.0))
  • 以水平或垂直方向布局多个控件

在布局有个很重要的东西就是如何把多个控件水平或者垂直放置,你可以用Row来让多个控件按水平布局,用Column来垂直布局多个控件。

一个Row中两个子控件左边是个Column,右边是个Image

左边Cloumn中继续包含多个Row
  • 对齐控件

你可以通过mainAxisAlignment /crossAxisAlignment 来控制一个Row / Cloumn的子控件。
MainAxisAlignment 和 CrossAxisAlignment 提供了一系列常量来控制对齐规则。

让3张图片加起来的宽小于屏幕宽情况下,自适应有间隙的排列

1
2
3
4
5
6
7
8
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Image.asset('images/pic1.jpg'),

如果图片宽度不可预计呢?使用Expanded控,这个控件可以接收一个flex属性来控制单个特定的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Image.asset('images/pic1.jpg'),
),
Expanded(
flex: 2,
child: Image.asset('images/pic2.jpg'),
),
Expanded(

可以看到第二张图片比其它的要宽。

内置布局框架

Flutter拥有丰富的布局控件,如常见的GridView,ListView,Stack,Card 可以很快的利用它们来布局。具体细节需要时查阅文档即可!

交互

创建一个Stateful的Widget

1
2
3
4
class FavoriteWidget extends StatefulWidget {
@override
_FavoriteWidgetState createState() => _FavoriteWidgetState();
}

监听onPressed

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
class _FavoriteWidgetState extends State<FavoriteWidget> {
bool _isFavorited = true;
int _favoriteCount = 41;

void _toggleFavorite() {
setState(() {
// If the lake is currently favorited, unfavorite it.
if (_isFavorited) {
_favoriteCount -= 1;
_isFavorited = false;
// Otherwise, favorite it.
} else {
_favoriteCount += 1;
_isFavorited = true;
}
});
}

@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.all(0.0),
child: IconButton(
icon: (_isFavorited
? Icon(Icons.star)
: Icon(Icons.star_border)),
color: Colors.red[500],
onPressed: _toggleFavorite,
),
),
SizedBox(
width: 18.0,
child: Container(
child: Text('$_favoriteCount'),
),
),
],
);
}
}

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
Widget titleSection = Container(
// ...
child: Row(
children: [
Expanded(
child: Column(
// ...
),
FavoriteWidget(),
],
),
);

return MaterialApp(
// ...
);
}
}

导航/路由

大部分情况下我们会有多个页面来显示不同的信息。如在一个List跳到新页面去看详细信息。在Flutter中新页面就是一个Widget,我们可以通过Navigator来控制。

Navigator.push 打开新页面,Navigator.pop 返回。

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

class Todo {
final String title;
final String description;

Todo(this.title, this.description);
}

void main() {
runApp(MaterialApp(
title: 'Passing Data',
home: TodosScreen(
todos: List.generate(
20,
(i) => Todo(
'Todo $i',
'A description of what needs to be done for Todo $i',
),
),
),
));
}

class TodosScreen extends StatelessWidget {
final List<Todo> todos;

TodosScreen({Key key, @required this.todos}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Todos'),
),
body: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(todos[index].title),
// When a user taps on the ListTile, navigate to the DetailScreen.
// Notice that we're not only creating a DetailScreen, we're
// also passing the current todo through to it!
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailScreen(todo: todos[index]),
),
);
},
);
},
),
);
}
}

class DetailScreen extends StatelessWidget {
// Declare a field that holds the Todo
final Todo todo;

// In the constructor, require a Todo
DetailScreen({Key key, @required this.todo}) : super(key: key);

@override
Widget build(BuildContext context) {
// Use the Todo to create our UI
return Scaffold(
appBar: AppBar(
title: Text("${todo.title}"),
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Text('${todo.description}'),
),
);
}
}

以上这个例子展示了一个TodoList点击查看Detail的例子。

至此你可以开始撸跨端的App啦!遇到问题不要方,布局问题可以学习sdk内置的material包,dart 语言问题参考dartlang.org, 善用第三方库!多看flutter.io的文档。

参考:
https://flutter.io/docs/development/ui/widgets-intro
https://flutter.io/docs/reference/widgets
https://docs.flutter.io/
https://pub.dartlang.org/flutter