给 iOS 开发者的 Flutter 指南(下)

382 阅读23分钟
原文链接: mp.weixin.qq.com

这篇文章是为那些想将已有的 iOS 开发经验运用到 Flutter 开发中的 iOS 开发者所作。 如果你理解 iOS framework 的基本原理,那么你可以将这篇文章作为学习 Flutter 开发的起点。

本系列上部分:给 iOS 开发者的 Flutter 指南(上)

本文结构如下:

1.  Views(上)

2.  导航(上)

3.  线程和异步(上)

4.  工程结构、本地化、依赖和资源(上)

5.  ViewControllers(下)

6.  布局(下)

7.  手势检测与 touch 事件处理(下)

8.  主题和文字(下)

9.  表单输入(下)

10.  和硬件、第三方服务以及系统平台交互(下)

11.  数据库和本地存储(下)

12.  通知(下)


五、ViewControllers

5.1 ViewController 相当于 Flutter 中的什么?

在 iOS 里,一个 ViewController 是用户界面的一部分,通常是作为屏幕或者其中的一部分来使用。 这些组合在一起构成了复杂的用户界面,并以此对应用的 UI 做不断的扩充。 在 Flutter 中,这一任务又落到了 Widget 这里。就像在导航那一章提到的, Flutter 中的屏幕也是使用 Widgets 表示的,因为“万物皆 widget!”。使用  Naivgator 在不同的 Route  之间切换,而不同的路由则代表了不同的屏幕或页面,或是不同的状态,也可能是渲染相同的数据。

5.2 如何监听 iOS 中的生命周期?

在 iOS 里,可以重写 ViewController  的方法来捕获自身的生命周期,或者在 AppDelegate  中注册生命 周期的回调。Flutter 中则没有这两个概念,但是你可以通过在 WidgetsBinding  的 observer 中挂钩子,也可以 通过监听didChangeAppLifecycleState()  事件,来实现相应的功能。

可监听的生命周期事件有:

  • inactive  - 应用当前处于不活跃状态,不接收用户输入事件。这个事件只在 iOS  上有效,Android 中没有类似的状态。

  • paused  - 应用处于用户不可见状态,不接收用户输入事件,但仍在后台运行。

  • resumed  - 应用可见,也响应用户输入。

  • suspending  - 应用被挂起,在 iOS 平台没有这一事件。

更多细节,请参见 AppLifecycleStatus  文档。

六、布局

6.1 UITableView 和 UICollectionView 相当于 Flutter 中的什么?

在 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  @override10  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}2021class SampleAppPage extends StatefulWidget {22  SampleAppPage({Key key}) : super(key: key);2324  @override25  _SampleAppPageState createState() => _SampleAppPageState();26}2728class _SampleAppPageState extends State<SampleAppPage> {29  @override30  Widget build(BuildContext context) {31    return Scaffold(32      appBar: AppBar(33        title: Text("Sample App"),34      ),35      body: ListView(children: _getListData()),36    );37  }3839  _getListData() {40    List<Widget> 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}

6.2 如何确定列表中被点击的元素?

在 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  @override10  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}2021class SampleAppPage extends StatefulWidget {22  SampleAppPage({Key key}) : super(key: key);2324  @override25  _SampleAppPageState createState() => _SampleAppPageState();26}2728class _SampleAppPageState extends State<SampleAppPage> {29  @override30  Widget build(BuildContext context) {31    return Scaffold(32      appBar: AppBar(33        title: Text("Sample App"),34      ),35      body: ListView(children: _getListData()),36    );37  }3839  _getListData() {40    List<Widget> 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}

6.3 如何动态更新?

在 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  @override10  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}2021class SampleAppPage extends StatefulWidget {22  SampleAppPage({Key key}) : super(key: key);2324  @override25  _SampleAppPageState createState() => _SampleAppPageState();26}2728class _SampleAppPageState extends State<SampleAppPage> {29  List widgets = [];3031  @override32  void initState() {33    super.initState();34    for (int i = 0; i < 100; i++) {35      widgets.add(getRow(i));36    }37  }3839  @override40  Widget build(BuildContext context) {41    return Scaffold(42      appBar: AppBar(43        title: Text("Sample App"),44      ),45      body: ListView(children: widgets),46    );47  }4849  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  @override10  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}2021class SampleAppPage extends StatefulWidget {22  SampleAppPage({Key key}) : super(key: key);2324  @override25  _SampleAppPageState createState() => _SampleAppPageState();26}2728class _SampleAppPageState extends State<SampleAppPage> {29  List widgets = [];3031  @override32  void initState() {33    super.initState();34    for (int i = 0; i < 100; i++) {35      widgets.add(getRow(i));36    }37  }3839  @override40  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  }5354  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  方法进行添加。

6.4 ScrollView 相当于 Flutter 里的什么?

在 iOS 中,把视图放在 ScrollView  里来允许用户在需要时滚动内容。

在 Flutter 中,使用 ListView  widget 是最简单的办法。它和 iOS 中 ScrollView   及 TableView  表现一致,也可以给它的 widget 做垂直排版。

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

关于 Flutter 中排布的更多细节,请参阅 layout tutorial。

七、手势检测与 touch 事件处理

7.1 如何给 Flutter 的 widget 添加点击事件?

在 iOS 中,通过把 GestureRecognizer  绑定给 UIView 来处理点击事件。在 Flutter 中, 有两种方法来添加事件监听者:

1. 如果 widget 本身支持事件检测,则直接传递处理函数给它。例如,RaisedButton  拥有 一个 onPressed  参数:

1@override2Widget 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}

7.2 我怎么处理 widget 上的其他手势?

你可以使用 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} 910class SampleApp extends StatelessWidget {11  @override12  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}

八、主题和文字

8.1 如何设置应用主题?

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: MyCustomFont3     fonts:4       - asset: fonts/MyCustomFont.ttf5       - 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}

8.3 我怎么给我的 Text widget 设置样式?

除了字体以外,你也可以自定义 Text widget 的其他样式。Text  widget 接收一个 TextStyle  对象的参数,可以指定很多参数,例如:

  • color

  • decoration

  • decorationColor

  • decorationStyle

  • fontFamily

  • fontSize

  • fontStyle

  • fontWeight

  • hashCode

  • height

  • inherit

  • letterSpacing

  • textBaseline

  • wordSpacing

九、表单输入

9.1 Flutter 中如何使用表单?我怎么拿到用户的输入?

我们知道 Flutter 使用的是不可变而且状态分离的 widget,你可能会好奇这种情况下如何处理用户的输入。在 iOS 上,一般会在提交数据时查询当前组件的数值或动作。那么在  Flutter 中会怎么样呢?

和 Flutter 的其他部分一样,表单处理要通过特定的 widget 来实现。如果你有一个 TextField  或者 TextFormField , 你可以通过 TextEditingController  来 获取用户的输入:

 1class _MyFormState extends State<MyForm> { 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  }1213  @override14  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 the27        // 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 our34                // TextEditingController35                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 中可以找到更多的相关内容以及详细的代码列表。

9.2 Text field 中的 placeholder 相当于什么?

在 Flutter 里,通过向 Text  widget 传递一个 InputDecoration  对象,你可以轻易的显示文本框的提示信息,或是 placeholder。

1body: Center(2  child: TextField(3    decoration: InputDecoration(hintText: "This is a hint"),4  ),5)

9.3 如何展示验证错误信息?

就和显示提示信息一样,你可以通过向 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}1415class SampleAppPage extends StatefulWidget {16  SampleAppPage({Key key}) : super(key: key);1718  @override19  _SampleAppPageState createState() => _SampleAppPageState();20}2122class _SampleAppPageState extends State<SampleAppPage> {23  String _errorText;2425  @override26  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  }4748  _getErrorText() {49    return _errorText;50  }5152  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,}))$';5556    RegExp regExp = RegExp(p);5758    return regExp.hasMatch(em);59  }60}

十、和硬件、第三方服务以及系统平台交互

10.1 如何与系统平台以及平台原生代码进行交互?

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 上。

10.2 如何访问 GPS 传感器?

使用 geolocator 插件,这一插件由社区提供。

10.3 如何访问摄像头?

image_picker  是常用的访问相机的插件。

10.4 我怎么登录 Facebook?

登录 Facebook 可以使用 flutter_facebook_login 社区插件。

10.5 如何集成 Firebase 功能?

大多数 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 插件,主要实现了官方插件没有直接实现的功能。

10.6 如何构建自己的插件?

如果有一些 Flutter 和遗漏的平台特性,可以 根据 developing packages and plugins 构建自己的插件。

Flutter 的插件结构,简单来说,更像是 Android 中的 Event bus:你发送一个消息,并让接受者处理并反馈结果给你。这种情况下,接受者就是在 iOS 或 Android 的原生代码。

十一、数据库和本地存储

11.1 Flutter 中如何访问 UserDefaults?

在 iOS 里,可以使用属性列表存储一个键值对的集合,也就是我们所说的 UserDefaults。

在 Flutter 里,可以使用 Shared Preferences 插件来实现相同的功能。这个插件封装了 UserDefaults  以及 Android 里类似的 SharedPreferences

11.2 CoreData 相当于 Flutter 中的什么

在 iOS 里,你可以使用 CoreData 来存储结构化的数据。这是一个基于 SQL 数据库的上层封装,可以使关联模型的查询变得更加简单。

在 Flutter 里,可以使用 SQFlite 插件来实现这个功能。

十二、通知

12.1 如何设置推送通知?

在 iOS 里,你需要向开发者中心注册来允许推送通知。

在 Flutter 里,使用 firebase_messaging  插件来实现这个功能。

关于 Firebase Cloud Messaging API 的更多信息,可以 查看 firebase_messaging  插件文档。

(完)


诚挚邀请大家参与 Flutter 官方的开发者调查,扫描下方二维码,填写调查问卷,Flutter 因你而更优秀:

▼ 往期精彩回顾 ▼ 给 iOS 开发者的 Flutter 指南(上) 给前端工程师的 Flutter 指南 Google 工程师中文演讲 | 深入了解 Flutter 的高性能图形渲染 戳“阅读原文”一起来充电吧!