本文的写作目的是为了方便有一定 UI 编程经验的开发者快速上手 Flutter 开发移动应用,并且对 Flutter 开发的相关主题形成全貌的认识。在每个主题之后,会附有相关的资源链接,方便有兴趣的同学按图索骥深入学习。在文章的最后,整理了一些学习资料,如果想更系统地学习 Flutter 推荐阅读。
Flutter 是 Google 开源的跨端开发方案,支持移动端、Web 以及桌面端开发,所谓 “One Codebase to Rule Them All”。Web 和桌面端的实现目前还不完善,相关功能还在开发中,所以暂时并不能直接投入到生产中使用。但移动端开发已经非常成熟,相比于同为跨端开发方案的 React-Native,Flutter 有势均力敌甚至赶超的趋势,不管是 Google Trends,还是 Stack Overflow Trends 都能从中窥见端倪。以下是 Stack Overflow Trends 上的截图,可以看出 Flutter 的社区活跃度可能已经超过 React-Native。
目前,无论国外还是国内,都已经有非常多的大厂投入使用,并且沉淀出了一些最佳实践。比如国内的阿里闲鱼、腾讯、美团等都有一些不错的相关技术内容输出。
所以为什么 Flutter 能在众多的跨端方案中脱颖而出,并且大有侵蚀原生开发技术的趋势?
可以从以下几个方面来回答这个问题:
相关资源
上文已经提到,Flutter 是基于 Dart 开发的。关于 Flutter 为什么选择 Dart 语言,可以阅读我之前翻译的“为什么 Flutter 选择了 Dart 语言”(见本节附录)。总结而言,主要有以下几点:
我们看下官网上的代码示例,体验一下 Dart 的语法:
// Hello World
void main() {
print('Hello, World!');
}
// Variables
var name = 'Voyager I';
var year = 1977;
var antennaDiameter = 3.7;
var flybyObjects = ['Jupiter', 'Saturn', 'Uranus', 'Neptune'];
var image = {
'tags': ['saturn'],
'url': '//path/to/saturn.jpg'
};
// Control flow statements
if (year >= 2001) {
print('21st century');
} else if (year >= 1901) {
print('20th century');
}
for (var object in flybyObjects) {
print(object);
}
for (int month = 1; month <= 12; month++) {
print(month);
}
while (year < 2016) {
year += 1;
}
// Functions
int fibonacci(int n) {
if (n == 0 || n == 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
var result = fibonacci(20);
// Classes
class Spacecraft {
String name;
DateTime launchDate;
// Constructor, with syntactic sugar for assignment to members.
Spacecraft(this.name, this.launchDate) {
// Initialization code goes here.
}
// Named constructor that forwards to the default one.
Spacecraft.unlaunched(String name) : this(name, null);
int get launchYear =>
launchDate?.year; // read-only non-final property
// Method.
void describe() {
print('Spacecraft: $name');
if (launchDate != null) {
int years =
DateTime.now().difference(launchDate).inDays ~/
365;
print('Launched: $launchYear ($years years ago)');
} else {
print('Unlaunched');
}
}
}
是不是很熟悉,很简单?
关于 Dart 的学习,建议直接阅读官方文档。
相关资源
首先需要安装 Flutter SDK,具体可以参考官方的安装指南(见附录)。安装完成后在命令行执行:
flutter doctor
会输出类似以下的结果,如果是首次安装,可能会有更多的检查项不通过,可以根据提示来完善开发环境。
根据官方指南配置好环境以后,需要选择一个 IDE 进行开发,Android Studio 和 VSCode 都是不错的选择,本文以 Android Studio 为例来说明。
下载安装 Android Studio 并且设置好模拟器(见 Flutter 官方安装指南 Set up the Android emulator 部分),为了方便 Flutter 开发需要安装 Dart 和 Flutter 插件,如下图所示:
安装完成以后在 Android Studio 启动欢迎窗口会多出一个 “Start a new Flutter project” 入口,点击可以快速创建一个 Flutter 应用。
相关资源
点击“Start a new Flutter project”,选择“Flutter Application” 创建 Flutter 应用。初始化项目的结构如下所示:
ios 和 android 目录下是相关平台的工程文件,lib 是我们的开发目录,其中 main.dart 是入口文件。该文件中有一个 main 函数,是应用启动的入口。
另外,pubspec.yml 这个文件很重要,它是项目的配置文件,类似于前端开发中的 package.json,可以设置应用名、版本、依赖、本地资源声明等。
下一小节,我们将通过阅读入口文件 main.dart 中的样板代码来介绍 Widget。
Widget 是 Flutter 中非常重要的概念,可以说 Flutter 中一切都是 Widget,比如 UI 组件、布局、动画等等。以下是初始化项目 main.dart 中的样板代码(删除了大量注释):
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
我们可以看到入口函数 main 中调用了 MyApp,而 MyApp 即是应用的根 Widget。它是一个类,继承了 StatelessWidget。
Widget 根据是否包含状态可以分为 StatelessWidget 和 StatefulWidget,有过 React 开发经验的同学看到这里应该会很熟悉,和 React 中无状态组件一样,StatelessWidget 相对性能更好,适合开发展示型的组件。
MyApp 中有一个 build 方法,组件每次渲染都会调用,类似 React 中的 render 函数。build 方法返回的也是 Widget,这里返回了一个 MaterialApp(因为我们引入了 flutter/material.dart)。它是 Flutter 官方提供的 Widget,用于开发 Material 风格的应用。更多关于 Material Widget 的内容可以参考本节附录中的官方 Widget 列表。
MaterialApp 接受很多配置参数,其中 home 是应用的入口页面,这里是 MyHomePage 这个 Widget。它继承了StatefulWidget,所以内部可以有状态。此处它维护了一个 _counter 状态,每次点击 FloatingActionButton 这个 Widget 会调用 _incrementCounter 方法,进而调用 setState 更新 _counter 的值。看到这里,熟悉 React 的你是不是莞尔一笑。
MyHomePage 的 build 方法中,包含了很多 Widget。我们一个一个来看:
在 Android Studio 中,选择模拟器后,可以点击右侧的三角按钮运行应用,如下所示:
从下图中可以看到各种 Widget 的显示效果:
Flutter 提供了大量的官方 Widget,想了解更多强烈建议学习附录中的资料。
相关资源
Flutter 最早是 Chrome 团队成员的实验项目,所以对布局一开始就有一些特殊的思考。为了避免传统 CSS 布局存在的相互覆盖、解析性能问题,在经过一些实验后,Flutter 团队通过以下方式简化了布局,并提升了性能:
Flutter 中的布局也是通过 Widget 实现的。除了上一小节中介绍的 Center、Column,常用的布局 Widget 还包括 Row、Stack、Expanded、ConstrainedBox、Align、Container 等。出于篇幅考虑,不可能对所有布局组件一一介绍,有兴趣的同学可以阅读附录中的相关资料进一步学习。这里以常用的 Expanded 为例来展示 Flutter 灵活的布局能力。
因为出于 Chrome 团队,Flutter 布局借鉴了很多 CSS 布局思想。Expanded 可以使用 flex 来进行布局,示例代码如下:
Row(
children: <Widget>[
Expanded(
child: Container(
decoration: const BoxDecoration(color: Colors.red),
),
flex: 3,
),
Expanded(
child: Container(
decoration: const BoxDecoration(color: Colors.green),
),
flex: 2,
),
Expanded(
child: Container(
decoration: const BoxDecoration(color: Colors.blue),
),
flex: 1,
),
],
),
最终显示效果:
相关资源
Flutter 中的动画是相对不容易理解的部分。下图是官方提供的导航图,教你如何选择正确的动画实现方式。有耐心的同学可以先自己看一下。
按照动画的实现方式,大体可以分为以下几类:
import 'package:flutter/material.dart';
class FadeInDemo extends StatefulWidget {
_FadeInDemoState createState() => _FadeInDemoState();
}
class _FadeInDemoState extends State<FadeInDemo> {
double opacityLevel = 0.0;
Widget build(BuildContext context) {
return Column(children: <Widget>[
MaterialButton(
child: Text(
'Show details',
style: TextStyle(color: Colors.blueAccent),
),
onPressed: () => setState(() {
opacityLevel = 1.0;
}),
),
AnimatedOpacity(
duration: Duration(seconds: 3),
opacity: opacityLevel,
child: Column(
children: <Widget>[
Text('Type: Owl'),
Text('Age: 39'),
Text('Employment: None'),
],
),
)
]);
}
}
点击 MaterialButton 按钮,AnimatedOpacity 的 child,也就是三个 Text 文本的透明度会从 0 渐变成 1,产生渐入的效果。很简单对吧,这正是 Implicit Animations 的使用场景,通过牺牲更精细的控制使简单动画的实现更容易。
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
void main() => runApp(LogoApp());
class LogoApp extends StatefulWidget {
_LogoAppState createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
void initState() {
super.initState();
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addListener(() {
setState(() {});
);
controller.forward();
}
Widget build(BuildContext context) {
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: FlutterLogo(),
),
);
}
void dispose() {
controller.dispose();
super.dispose();
}
}
上面的代码很容易理解,Animation 维护了动画的值(animation.value)和状态(dismissed、 completed 等),AnimationController 负责控制动画(controller.forward 等),Tween 提供了动画的范围,这里是 double 类型的 0 到 300。通过 addListener 监听数值变化,然后调用 setState 去更新视图。其中 .. 是 Dart 中的语法,可以实现链式的调用。
此外,你也可以使用 AnimatedWidget、AnimatedBuilder 来实现自动监听动画值进而重新渲染视图。具体做法可以参考附录中的官方教程。最后值得一提的是,在 dispose 方法中调用了 controller.dispose(),确保页面销毁后释放动画相关资源,避免内存泄漏。
相关资源
好的交互体验离不开灵活方便的手势操作。Flutter 对此有非常好的支持。如果使用 Material 组件,很多 Widget 本身已经实现了响应 tap 等手势操作。比如上文中提到的 FloatingActionButton,可以支持 onPressed 监听。另外如果你想实现 Material UI 中非常酷炫的 “ink splash” 效果,可以使用 InkWell 这个类。
GestureDetector 是 Flutter 中实现手势操作最重要的类,它本身也是一个 Widget。它支持非常多的手势监听,比如 tap、pan、scale、drag 等,具体可以参考附录中的文档。下面以一个简单的例子来演示如何使用 GestureDetector 做手势监听。
Container(
alignment: FractionalOffset.center,
color: Colors.white,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
Icons.lightbulb_outline,
color: _lights ? Colors.yellow.shade600 : Colors.black,
size: 60,
),
),
GestureDetector(
onTap: () {
setState(() {
_lights = true;
});
},
child: Container(
color: Colors.yellow.shade600,
padding: const EdgeInsets.all(8),
child: const Text('TURN LIGHTS ON'),
),
),
],
),
)
GestureDetector 包裹了 Container,监听 onTap,点击后调用 setState 修改 _lights 状态,使灯泡变亮。
相关资源
在 Flutter 中实现路由跳转非常简单。如果之前有前端开发经验也会感觉似曾相识。这是 Flutter 容易上手的原因之一——借鉴了其他 UI 编程技术中的实践,使得开发者可以复用经验,降低学习成本。
MaterialApp 封装了路由相关的功能。在 Flutter 中可以将路由简单分成直接路由和命名路由,我们先来看直接路由的例子:
Navigator.push(context, MaterialPageRoute(
builder: (context) => Page2()
));
上面的代码中,调用 Navigator.push 来进行路由导航,第一个参数是上下文对象,第二个参数是一个 MaterialPageRoute 实例,其中有一个 builder 方法,返回的正是导航的目标页面 Page2。
命名路由,需要在 MaterialApp 的 routes 属性中配置,如下所示:
void main() {
runApp(MaterialApp(
home: Landing(),
routes: <String, WidgetBuilder>{
'/page2': (BuildContext context) => Page2(),
'/page3': (BuildContext context) => Page3()
},
));
}
然后通过以下的方式来进行导航:
onPressed: (() => Navigator.of(context).pushNamed('/page2')),
上面的代码表示,在某个 Widget 的 onPressed 事件回调函数中,调用 pushNamed 方法跳转到 Page2。
可以通过以下的方式进行路由回退:
Navigator.pop(context);
另外,路由导航返回的是一个 Future,类似 JS 中的 Promise,是 Dart 异步编程中非常重要的概念。同样可以用 await 语法将异步编程转换成同步的写法,以下是简单的示例:
var navigationResult = await Navigator.push(
context, new MaterialPageRoute(builder: (context) => Page2()));
if(navigationResult == 'my_value') {
print('I have received results from the navigation');
}
// 通过第二个参数返回 navigationResult
Navigator.pop(context, 'my_value');
相关资源
在 Flutter 中实现 http 请求可以使用官方 dart:io 库中提供的 HttpClient 类,也可以使用第三方的库,比如 http,以下分别做简单介绍。
HttpClient client = new HttpClient();
client.getUrl(Uri.parse("http://www.example.com/"))
.then((HttpClientRequest request) {
// Optionally set up headers...
// Optionally write to the request object...
// Then call close.
...
return request.close();
})
.then((HttpClientResponse response) {
// Process the response.
...
});
可以分为两个过程,首先 getUrl 方法返回的是一个 Future 对象,所以可以调用 then 方法,在 then 的回调中设置 request 对象,最后返回 request.close() 。然后,request.close() 返回的也是一个 Future,所以继续调用 then 方法,该方法回调函数的参数即是请求响应对象。
使用第三方库,首先需要在项目配置文件 pubspec.yaml 中添加依赖:
dependencies:
http: ^0.12.2
使用方式可以参考 pub.dev 上 http 的文档:
import 'package:http/http.dart' as http;
var url = 'https://example.com/whatsit/create';
var response = await http.post(url, body: {'name': 'doodle', 'color': 'blue'});
print('Response status: ${response.statusCode}');
print('Response body: ${response.body}');
print(await http.read('https://example.com/foobar.txt'));
相关资源
Flutter 状态管理的方案非常多,普遍被推荐和使用的有 Provider、Scoped Model、BLoC 等。我之前有翻译过一篇 “BLoC 模式入门”,感兴趣的同学可以通过附录链接阅读。因为要介绍清楚每种状态管理方案都需要一篇单独的文章,所以不再继续深入,读者可以直接阅读附录中的三篇文章学习。
相关资源
这篇文章旨在提供 Flutter 开发的 “最少必要知识”,并且就每个知识主题提供相关资料方便读者进一步深入学习。同时它也是我最近一年断断续续学习 Flutter 的总结,随着自己相关知识和使用经验的积累,我会持续完善这篇文章,尽量保证文章的知识正确性和时效性。最后,我列出了一些系统性学习 Flutter 的资料和课程,有兴趣的同学可以阅读观看。