Flutter 目前还是 Beta 3 版本,1.0 版本还在路上。不过它在 React Native/weex等跨平台方案之外,又为我们提供了一种跨平台的方案。而且其自身的许多特性,也为我们扩展了新的视野。如果
Fuchsia
系统最终能和 iOS、Android 成三足鼎立之式,甚至于取代 Android,那么 Flutter 就能为我们带来更多的可能。所以现在了解一下还是有必要的。 本文将通过一个简单的实例(知识小集 Flutter 版本客户端,我们后期会慢慢优化),同时半翻译半参考Raywenderlich
上的 Getting Started with Flutter 这篇文章,来一步步了解如何使用 Flutter 构建 App。
在这个 App 的开发过程中,我们将学习以下关于 Flutter 的内容:
- 设置开发环境
- 创建新工程
- Hot Reload
- 导入文件
- 使用 Widget 及自定义 Widget
- 网络请求
- 在列表中展示信息
- 为 App 添加主题
在这个过程中,我们将同时学习一些 Dart 相关的知识。项目的完整代码在 Github 上可以找到。
设置开发环境
我们可以在 macOS
、Linux
或者 Windows
上开发 Flutter 应用。目前 Flutter 团队为一些 IDE 开发了相应的插件,这些 IDE 包括 IntelliJ IDEA
、Android Studio
和 Visual Studio Code
。我的开发环境主要为 macOS + Visual Studio Code,所以本文主要基这两者来进行描述。
实际的配置过程可以参考官方文档 Get Started: Install on macOS。具体的步骤各个平台稍有不同,但主要是以下几步:
- 拷贝 Flutter 的
git
库; - 添加 Flutter
bin
目录到我们指定的目录; - 运行
flutter doctor
命令,这个命令将告诉我们缺少哪些依赖; - 安装缺失的依赖;
- 在 IDE 中安装 Flutter 插件/扩展;
- 测试
需要注意的是,如果想在 iOS 模拟器或 iOS 设备上构建和测试应用,我们需要使用 macOS 系统,同时需要安装
Xcode 9.0+
。
创建新工程
在安装了 Flutter 插件的 VS Code 中,我们可以通过 View > Command Palette...
或者快捷键 cmd+shift+p
来打开 命令面板(command palette),然后输入 Flutter:New Project
并回车:
为工程取名为 awesome_tips_flutter
,并回车。选择一个目录来存储工程,然后等待 Flutter 配置好工程。配置的过程主要有几个步骤:
- 创建工程所需要的模板文件,包括对应的 iOS 和 Android 工程;
- 运行
flutter packages get
命令来获取依赖包; - 运行
flutter doctor
命令来检测依赖包;
如图是构建过程的部分信息:
工程创建完成后,IDE 会默认打开 lib
目录下的 main.dart
文件,这也是我们 App 的入口。
注意:从 Flutter Beta 3 开始,创建 Widget 时,
new
关键字是可选的。目前我这生成的模板代码部分还是带 new 关键字的。
在左侧的工程目录中,我们可以看到 ios
、android
、lib
这些目录,lib 目录下的代码将应用于两个平台,目前我们也主要是在这个目录下工作。
为了构建我们自己的应用,先删除 main.dart 中现有的代码,并用如下代码替代:
import 'package:flutter/material.dart';
void main() => runApp(new AwesomeTips());
class AwesomeTips extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Awesome Tips',
home: Scaffold(
appBar: AppBar(title: Text('Awesome Tips')),
body: Center(
child: Text('Awesome Tips'),
)
)
);
}
}
复制代码
顶部的 main() 函数使用 =>
操作符来指定单行函数的函数体(类似于 ES6 中的箭头函数),并运行 App。runApp
的参数是我们的 AwesomeTipsApp
类(根 Widget)。
在这里,我们的 AwesomeTipsApp 类继承自 StatelessWidget
。Flutter 中大部分实体都是 Widget,或者是无状态的(stateless),或者是有状态的(stateful)。我们重写 Widget 的 build()
方法来构建自定义的 App Widget。
我们先来运行一下这个 App。首先启动 iOS 模拟器。选择菜单 Debug -> Start Debugging
构建并运行工程。可以看到 VS Code 打开了 Debug Console
(调试控制台) 面板,同时 xcode-builder
开始构建并启动 App。初始效果如下图:
同时,我们可以在 VS Code 顶部看到一个调试工具栏,我们可以通过这个工具栏来停止或者重新加载 App。
Hot Reload
Flutter 开发最吸引人的一个方面就是当程序代码更改时,可以自动执行 Hot Reload
操作,来重新加载 App。我们来试试这个特性,对我们的程序做个小小的修改:
appBar: AppBar(title: Text('Awesome Tips for Test')),
复制代码
在我们保存文件时,VS Code 会自动启动 Hot Reload 功能,加载完成后,模拟器会显示新的内容。当然我们也可以手动点击调试工具栏上的 Hot Reload 按钮来启动热加载。来看看效果。
注:由于 Flutter 还是 Beta 版,所以 Hot Reload 并不总是能正常工具。我就遇到了类似
Request to Dart VM Service timed out: _flutter.listViews({})
这样的问题,解决方法是重启 Debug。
导入文件
通常我们都不希望在一个文件中放入大量的代码,而是将代码分散在不同的文件中,并通过一定的方式将这些文件组织起来。然后如果一个文件需要用到其它文件的类或方法,只需要导入相关文件即可。在一个 Dart 文件中,我们可以通过 import
关键字来实现这一目标。
比如上面代码中,我们希望将字符串统一放在一个文件中来管理,那么可以创建一个 strings.dart
文件。在 lib 目录处点击右键,会弹出菜单,选择 New File
,并输入文件名。
在 string.dart
中添加以下代码:
class Strings {
static String appTitle = "Awesome Tips";
}
复制代码
然后在 main.dart
中通过以下方式导入:
import 'strings.dart';
复制代码
现在就可以在 AwesomeTipsApp 中使用 appTitle
了:
class AwesomeTipsApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: Strings.appTitle,
home: Scaffold(
appBar: AppBar(title: Text(Strings.appTitle)),
body: Center(
child: Text(Strings.appTitle),
)
)
);
}
}
复制代码
Widgets
在 Flutter App 中,几乎所有的界面元素都是 Widget。Widget 被设计成是不可变的(immutable),因为这样可以让 App 的 UI 轻量化。我们可以使用两种类型的 Widget:
- Stateless:无状态 Widget,只依赖于自身的配置信息,例如一个 image view 的静态图片;
- Stateful:有状态 Widget,需要处理动态信息,并与
State
对象交互。
两种类型的 Widget 都会在 Flutter App 的每一帧进行重绘,不同的是 Stateful Widgets 会将其配置交给 State 对象来管理。关于 Flutter 界面开发,可以参考阿里闲鱼团队 的**《深入了解Flutter界面开发》**一文。
我们现在来创建一个 Widget 展示列表。在 lib 目录中新建文件 content_list.dart
,在文件中加入如下代码:
import 'package:flutter/material.dart';
class ContentList extends StatefulWidget {
@override
createState() => _ContentListState();
}
复制代码
这里我们创建了 StatefulWidget
的一个子类 ContentList
并重写了 createState()
方法,该方法返回 ContentList 对应的 State 对象。然后我们在同一文件中添加以下代码:
class _ContentListState extends State<ContentList> {
}
复制代码
_ContentListState
继承自泛型参数为 ContentList 的 State 对象。在 _ContentListState 中,我们的主要工作就是重写 build()
方法,这个方法在 Widget 被渲染到屏幕上时会调用。目前我们还没有涉及到数据的处理,所以暂时和之前一样,在 ContentList 中显示一个简单的文本。在 build() 方法中添加以下代码:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(Strings.appTitle)),
body: Text(Strings.appTitle),
);
}
复制代码
Scaffold
类是Material Design Widgets
的容器。它通常作为 Widget 层级的根。
上面的代码我们添加了一个 AppBar 和一个 body 到 Scaffold 中。接下来我们用这个 ContentList Widget 替换 main.dart 中的 home
属性的内容:
import 'content_list.dart';
void main() => runApp(AwesomeTipsApp());
class AwesomeTipsApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: Strings.appTitle,
home: ContentList(), // 替换此处内容
);
}
}
复制代码
编译运行程序,得到的结果和上面差不多。
网络请求及数据转换
我们最终要展示的是知识小集的内容清单,所以需要从服务器上获取到清单内容,并转换成我们需要的 Dart 对象。这里我们需要用到两个库:
package:http/http.dart
:负责网络请求,从服务端获取数据;dart:convert
:将服务端返回的字符串转换成JSON
对象;
我们在 main.dart 中导入这两个模块:
import 'package:http/http.dart';
import 'dart:convert';
复制代码
需要注意的是:Dart 应用是单线程的,但是 Dart 支持代码运行在其它线程上,同时也支持使用
async/await
模式让代码异步执行,而不会阻塞 UI 线程。
接下来我们需要通过异步网络调用来获取知识小集的内容列表。首先我们在 _ContentListState 类的顶部添加一个空列表属性,用于保存内容清单:
var _items = [];
复制代码
Dart 语言中,如果属性/方法名是以_开头,则表示这个属性/方法是类私有的。
然后添加一个 _loadData() 方法,我们在这做网络请求:
void _loadData() async {
String dataURL =
"https://app.kangzubin.com/iostips/api/feed/list?page=1&from=flutter-app&version=1.0";
http.Response response = await http.get(dataURL);
// ...
}
复制代码
这里我们在 _loadData() 后面加上 async
关键字,用于告诉 Dart 这是一个异步方法,同时在 http.get
前使用 await
关键字,来阻塞后面的代码执行。当 HTTP 调用完成后,服务端返回的是一个 JSON
字符串,具体结构如下:
{
"code": 0,
"msg": "SUCCESS",
"data": {}
}
复制代码
对于 feed/list
接口,其 data
中的结构如下:
"data": {
"feeds": [{
"fid": "96",
"auther": "halohily",
"title": "如何重写自定义对象的 hash 方法",
"url": "https://weibo.com/3656155132/GfEGebnEN",
"platform": "0",
"postdate": "2018-05-08"
}, {
"fid": "95",
"auther": "南峰子",
"title": "微博一周推送",
"url": "https://weibo.com/3321824014/GfviNzT3z",
"platform": "0",
"postdate": "2018-05-07"
}]
}
复制代码
在获取到 JSON 字符串后,我们首先需要将其转换成 JSON 对象,然后根据 code 是否为 0 做处理。如果请求成功,则需要从 data
中取出 feeds
的数据。同时,我们希望将 feed 数据转换成一个 Dart 对象,所以我们创建一个 feed.dart
文件,并添加如下代码:
class Feed {
final String author;
final String title;
final String postdate;
Feed(this.author, this.title, this.postdate);
}
复制代码
然后我们就可以对返回的数据做处理,将每一条 feed 转换成一个 Feed
对象,并存储在 _items 中。完整的 _loadData() 代码如下所示:
void _loadData() async {
String dataURL =
"https://app.kangzubin.com/iostips/api/feed/list?page=1&from=flutter-app&version=1.0";
http.Response response = await http.get(dataURL);
final body = JSON.decode(response.body);
final int code = body["code"];
if (code == 0) {
final feeds = body["data"]["feeds"];
var items = [];
feeds.forEach((item) =>
items.add(Feed(item["author"], item["title"], item["postdate"])));
setState(() {
_items = items;
});
}
}
复制代码
如果我们希望在状态改变时,触发界面重新渲染,则需要调用 setState() 方法来设置我们的属性值。
有了加载数据的方法,我们就需要在合适的位置来调用。我们暂且在 _ContentListState 类中重写 State 的 initState()
方法,如下所示:
@override
void initState() {
super.initState();
_loadData();
}
复制代码
Widget 生命周期相关的内容,我们有机会再讲。
使用 ListView
至此,我们已经有了列表数据,接下来就需要将数据显示在界面上了。Flutter 提供了 ListView
Widget 来显示一个列表,这个 Widget 能很流畅地展示列表内容。
我们先在 _ContentListState 类中添加一个私有方法 _buildRow()
,以创建显示单元格的 widget:
Widget _buildRow(int i) {
Feed feed = this._items[i];
return ListTile(
title: Text(
feed.title,
overflow: TextOverflow.fade,
),
subtitle: Text(
'${feed.postdate} @${feed.author}',
));
}
复制代码
我们暂且返回一个 ListTile
来显示内容的标题及发布日期和作者。接下来我们修改 build()
方法中 Scaffold 的 body:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(Strings.appTitle)),
body: new ListView.builder(
padding: const EdgeInsets.all(13.0),
itemCount: _items.length * 2,
itemBuilder: (BuildContext context, int position) {
// 此处为添加分割线
if (position.isOdd) return Divider();
final index = position ~/ 2;
return _buildRow(index);
},
),
);
}
复制代码
在这段代码中,我们通过 ListView.builder
来创建一个 ListView,并通过参数来配置列表的显示。这里我们没有处理单元格点击等事件,后续我们会做改进。
OK,保存代码,Hot Reload 后的效果如下:
很简单吧?这样,我们的任务基本完成。
这里我们只是获取了第1页的数据,分页处理后续再完善。
添加主题(Theme)
最后我们来看看如何为 App 添加主题。可以说这很容易,只需要设置 main.dart 中 MaterialApp 的 theme
属性,我们来试试:
class AwesomeTipsApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: Strings.appTitle,
theme: ThemeData(primaryColor: Colors.red.shade800),
home: ContentList(),
);
}
}
复制代码
我们使用了 Material Design
颜色值来设置主题颜色,效果如下:
总结
在本文中,我们通过一个简单的例子来了解了一下如果使用 Flutter 来构建 App,可以在 awesome-tips-flutter-app 下载完整的示例代码。当然,构建一个完整的 App 还需要做很多事情,还有许多技术学习。后期我们会逐步来完善这个 App,并让其达到上线的标准,最终发布到应用市场上。
为了更方便大家获取 Flutter 相关的开发资源,我们在 Github 上开了一个 repo flutter-resources,欢迎大家一起来维护这个 repo。
参考
知识小集是一个团队公众号,主要定位在移动开发领域,分享移动开发技术,包括 iOS、Android、小程序、移动前端、React Native、weex 等。每周都会有 原创 文章分享,我们的文章都会在公众号首发。欢迎关注查看更多内容。