1.4 第一个应用——计数器
在真正领略Flutter的风采之前,我们可以参考本书的附录A安装Flutter开发环境。完成安装操作之后,读者现在就可以跟随本节的内容尝试在没有Dart语言基础的情况下理解Flutter默认实现的一个计数器应用,你会发现一切都比你想象中的简单很多。
1.4.1 创建第一个应用
为了使各个阶段的开发者都能够轻松地完成本书中的操作,本书将会统一采用更加轻量的Visual Studio Code作为编写代码和展示示例的IDE,并且使用macOS进行演示(Windows系统中的操作与之类似)。可以按照如下步骤创建第一个Flutter应用。
(1)启动Visual Studio Code。
(2)选择菜单栏中的“查看”→“命令面板”,如图1.6所示,打开命令面板。
图1.6 选择“查看”→“命令面板”
(3)在命令面板中输入“flutter”,并选择下拉列表框中的“Flutter: New Project”,如图1.7所示,新建一个Flutter项目。
图1.7 选择下拉列表框中的“Flutter: New Project”
(4)输入自定义的项目名称(如hello_world),如图1.8所示,按Return键创建项目。
图1.8 输入项目名称
(5)单击Select a folder to create the project in按钮(见图1.9),指定项目将要放置的位置,这里选择目录后,单击OK按钮。
图1.9 单击Select a folder to create the project in按钮
(6)等待项目创建结束之后,新项目窗口便会自动打开。
1.4.2 Flutter项目的结构
创建Flutter项目后,可以在Visual Studio Code中看到Flutter项目的结构,如图1.10所示。
图1.10 Flutter项目的结构
部分文件夹的作用如下。
- android文件夹:存放Flutter与Android原生交互的代码,该文件夹下的文件和单独创建的Android项目基本一样。
- ios文件夹:对于标准的iOS项目,存放Flutter与iOS原生交互的代码。
- lib文件夹:Flutter的核心目录,存放的是使用Dart语言编写的代码。不管是Android平台,还是iOS平台,安装、配置开发环境后,都可以在对应的设备或模拟器上面运行这里的Dart代码,而整个应用的入口是lib文件夹下的main.dart文件。也可以在这个lib文件夹下面创建不同的文件夹,里面存放了不同的文件来管理日益壮大的应用。
- test文件夹:存放Flutter的测试代码。
注意,pubspec.yaml文件是项目的配置文件,可以在该文件中声明项目中使用到的依赖库、环境版本以及资源文件等。附录A会介绍更多相关内容。
pubspec.yaml的另一个重要功能便是指定应用中需要使用的本地资源(图片、字体、音频、视频等)。通常情况下,我们会在项目根目录下创建一个images目录,用来存放应用中会使用到的图片资源,这些图片资源需要在该配置文件中的assent属性下声明(见图1.11)。
图1.11 pubspec.yaml中的资源文件声明
应用运行在设备上之后,这些资源文件就会一并打包在安装程序中,在之后的章节会对配置文件的其他配置项做具体的介绍。
熟悉了这些文件夹的大致作用之后,我们先尝试运行一下这个默认的项目。为此,我们需要启动模拟器或者使用USB接口接入真机。具体步骤如下。
(1)模拟器打开或者真机接入后,在Visual Studio Code主界面右下角的状态栏中选择可以运行的目标设备(见图1.12)。
图1.12 选择可用设备
(2)打开lib文件夹下的main.dart文件,按F5键或选择菜单栏中的“调试”→“启动调试”,开始运行项目(见图1.13)。
图1.13 选择“调试”→“启动调试”
(3)等待应用在模拟器或真机上自动启动。
(4)如果一切正常,在应用安装成功后,我们应该就能够在设备上看到图1.14所示的计数器应用。
图1.14 计数器应用
此时,我们的第一个应用就已经启动了,你可以看到这个应用的首页展示了一个标准的Material Design风格的界面,顶部有一个带有页面标题的导航栏,右下角有一个带有“+”号的悬浮按钮,单击这个按钮就会使页面中的数字增加。这个应用可以用来记录单击按钮的次数。
1.4.3 计数器应用的实现
已经运行的计数器应用是我们步入Flutter殿堂的阶梯。通过分析这个应用的实现方式,我们会对Flutter中的应用开发有一个直观的理解。
首先,打开lib文件夹下的main.dart文件,这里面存放了这个计数器应用的所有代码。忽视注释中的内容,我们可以在文件的最上方看到带有import字样的代码行,它的作用是导入该文件需要使用到的库。这里我们导入了Material库,因为我们需要使用该这个库下面的UI组件。下面我们可以看到一个main()函数,它是Dart语言的主函数,每当我们运行应用后,系统都会首先调用main.dart文件中的这个函数。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
main()函数中调用了runApp()函数,我们可以将runApp()理解为运行Flutter应用的入口,而传入的MyApp对象就代表了需要运行的应用。在Flutter中,MyApp又称为组件对象,它在这里就相当于应用显示在屏幕上的UI组件,应用启动后就能够显示MyApp中的内容。下面是MyApp组件的具体实现。
class MyApp extends StatelessWidget {
// 重写StatelessWidget的build()方法,返回一个组件对象
@override
Widget build(BuildContext context) {
/*
* MaterialApp表明应用采用Material Design风格,
* 可以在theme属性下配置应用中与主题相关的属性,如颜色、按钮风格
* */
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
通过阅读上面的代码,我们发现类MyApp继承自StatelessWidget,并重写了它的build()方法,这个方法返回了一个组件对象,所以这里我们可以推理出MaterialApp()也是一个组件对象。前面已经提到了“一切皆为组件”,读者可以从这里开始随着阅读本书慢慢地理解这句话了,它是我们开发出用户能看见的应用的基础,我们可以通过设置组件的属性来控制应用所能展示的内容。
继续分析下面的代码,我们可以看到在MaterialApp组件中有3个属性,分别是title、theme、home。其中,title表示组件的标题属性;theme可以用来配置应用的主题样式;home参数用来指定MaterialApp中的主体内容,它接受另一个组件,这里指定为MyHomePage。在使用MyHomePage时,还传入了一个title参数,它用来接受显示在计数器应用顶部导航栏中的标题。我们可以尝试修改这个值然后保存代码,如果程序依然处于运行状态,由于Flutter支持热加载的特性,导航栏中的文字就会实时更新为最新的值,这个特性能够帮助我们更高效地开发应用。
继续向下,我们就可以看到MyHomePage组件的具体实现了。
class MyHomePage extends StatefulWidget {
// 构造函数,用于接受调用者的参数
MyHomePage({Key key, this.title}) : super(key: key);
// 声明了一个字符串类型的final变量,并在构造函数中初始化
final String title;
/*
* 所有继承自StatefulWidget的组件都要重写createState() 方法,
* 用于指定该页面的状态是由谁来控制的。
* 在Dart中,以下划线开头的变量和方法的默认访问权限就是私有的,
* 类似于Java中用private关键字修饰的变量和方法,只能在类的内部访问
*/
@override
_MyHomePageState createState() => _MyHomePageState();
}
/*
* State是一个状态对象,<> 里面表示该状态是与谁绑定的。
* 在修改状态时,在该类中进行编写
*/
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
// 实现计数值加1的函数
void _incrementCounter() {
// setState方法用于更新属性
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
/*
* Scaffold是一个Material Design风格的组件,
* 它继承自StatefulWidget,包含appBar、body、drawer等属性
* */
return Scaffold(
/* 顶部导航栏 */
appBar: AppBar(
/*
* 这里的Widget其实就是MyHomePage,
* 它在这里调用了上面传递过来的title变量
*/
title: Text(widget.title),
),
// Scaffold中的主体布局
body: Center(
/*
* 在Center组件中有一个child属性,用来定义它的子组件Column,
* Column表示以行的形式显示其子组件
*/
child: Column(
/*
* mainAxisAlignment用来控制子组件的对齐方式,
* 也可以把值设置为start、end等
*/
mainAxisAlignment: MainAxisAlignment.center,
/*
* Column组件的children属性用于指定它的子组件,
* 它接受一个数组,可以向该属性传递多个组件
*/
children: <Widget>[
// Text组件,用于显示文本
Text(
'You have pushed the button this many times:',
),
// Text组件,使用style属性来设置它的样式
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
/*
* FloatingActionButton也是Material Design风格的组件,
* 可以在onPressed属性中定义其单击事件
*/
floatingActionButton: FloatingActionButton(
// 通过单击触发_incrementCounter函数
onPressed: _incrementCounter,
tooltip: 'Increment',
// 指定child的子组件为一个“+”号图标
child: Icon(Icons.add),
),
);
}
}
MyHomePage是实现计数器应用的核心,它同样是一个组件,最终会显示在应用中。在_MyHomePageState中,我们可以重写build()方法,返回MyHomePage组件中显示的内容。根据注释,我们可以在代码中找到显示在屏幕中的组件,其中涉及了Scaffold、Column、Text等常用组件,以及对FloatingActionButton响应事件的处理。Dart语言的相关语法和组件的具体含义会在下面的章节中介绍。
在上述代码中,很多组件都有child属性,如FloatingActionButton组件中的child属性是一个“+”图标,它就表示将“+”图标设置为FloatingActionButton的子组件,表现在屏幕上的效果就是“+”图标显示在FloatingActionButton中。通过这种方法,我们可以将多个组件组合在一起而开发出一个完整的页面。
另外,读者还可能注意到一个重要的部分,MyHomePage继承自StatefulWidget,MyApp也继承自一个与它类似的StatelessWidget。作为计数器应用的核心组件,MyHomePage用于改变计数值。也就是说,当单击“+”按钮后,增加计数这个功能需要由它负责。要想达到这样的效果,必然就需要改变_counter变量的值,StatefulWidget是可以改变它对应State对象中的值的一个组件,而StatelessWidget不具备这个功能,它只能用来展示UI。第3章会具体介绍StatefulWidget和StatelessWidget这两个类的具体用法。