Flutter 开发之旅从南到北
上QQ阅读APP看书,第一时间看更新

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这两个类的具体用法。