这篇文章是为那些想将已有的 iOS 开发经验运用到 Flutter 开发中的 iOS 开发者所作。 如果你理解 iOS framework 的基本原理,那么你可以将这篇文章作为学习 Flutter 开发的起点。
本系列上部分:
本文结构如下:
1. Views(上)
2. 导航(上)
3. 线程和异步(上)
4. 工程结构、本地化、依赖和资源(上)
5. ViewControllers(下)
6. 布局(下)
7. 手势检测与 touch 事件处理(下)
8. 主题和文字(下)
9. 表单输入(下)
10. 和硬件、第三方服务以及系统平台交互(下)
11. 数据库和本地存储(下)
12. 通知(下)
在 iOS 里,一个 ViewController 是用户界面的一部分,通常是作为屏幕或者其中的一部分来使用。 这些组合在一起构成了复杂的用户界面,并以此对应用的 UI 做不断的扩充。 在 Flutter 中,这一任务又落到了 Widget 这里。就像在导航那一章提到的, Flutter 中的屏幕也是使用 Widgets 表示的,因为“万物皆 widget!”。使用 Naivgator
在不同的 Route
之间切换,而不同的路由则代表了不同的屏幕或页面,或是不同的状态,也可能是渲染相同的数据。
在 iOS 里,可以重写 ViewController
的方法来捕获自身的生命周期,或者在 AppDelegate
中注册生命 周期的回调。Flutter 中则没有这两个概念,但是你可以通过在 WidgetsBinding
的 observer 中挂钩子,也可以 通过监听didChangeAppLifecycleState()
事件,来实现相应的功能。
可监听的生命周期事件有:
inactive
- 应用当前处于不活跃状态,不接收用户输入事件。这个事件只在 iOS 上有效,Android 中没有类似的状态。
paused
- 应用处于用户不可见状态,不接收用户输入事件,但仍在后台运行。
resumed
- 应用可见,也响应用户输入。
suspending
- 应用被挂起,在 iOS 平台没有这一事件。
更多细节,请参见 AppLifecycleStatus
文档。
六、布局
在 iOS 里,你可能使用 UITableView
或者 UICollectionView
来展示一个列表。而在 Flutter 里,你可以使用 ListView 来达到类似的实现。在 iOS 中,你通过 delegate 方法来确定显示的行数,相应位置的 cell,以及 cell 的尺寸。
由于 Flutter 中 widget 的不可变特性,你需要向 ListView
传递一个 widget 列表,Flutter 会确保滚动快速而流畅。
1import 'package:flutter/material.dart';
2
3void main() {
4 runApp(SampleApp());
5}
6
7class SampleApp extends StatelessWidget {
8 // This widget is the root of your application.
9 @override
10 Widget build(BuildContext context) {
11 return MaterialApp(
12 title: 'Sample App',
13 theme: ThemeData(
14 primarySwatch: Colors.blue,
15 ),
16 home: SampleAppPage(),
17 );
18 }
19}
20
21class SampleAppPage extends StatefulWidget {
22 SampleAppPage({Key key}) : super(key: key);
23
24 @override
25 _SampleAppPageState createState() => _SampleAppPageState();
26}
27
28class _SampleAppPageState extends State {
29 @override
30 Widget build(BuildContext context) {
31 return Scaffold(
32 appBar: AppBar(
33 title: Text("Sample App"),
34 ),
35 body: ListView(children: _getListData()),
36 );
37 }
38
39 _getListData() {
40 List widgets = [];
41 for (int i = 0; i < 100; i++) {
42 widgets.add(Padding(padding: EdgeInsets.all(10.0), child: Text("Row $i")));
43 }
44 return widgets;
45 }
46}
在 iOS 中,tableView:didSelectRowAtIndexPath:
代理方法可以用来实现该功能。而在 Flutter 中,需要通过 widget 传递进来的 touch 响应处理来实现。
1import 'package:flutter/material.dart';
2
3void main() {
4 runApp(SampleApp());
5}
6
7class SampleApp extends StatelessWidget {
8 // This widget is the root of your application.
9 @override
10 Widget build(BuildContext context) {
11 return MaterialApp(
12 title: 'Sample App',
13 theme: ThemeData(
14 primarySwatch: Colors.blue,
15 ),
16 home: SampleAppPage(),
17 );
18 }
19}
20
21class SampleAppPage extends StatefulWidget {
22 SampleAppPage({Key key}) : super(key: key);
23
24 @override
25 _SampleAppPageState createState() => _SampleAppPageState();
26}
27
28class _SampleAppPageState extends State {
29 @override
30 Widget build(BuildContext context) {
31 return Scaffold(
32 appBar: AppBar(
33 title: Text("Sample App"),
34 ),
35 body: ListView(children: _getListData()),
36 );
37 }
38
39 _getListData() {
40 List widgets = [];
41 for (int i = 0; i < 100; i++) {
42 widgets.add(GestureDetector(
43 child: Padding(
44 padding: EdgeInsets.all(10.0),
45 child: Text("Row $i"),
46 ),
47 onTap: () {
48 print('row tapped');
49 },
50 ));
51 }
52 return widgets;
53 }
54}
在 iOS 中,可以更新列表数据,调用 reloadData
方法通知 tableView 或 collectionView。
在 Flutter 里,如果你在 setState()
中更新了 widget 列表,你会发现展示的数据并不会立刻更新。这是因为当 setState()
被调用时,Flutter 的渲染引擎回去检索 widget 树是否有改变。当它获取到 ListView
,会进行 ==
判断,然后发现两个 ListView 是相等的。没发现有改变,所以也就不会进行更新。
更新 ListView
简单的方法是在 setState()
创建一个新的 List,然后拷贝旧列表中的所有数据到新列表。这样虽然简单,但是像下面示例一样数据量很大时,并不推荐这样做。
1import 'package:flutter/material.dart';
2
3void main() {
4 runApp(SampleApp());
5}
6
7class SampleApp extends StatelessWidget {
8 // This widget is the root of your application.
9 @override
10 Widget build(BuildContext context) {
11 return MaterialApp(
12 title: 'Sample App',
13 theme: ThemeData(
14 primarySwatch: Colors.blue,
15 ),
16 home: SampleAppPage(),
17 );
18 }
19}
20
21class SampleAppPage extends StatefulWidget {
22 SampleAppPage({Key key}) : super(key: key);
23
24 @override
25 _SampleAppPageState createState() => _SampleAppPageState();
26}
27
28class _SampleAppPageState extends State {
29 List widgets = [];
30
31 @override
32 void initState() {
33 super.initState();
34 for (int i = 0; i < 100; i++) {
35 widgets.add(getRow(i));
36 }
37 }
38
39 @override
40 Widget build(BuildContext context) {
41 return Scaffold(
42 appBar: AppBar(
43 title: Text("Sample App"),
44 ),
45 body: ListView(children: widgets),
46 );
47 }
48
49 Widget getRow(int i) {
50 return GestureDetector(
51 child: Padding(
52 padding: EdgeInsets.all(10.0),
53 child: Text("Row $i"),
54 ),
55 onTap: () {
56 setState(() {
57 widgets = List.from(widgets);
58 widgets.add(getRow(widgets.length + 1));
59 print('row $i');
60 });
61 },
62 );
63 }
64}
一个高效且有效的方法是使用 ListView.Builder
来构建列表。当你的数据量很大,且需要构建动态列表时,这个方法会非常好用。
1import 'package:flutter/material.dart';
2
3void main() {
4 runApp(SampleApp());
5}
6
7class SampleApp extends StatelessWidget {
8 // This widget is the root of your application.
9 @override
10 Widget build(BuildContext context) {
11 return MaterialApp(
12 title: 'Sample App',
13 theme: ThemeData(
14 primarySwatch: Colors.blue,
15 ),
16 home: SampleAppPage(),
17 );
18 }
19}
20
21class SampleAppPage extends StatefulWidget {
22 SampleAppPage({Key key}) : super(key: key);
23
24 @override
25 _SampleAppPageState createState() => _SampleAppPageState();
26}
27
28class _SampleAppPageState extends State {
29 List widgets = [];
30
31 @override
32 void initState() {
33 super.initState();
34 for (int i = 0; i < 100; i++) {
35 widgets.add(getRow(i));
36 }
37 }
38
39 @override
40 Widget build(BuildContext context) {
41 return Scaffold(
42 appBar: AppBar(
43 title: Text("Sample App"),
44 ),
45 body: ListView.builder(
46 itemCount: widgets.length,
47 itemBuilder: (BuildContext context, int position) {
48 return getRow(position);
49 },
50 ),
51 );
52 }
53
54 Widget getRow(int i) {
55 return GestureDetector(
56 child: Padding(
57 padding: EdgeInsets.all(10.0),
58 child: Text("Row $i"),
59 ),
60 onTap: () {
61 setState(() {
62 widgets.add(getRow(widgets.length + 1));
63 print('row $i');
64 });
65 },
66 );
67 }
68}
与创建 ListView
不同,
创建 ListView.Builder
需要两个关键参数:初始化列表长度和 ItemBuilder
函数。
ItemBuilder
方法和 cellForItemAt
代理方法非常类似,它接收位置参数,然后返回想要在该位置渲染的 cell。
最后,也是最重要的,注意 onTap()
方法并没有重新创建列表,而是使用 .add
方法进行添加。
在 iOS 中,把视图放在 ScrollView
里来允许用户在需要时滚动内容。
在 Flutter 中,使用 ListView
widget 是最简单的办法。它和 iOS 中 ScrollView
及 TableView
表现一致,也可以给它的 widget 做垂直排版。
1@override
2Widget build(BuildContext context) {
3 return ListView(
4 children: [
5 Text('Row One'),
6 Text('Row Two'),
7 Text('Row Three'),
8 Text('Row Four'),
9 ],
10 );
11}
关于 Flutter 中排布的更多细节,请参阅 layout tutorial。
在 iOS 中,通过把 GestureRecognizer
绑定给 UIView 来处理点击事件。在 Flutter 中, 有两种方法来添加事件监听者:
1. 如果 widget 本身支持事件检测,则直接传递处理函数给它。例如,RaisedButton
拥有 一个 onPressed
参数:
1@override
2Widget build(BuildContext context) {
3 return RaisedButton(
4 onPressed: () {
5 print("click");
6 },
7 child: Text("Button"),
8 );
9}
2. 如果 widget 本身不支持事件检测,那么把它封装到一个 GestureDetector 中,并给它的 onTap 参数传递一个函数:
1class SampleApp extends StatelessWidget {
2 @override
3 Widget build(BuildContext context) {
4 return Scaffold(
5 body: Center(
6 child: GestureDetector(
7 child: FlutterLogo(
8 size: 200.0,
9 ),
10 onTap: () {
11 print("tap");
12 },
13 ),
14 ),
15 );
16 }
17}
你可以使用 GestureDetector
来监听更多的手势,例如:
单击事件
onTapDown
- 在特定区域发生点触屏幕的一个即时操作。
onTapUp
- 在特定区域发生触摸抬起的一个即时操作。
onTap
- 从点触屏幕之后到触摸抬起之间的单击操作。
onTapCancel
- 触发了 onTapDown
,但未触发 tap 。
双击事件
onDoubleTap
- 用户在同一位置发生快速点击屏幕两次的操作。
长按事件
onLongPress
- 用户在同一位置长时间触摸屏幕的操作。
垂直拖动事件
onVerticalDragStart
- 用户手指接触屏幕,并且将要进行垂直移动事件。
onVerticalDragUpdate
- 用户手指接触屏幕,已经开始垂直移动,且会持续进行移动。
onVerticalDragEnd
- 用户之前手指接触了屏幕并发生了垂直移动操作,并且停止接触前还在以一定的速率移动。
水平拖动事件
onHorizontalDragStart
- 用户手指接触屏幕,并且将要进行水平移动事件。
onHorizontalDragUpdate
- 用户手指接触屏幕,已经开始水平移动,且会持续进行移动。
onHorizontalDragEnd
- 用户手指接触了屏幕并发生了水平移动操作,并且停止接触前还在以一定的速率移动。
下面的示例展示了 GestureDetector
是如何实现双击时旋转 Flutter 的 logo 的:
1AnimationController controller;
2CurvedAnimation curve;
3
4@override
5void initState() {
6 controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
7 curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
8}
9
10class SampleApp extends StatelessWidget {
11 @override
12 Widget build(BuildContext context) {
13 return Scaffold(
14 body: Center(
15 child: GestureDetector(
16 child: RotationTransition(
17 turns: curve,
18 child: FlutterLogo(
19 size: 200.0,
20 )),
21 onDoubleTap: () {
22 if (controller.isCompleted) {
23 controller.reverse();
24 } else {
25 controller.forward();
26 }
27 },
28 ),
29 ),
30 );
31 }
32}
Flutter 实现了一套漂亮的 Material Design 组件,而且开箱可用,它提供了许多常用的样式和主题。
为了充分发挥应用中 Material Components 的优势,声明一个顶级的 widget,MaterialApp,来作为你的应用 入口。MaterialApp 是一个封装了大量常用 Material Design 组件的 widget。它基于 WidgetsApp 添加了 Material 的相关功能。
但是 Flutter 有足够的灵活性和表现力来实现任何设计语言。在 iOS 上,可以使 用 Cupertino library 来制作遵循 Human Interface Guidelines 的界面。关于这些 widget 的全部集合,可以参看 Cupertino widgets gallery。
也可以使用 WidgetApp
来做为应用入口,它提供了一部分类似的功能接口,但是不如 MaterialApp
强大。
定义所有子组件颜色和样式,可以直接传递 ThemeData
对象给 MaterialApp widget
。例如,在下面的代码中,primary swatch 被设置为蓝色,而文本选中后的颜色被设置为红色:
1class SampleApp extends StatelessWidget {
2 @override
3 Widget build(BuildContext context) {
4 return MaterialApp(
5 title: 'Sample App',
6 theme: ThemeData(
7 primarySwatch: Colors.blue,
8 textSelectionColor: Colors.red
9 ),
10 home: SampleAppPage(),
11 );
12 }
13}
8.2 如何给 Text widget 设置自定义字体?
在 iOS 里,可以在项目中引入任何的 ttf
字体文件,并在 info.plist
文件中声明并进行引用。在 Flutter 里,把字体放到一个文件夹中,然后在 pubspec.yaml
文件中引用它,就和引用图片一样。
1fonts:
2 - family: MyCustomFont
3 fonts:
4 - asset: fonts/MyCustomFont.ttf
5 - style: italic
然后在 Text
widget 中指定字体:
1@override
2Widget build(BuildContext context) {
3 return Scaffold(
4 appBar: AppBar(
5 title: Text("Sample App"),
6 ),
7 body: Center(
8 child: Text(
9 'This is a custom font text',
10 style: TextStyle(fontFamily: 'MyCustomFont'),
11 ),
12 ),
13 );
14}
除了字体以外,你也可以自定义 Text widget 的其他样式。Text
widget 接收一个 TextStyle
对象的参数,可以指定很多参数,例如:
color
decoration
decorationColor
decorationStyle
fontFamily
fontSize
fontStyle
fontWeight
hashCode
height
inherit
letterSpacing
textBaseline
wordSpacing
我们知道 Flutter 使用的是不可变而且状态分离的 widget,你可能会好奇这种情况下如何处理用户的输入。在 iOS 上,一般会在提交数据时查询当前组件的数值或动作。那么在 Flutter 中会怎么样呢?
和 Flutter 的其他部分一样,表单处理要通过特定的 widget 来实现。如果你有一个 TextField
或者 TextFormField
, 你可以通过 TextEditingController
来 获取用户的输入:
1class _MyFormState extends State {
2 // Create a text controller and use it to retrieve the current value.
3 // of the TextField!
4 final myController = TextEditingController();
5
6 @override
7 void dispose() {
8 // Clean up the controller when disposing of the Widget.
9 myController.dispose();
10 super.dispose();
11 }
12
13 @override
14 Widget build(BuildContext context) {
15 return Scaffold(
16 appBar: AppBar(
17 title: Text('Retrieve Text Input'),
18 ),
19 body: Padding(
20 padding: const EdgeInsets.all(16.0),
21 child: TextField(
22 controller: myController,
23 ),
24 ),
25 floatingActionButton: FloatingActionButton(
26 // When the user presses the button, show an alert dialog with the
27 // text the user has typed into our text field.
28 onPressed: () {
29 return showDialog(
30 context: context,
31 builder: (context) {
32 return AlertDialog(
33 // Retrieve the text the user has typed in using our
34 // TextEditingController
35 content: Text(myController.text),
36 );
37 },
38 );
39 },
40 tooltip: 'Show me the value!',
41 child: Icon(Icons.text_fields),
42 ),
43 );
44 }
45}
你在 Flutter Cookbook 的 Retrieve the value of a text field 中可以找到更多的相关内容以及详细的代码列表。
在 Flutter 里,通过向 Text
widget 传递一个 InputDecoration
对象,你可以轻易的显示文本框的提示信息,或是 placeholder。
1body: Center(
2 child: TextField(
3 decoration: InputDecoration(hintText: "This is a hint"),
4 ),
5)
就和显示提示信息一样,你可以通过向 Text
widget 传递一个 InputDecoration
来实现。
然而,你并不想在一开始就显示错误信息。相反,在用户输入非法数据后,应该更新状态,并传递一个新的 InputDecoration
对象。
1class SampleApp extends StatelessWidget {
2 // This widget is the root of your application.
3 @override
4 Widget build(BuildContext context) {
5 return MaterialApp(
6 title: 'Sample App',
7 theme: ThemeData(
8 primarySwatch: Colors.blue,
9 ),
10 home: SampleAppPage(),
11 );
12 }
13}
14
15class SampleAppPage extends StatefulWidget {
16 SampleAppPage({Key key}) : super(key: key);
17
18 @override
19 _SampleAppPageState createState() => _SampleAppPageState();
20}
21
22class _SampleAppPageState extends State {
23 String _errorText;
24
25 @override
26 Widget build(BuildContext context) {
27 return Scaffold(
28 appBar: AppBar(
29 title: Text("Sample App"),
30 ),
31 body: Center(
32 child: TextField(
33 onSubmitted: (String text) {
34 setState(() {
35 if (!isEmail(text)) {
36 _errorText = 'Error: This is not an email';
37 } else {
38 _errorText = null;
39 }
40 });
41 },
42 decoration: InputDecoration(hintText: "This is a hint", errorText: _getErrorText()),
43 ),
44 ),
45 );
46 }
47
48 _getErrorText() {
49 return _errorText;
50 }
51
52 bool isEmail(String em) {
53 String emailRegexp =
54 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,}))$';
55
56 RegExp regExp = RegExp(p);
57
58 return regExp.hasMatch(em);
59 }
60}
Flutter 并不直接在平台上运行代码;而是以 Dart 代码的方式原生运行于设备之上,这算是绕过了平台的 SDK 的限制。 这意味着,例如,你用 Dart 发起了一个网络请求,它会直接在 Dart 的上下文中运行。 你不需要调用写 iOS 或者 Android 原生应用时常用的 API 接口。你的 Flutter 应用仍旧被原生平台 的 ViewController
当做一个 view 来管理,但是你不能够直接访问 ViewController
自身或是对应的原生框架。
这并不意味着 Flutter 应用不能够和原生 API,或是原生代码进行交互。Flutter 提供了用来和宿主 ViewController
通信 和交换数据的 platform channels。 platform channels 本质上是一个桥接了 Dart 代码与宿主 ViewController
和 iOS 框架的异步通信模型。你可以通过 platform channels 来执行原生代码的方法,或者获取设备的传感器信息等数据。
除了直接使用 platform channels 之外,也可以使用一系列包含了原生代码和 Dart 代码,实现了特定功能的现有插件。例如,你在 Flutter 中可以直接使用插件来访问相册或是设备摄像头,而不需要自己重新集成。Pub 是一个 Dart 和 Flutter 的开源包仓库,你可以在这里找到需要的插件。有些包可能支持集成 iOS 或 Android,或两者皆有。
如果你在 Pub 找不到自己需要的包,你可以自己写一个,并发布到 Pub 上。
使用 geolocator 插件,这一插件由社区提供。
image_picker
是常用的访问相机的插件。
登录 Facebook 可以使用 flutter_facebook_login
社区插件。
大多数 Firebase 特性被 first party plugins 包含了。这些插件由 Flutter 团队维护:
搭配 firebase_admob
插件来使用 Firebase AdMob
搭配 firebase_analytics
插件来使用 Firebase Analytics
搭配 firebase_auth
插件来使用 Firebase Auth
搭配 firebase_core
插件来使用 Firebase 核心库
搭配 firebase_database
插件来使用 Firebase RTDB
搭配 firebase_storage
插件来使用 Firebase Cloud Storage
搭配 firebase_messaging
插件来使用 Firebase Messaging (FCM)
搭配 cloud_firestore
插件来使用 Firebase Cloud Firestore
在 Pub 上你也可以找到一些第三方的 Firebase 插件,主要实现了官方插件没有直接实现的功能。
如果有一些 Flutter 和遗漏的平台特性,可以 根据 developing packages and plugins 构建自己的插件。
Flutter 的插件结构,简单来说,更像是 Android 中的 Event bus:你发送一个消息,并让接受者处理并反馈结果给你。这种情况下,接受者就是在 iOS 或 Android 的原生代码。
在 iOS 里,可以使用属性列表存储一个键值对的集合,也就是我们所说的 UserDefaults。
在 Flutter 里,可以使用 Shared Preferences 插件来实现相同的功能。这个插件封装了 UserDefaults
以及 Android 里类似的 SharedPreferences
。
在 iOS 里,你可以使用 CoreData 来存储结构化的数据。这是一个基于 SQL 数据库的上层封装,可以使关联模型的查询变得更加简单。
在 Flutter 里,可以使用 SQFlite 插件来实现这个功能。
在 iOS 里,你需要向开发者中心注册来允许推送通知。
在 Flutter 里,使用 firebase_messaging
插件来实现这个功能。
关于 Firebase Cloud Messaging API 的更多信息,可以 查看 firebase_messaging
插件文档。
(完)
诚挚邀请大家参与 Flutter 官方的开发者调查,扫描下方二维码,填写调查问卷,Flutter 因你而更优秀: