Flutter App开发:从入门到实战

978-7-115-56875-5
作者: 李元静
译者:
编辑: 刘雅思

图书目录:

详情

Flutter是谷歌公司推出的跨平台开源UI框架,同时支持Android App与iOS App开发,使用这一框架可以大大提高开发效率。本书共14章,系统讲解Flutter背景、Dart语言的语法基础、Flutter组件、状态管理、事件处理、路由管理、动画、网络编程、数据存储、相机、主题与国际化、混合开发等核心内容,并通过多个案例以及“天气预报”App和“我的视频”App两个完整的实战项目,将理论知识与实践结合,提升读者的实战开发能力。通过对本书的学习,读者将会对Flutter框架以及跨平台开发有全面的认识,并可在实践中使用Flutter大大提高移动开发效率。 本书适合正在使用Flutter以及对Flutter感兴趣的开发人员阅读和参考。

图书摘要

版权信息

书名:Flutter App开发:从入门到实战

ISBN:978-7-115-56875-5

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。


著    李元静

责任编辑 刘雅思

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


Flutter是谷歌公司推出的跨平台开源UI框架,同时支持Android App与iOS App开发,使用这一框架可以大大提高开发效率。本书共14章,系统讲解Flutter背景、Dart语言的语法基础、Flutter组件、状态管理、事件处理、路由管理、动画、网络编程、数据存储、相机、主题与国际化、混合开发等核心内容,并通过多个案例以及“天气预报”App和“我的视频”App两个完整的实战项目,将理论知识与实践结合,提升读者的实战开发能力。通过对本书的学习,读者将会对Flutter框架以及跨平台开发有全面的认识,并可在实践中使用Flutter大大提高移动开发效率。

本书适合正在使用Flutter以及对Flutter感兴趣的开发人员阅读和参考。


当人民邮电出版社的刘雅思编辑发来约稿消息时,我十分惊喜。虽然我从事移动开发工作已经很多年,但是从来没有考虑过自己写一本书,只是在业余时间写写博客。能收到出版社主动邀约,在我看来是完全想象不到的事情。这件事竟然真的发生在我身上,可想而知我当时的心情是多么激动、欣喜。

但是,当我接下Flutter新书的写作任务时,倍感责任重大,又开始忧虑如何完成这本书。对IT行业的从业者来说,日常工作已经非常繁忙,如何在约定的时间内保证书稿质量并按时交稿,是一件非常令人头疼的事情。而且,写一本书最难的并不是内容的创作,而是对书的整体框架的规划。

不过,后来我发现,我可能忧虑得有点儿过头了,毕竟什么事情都是“起于微末,发于华枝”。在开始写书的第二周,我做了一个重大的决定—边写边构思书的整体框架。很明显这个方法奏效了,因为任何知识的学习都需要遵循由易到难、循序渐进的逻辑规律,只要掌握了其中的规律,书的整体框架自然就非常清晰了。

李元静

2020年10月14日


随着3G、4G以及5G通信技术的发展,越来越多的人养成了使用移动互联网的习惯。移动互联网已经成功超越了传统互联网,成为互联网发展的主方向。面对当前的移动互联网时代,谁能掌握并构建属于自己的移动互联网生态,谁就能站在“时代的风口”上。

目前,移动互联网的发展已经相当成熟,开发人员不只关注如何开发移动App,也在思考如何更高效、更低成本地维护App。虽然传统的原生开发技术经过10余年的发展已经非常成熟和完善了,但其依然受制于开发效率与维护成本,越来越无法适应移动跨平台框架行业发展的迫切需求。

如今,比较成熟的跨平台技术有两种:一种是通过浏览器加载本地网页,App相当于本地网站,对应的技术有PhoneGap、Cordova和Ionic等;另一种则是通过在不同平台上运行某种语言的虚拟机来实现App的跨平台,此种方案也是移动跨平台的主流方案,代表技术有Flutter、React Native和Weex等。

Flutter是谷歌公司于2014年10月开源的一套移动跨平台开发框架。当时,Flutter被称为“Sky”,最开始仅支持Android平台,截至本书成书时已支持Android与iOS两大平台。

自2018年12月发布Flutter 1.0以来,Flutter在GitHub平台的贡献增速长期稳居前三,每一位移动开发人员都在为Flutter“快速开发”“创建灵活且富有表现力的UI”“原生性能”的特色和理念而“痴狂”。从超级App到独立App,从纯Flutter到混合开发,开发人员在不同的场景下乐此不疲地探索和应用着Flutter技术。这说明Flutter跨平台技术已经受到了非常多的开发人员的青睐。

下面概括介绍本书各章的主要内容。

第1章将详细介绍Flutter的历史、优势以及开发环境的搭建等内容。通过这一章的学习,你将了解Flutter技术,掌握在主流操作系统中编译和测试Flutter项目的技能。

第2章将详细讲解Dart语言的特性和用法。通过这一章的学习,你将掌握Flutter开发的基础语言。

第3章~第11章将讲解Flutter开发的基础知识,包括Flutter组件、状态管理、事件处理、路由管理、动画、网络编程、数据存储、相机、主题与国际化等基础知识。通过这9章的学习,你将掌握Flutter开发的基础技能。

第12章将讲解在实际开发中混合开发的基础应用。通过这一章的学习,你可以掌握谷歌公司提供的两种混合开发方案,也能掌握闲鱼开发团队开发的FlutterBoost混合开发框架。

第13章与第14章将通过两个实战项目巩固本书涉及的Flutter知识,以达到理论知识与实践结合的目的。同时,通过这两章的实战练习,你能够切身感受到Flutter相对于原生开发的魅力。

“千里之行,始于足下”,现在学习Flutter技术正当其时,让我们一起开启Flutter的学习之旅吧!


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

本书提供示例与章末习题的源代码,要获得配套资源,请在异步社区本书页面中点击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,点击“提交勘误”,输入勘误信息,点击“提交”按钮即可(见下图)。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

本书责任编辑的联系邮箱是liuyasi@ptpress.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频或者参与技术审校等工作,可以直接发邮件给本书的责任编辑。

如果您来自学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


自从“大前端”的概念被提出来以后,移动端与前端之间的边界越来越模糊。例如,开发人员可以通过Android与iOS原生技术来开发App,也可以使用React Native(RN)、H5等前端框架开发App。这些技术无不体现着移动端开发的前端化。

但不管是使用移动端原生开发App,还是使用前端技术开发App,都会导致同一个App被多端重复开发的问题,大大增加开发成本。正因如此,“一次编写,处处运行”成了移动互联网企业的美好愿景,但实际上往往无法如愿以偿。目前业界比较成熟的跨平台技术几乎都存在一定的缺陷,例如,微信小程序(基于X5内核的WebView)渲染耗时过长、白屏率过高会影响转化收益,能实现的功能非常受限;React Native具有性能不足、问题排除难、维护成本高等缺陷。所以,到目前为止,开发人员依然需要准备两套代码,分别运行在Android端与iOS端,这样不仅增加了开发的成本,还要维护两端代码,耗时、耗力。而Flutter的出现,让这些开发问题有所改善。

说了这些,想必你已经初步了解Flutter技术,迫不及待地想要加入这个“阵营”。那么,从现在开始,我就带你踏上学习Flutter的“旅程”,一步步引导你成为一名合格的跨平台开发人员。

Flutter是谷歌公司推出的一套跨平台的开源用户界面(User Interface,UI)框架,同时支持Android App与iOS App开发。Flutter在2017年5月发布第一个版本,在2018年12月发布了Flutter 1.0的稳定版,到本书成书时的版本是Flutter 1.13.6。

从Flutter团队开发的那么多版本,再结合目前的技术优势来看,谷歌公司正在大力推广Flutter。不管是在GitHub社区,还是在Stack Overflow网站上,关于Flutter的开源项目以及相关的提问越来越多。一直备受关注的、神秘的Fuchsia操作系统使用的也是Flutter的UI框架,这也增强了大家对Flutter的信心。下面让我们详细地了解Flutter的优势。

与其他前端技术相比,Flutter有着明显的优势。这些优势融入基础语言和软件开发工具包(Software Development Kit,SDK)设计中,以解决其他前端技术的常见问题。想要知道你为什么要为下一个项目选择Flutter,不妨先了解其技术优势。

一提到跨平台性,估计很多前端开发人员都会想到Facebook公司推出的React Native开发框架。开发人员可以利用JavaScript和React Native获得一致的开发体验,但其实React Native在实际的平台上还需要适配和桥接差异。

Flutter就不一样,它是依靠Flutter引擎虚拟机在iOS和Android上运行的,开发人员可以通过Flutter框架和应用程序接口(Application Programming Interface,API)在内部进行交互。Flutter引擎是使用C/C++编写的,具有低延迟输入和高帧率的特点。所以Flutter不仅实现了将一套代码部署在多个平台,而且适合开发游戏,能达到120fps的超高性能技术实现。除此之外,Flutter还提供了自己的小组件集合,可以直接在操作系统提供的画布上描绘控件。

Flutter开发不使用WebView这种比较“老”的开发模式,也不使用操作系统的原生控件,而是使用Skia作为二维渲染引擎,保证了各个平台上UI的一致性,通过“自绘UI+原生系统”,可以实现高帧率的流畅UI,也可以避免对原生控件的依赖所带来的高昂的维护成本。

对经常开发移动应用的程序员来说,重复修改代码,然后运行项目校验,会浪费很多不必要的开发时间。Flutter的热重载功能可以帮助移动应用在无须重新启动的情况下轻松完成测试、构建UI,以及修复代码中的错误。

将更新后的源码文件注入正在运行的Dart虚拟机(Virtual Machine,VM)即可实现热重载。在虚拟机使用新的字段和函数更新类后,Flutter框架会自动重新构建Widget树,以便快速查看更改的效果。

对移动应用开发人员来说,如果经常使用Kotlin语言开发Android应用,往往会选择Android Studio开发工具;而对前端应用开发人员来说,常用的开发工具是Visual Studio Code(VSCode)。

前端应用开发人员和移动应用开发人员各自使用不同的开发工具。而Flutter并不在意这些,不管你是前端的VSCode开发人员还是移动端的Android Studio开发人员,都可以轻而易举地使用自己常用的开发工具来搭建Flutter的开发环境。

谷歌公司作为Flutter技术的贡献者,直接在Android与iOS两个平台上重写了各自的UIKit,并将其对接到平台底层,减少UI层的多层转换,可直接调用系统的API绘制UI。因此,Flutter的性能更接近原生,尤其在操作界面和播放动画的效果上非常明显。

不管你是学习过React Native框架,还是使用Java或者Kotlin语言开发过Android Studio移动端App,都可以无障碍地快速掌握Flutter开发框架。如果你是具有前端或者原生开发经验的程序员,那么学习起来会更加容易。

在Flutter跨平台技术诞生之前,已经有许多前端技术,例如前文提到的React Native以及Weex、Qt、Ionic等技术,这些技术都被应用在各大App中。表1-1所示为一些前端技术的对比。

表1-1 一些前端技术的对比

前端技术

技术类型

UI渲染方式

性能

开发效率

动态化

Flutter、Qt

自绘UI+原生系统

调用系统API渲染

很好

Flutter高、Qt低

默认不支持

React Native、Weex

JavaScript+原生渲染

原生控件渲染

很好

支持

Ionic、Cordova

HTML5+原生系统

WebView渲染

一般

支持

从表1-1来看,实现了自绘UI功能的技术除了Flutter,还有Qt。但是Qt有一个缺陷,因为Qt使用的是C++语言,而Flutter使用的是Dart语言,所以从开发效率来说,Qt比Flutter低。而且Flutter技术开发时使用即时(Just In Time,JIT)模式编译,调试快,所见即所得。

Flutter的Release包默认是使用Dart的提前(Ahead Of Time,AOT)编译模式编译的,不支持动态化。不过从表1-1可以看出,Flutter默认不支持动态化,但并不是不支持动态化。如果你使用Flutter进行开发,可以通过Dart语言中的JIT和SnapShot等运行方式间接达到动态化。关于Dart语言的优势,后文会有相应介绍。

每学习一种新的技术,我们都要对其整体技术架构有一定的了解。这就好比造车子要有车辆设计图,写代码同样要先了解其架构,然后根据其核心架构实现各种各样的需求。所以,我们先来看看Flutter的架构,如图1-1所示。

图1-1形象地展示了Flutter的架构,可以看到,Flutter架构分为两部分,一部分是框架,另一部分是引擎。

如图1-1所示,框架是由纯Dart语言实现的,包括UI、文本、图片和按钮等Widgets,以及Rendering(渲染)、Animation(动画)、Gestures(手势)等层。Dart语言是Flutter的官方语言,将在第2章重点讲解,这里先介绍图1-1中相关层的具体内容和作用。

图1-1

(1)Foundation层与Animation、Painting、Gestures层,这两层提供了动画、绘制以及手势操作,是谷歌公司专门提供给开发人员调用的。

(2)Rendering层,这一层负责构建UI树,也就是当UI树上的Element发生变更时,会重新计算变更部分的位置以及大小,并更新UI树,也就是更新界面,最终将更新的界面呈现给用户。

(3)Widgets层与Material、Cupertino层,其中Widgets层是Flutter提供的基础组件库。Material和Cupertino是另外两种视觉风格的组件库。在绝大多数情况下,使用官方提供的基础组件库就能满足多样化的日常开发需求。

引擎是由纯C++实现的SDK,主要包括Skia、Dart和Text。Framework层中所有的UI库都会调用引擎层。

(1)Skia:一个开源的二维图形库,提供了多种软/硬件平台的API,其已作为Google Chrome、Chrome OS、Android、Mozilla Firefox、Firefox OS等众多产品的图形引擎。但是因为iOS并不自带Skia,所以iOS包所占存储空间比其他操作系统的大。

(2)Dart:主要包括Dart Runtime、内存垃圾回收(Garbage Collection,GC),如果是Debug模式的话,还包括JIT支持。在Release和Profile模式下,是AOT编译成了原生的ARM代码,并不存在JIT部分。

(3)Text:文字排版引擎。

到本书成书时,官方已经发布了Flutter 1.13.6的稳定版,基于这个版本,我们将以图文形式介绍Flutter开发环境是怎样搭建的。如果读者已有安装Flutter开发环境的经验,可以跳过本节。

谷歌公司作为Flutter的“缔造者”,其推荐的开发工具是Android Studio,所以使用Android Studio是必不可少的。而且后续的开发都是通过Android Studio实现的,所以这里我们先来安装Android Studio。安装步骤如下。

(1)在Android官网下载Android Studio安装包。

(2)下载完成之后,如果你使用的是Windows操作系统或者macOS,只需要根据Android Studio的安装引导界面,一直点击“Next”进行安装即可。如果你使用的是Linux操作系统,请下载相应的Linux版本的Android Studio,通过命令行进行安装,如代码清单1-1所示。

代码清单1-1 在Linux操作系统中安装Android Studio

//解压
unzip -x /下载/android-studio-ide-192.6392135-linux.tar.gz
//移动解压的文件夹
mv /下载/android-studio/program/Android/

(3)创建Android模拟器。启动Android Studio之后,在菜单栏中选择“Tools”→“AVD Manager”→“Android Virtual Device Manager”,然后点击“Create Virtual Device”按钮,选择一个你需要的设备,如图1-2所示。

图1-2

和前文步骤一样,一直点击“Next”即可完成创建。

目前Android Studio默认不能开发Flutter应用,所以我们需要给Android Studio添加Flutter开发库。因为Flutter的官方语言是Dart语言,所以我们还需要安装Dart语言库。操作步骤如下。

(1)在1.5.1节中,我们已经安装好了Android Studio,这里首先启动Android Studio。

(2)在Android Studio中,选择“Settings”→“Plugins”,搜索Flutter并安装,如图1-3所示。

图1-3

(3)安装Flutter之后,重启Android Studio,我们会发现一个新的选项“Start a new Flutter project”,如图1-4所示。如果有这个选项,说明Flutter安装成功。

图1-4

(4)虽然我们已经在Android Studio中安装了Flutter开发库,但其实只是多了一个按钮,并没有安装Flutter SDK,所以我们还需要安装Flutter SDK。Flutter SDK的安装比较简单,直接点击“Start a new Flutter project”选项,进入下一个界面后,点击“Install”按钮,选择Flutter SDK的安装目录,就会自动下载Flutter SDK,如图1-5所示。

图1-5

(5)下载Flutter SDK之后,就可以创建项目了,如图1-6所示。

图1-6

(6)虽然环境搭建成功后就可以进行基本的Flutter开发了,但是为了方便后续的命令行操作,我们还需要配置两个环境变量。这里以在Windows操作系统中的配置为例,首先,在其系统变量中添加ANDROID_HOME,也就是Android的SDK目录,如图1-7所示。

图1-7

其次,我们还需要在系统变量Path中添加Flutter的环境变量,如图1-8所示。

图1-8

如果配置之后不知道是否配置成功,可以在Windows操作系统中通过命令行flutter doctor进行验证。如果显示结果如图1-9所示,就说明配置成功。

图1-9

如果你是前端开发人员,并且一直使用VSCode,现在想使用Flutter开发手机应用,也可以直接使用VSCode进行开发。首先要在VSCode中安装Flutter开发插件,安装步骤如下。

(1)启动VSCode,在菜单栏中选择“View”→“Command Palette”,会出现一个搜索文本框,然后输入“install”,点击“Extensions:Install Extensions”,如图1-10所示。

图1-10

(2)之后会出现图1-11所示的界面,然后搜索“flutter”并安装。

图1-11

(3)安装完成后,同样通过第一步的“View”→“Command Palette”搜索“flutter”,然后点击“Flutter:New Project”创建项目,从而进行开发,如图1-12所示。

图1-12

因为第2章会重点讲解Dart语言,所以为了测试后续的代码,我们同样需要搭建Dart语言开发环境。因为我们的主要工具是Android Studio,所以我们先使用Android Studio搭建Dart语言开发环境。

(1)在Android Studio中,选择“Settings”→“Plugins”,搜索“Dart”并安装,如图1-13所示。

图1-13

(2)安装完成之后,我们就可以运行Dart文件。但是Android Studio目前并不能创建纯Dart项目,而只能通过Flutter项目运行.dart文件,所以用户体验不是很好,如图1-14所示。

图1-14

Android Studio虽然可以单独在Flutter项目中测试Dart语言的脚本,但是如果想创建纯Dart项目,Android Studio目前无法做到,所以只能另辟蹊径。如果你是前端开发人员,可以直接使用VSCode创建纯Dart语言项目;如果你之前从事Java、Kotlin等语言的开发,推荐使用IntelliJ IDEA这款开发工具创建纯Dart语言项目。下面我们来搭建纯Dart语言开发环境。

(1)在IntelliJ IDEA官网下载IntelliJ IDEA。

(2)通过一直点击“Next”安装IntelliJ IDEA。

(3)打开IntelliJ IDEA,会看到Dart分类,点击后选择Dart SDK path就可以创建其项目,Dart SDK path在我们之前安装Flutter SDK目录下的bin\cache\dart-sdk中,如图1-15所示。(这里的Dart SDK版本为2.8.0。)

图1-15

(4)选择之后,就可以点击“Next”,一步一步创建纯Dart语言项目了,如图1-16所示。

图1-16

通过前文开发环境的搭建,以及flutter doctor命令的检测,运行环境已经准备就绪。现在,我们就来创建第一个Flutter项目,看看Flutter项目的结构。

前文已经介绍了使用Android Studio和VSCode创建Flutter项目的方式,这里不再重复。使用一种纯命令行的方式创建一个Flutter项目,创建项目的命令如代码清单1-2所示。

代码清单1-2 使用纯命令行创建Flutter项目

Flutter create myapp

输入命令之后,可能会卡顿一会儿,但是别担心,这是正常的项目创建过程,而且当项目创建完成之后,也会自动执行flutter doctor命令来诊断项目有没有问题。创建过程如图1-17所示。

图1-17

虽然我们使用命令行创建Flutter项目,但对编写代码来说,使用工具要比使用记事本方便许多,所以我们将创建的“myapp”项目导入Android Studio中。图1-18所示是将“myapp”项目导入Android Studio后的目录结构。

图1-18

现在我们来分析一下各个目录以及文件的用途。

(1)android目录:该目录存放Flutter与Android原生交互的一些代码文件。该目录中的文件和单独创建的Android项目基本一样,不过该目录中的代码配置和单独创建的Android项目有些不一样。

(2)ios目录:android目录下面就是ios目录。同样,该目录存放Flutter与iOS原生交互的一些代码文件。

(3)lib目录:该目录存放main.dart文件,包含程序员开发的Dart代码。不管是在Android平台,还是在iOS平台,这个目录下的Dart代码都可以运行。

(4)test目录:用于存放测试代码文件。

(5)pubspec.yaml文件:与Android项目中的build.gradle(App)文件一样,它是Flutter项目的配置文件,如配置远程pub仓库的依赖库,或者指定本地资源(图片、字体、音频、视频等)。

以上5个目录和文件在项目中用得最多,所以需要重点了解,其他文件暂时用不到,这里就不再赘述。我们编写的大多数代码保存在lib目录的main.dart文件中,main.dart文件又是程序的入口文件。

接着,我们来运行这个项目,看看其在模拟器上显示的效果,如图1-19所示。

可以看到,默认创建的Flutter项目是一个简单的点击计数器,只要点击右下角的“+”按钮,计数器会根据点击的次数,自动累加并将其显示到界面上。

图1-19

前文提到过,Flutter开发最重要的优势之一是热重载。接下来,我们来体验一下“热重载”。例如,我们试图修改main.dart文件中的代码,将“You have pushed the button this many times:”改成“自动累加的次数:”,如图1-20所示。

图1-20

改完之后,直接保存lib/main.dart文件,或者按“Ctrl+S”组合键保存,模拟器的界面马上发生了变化,如图1-21所示。

这个特性在开发过程中是非常有用的,有点类似于前端的webpack实现的热重载功能,对开发人员非常友好,也提高了开发人员的开发效率。

图1-21

1.通过本章的学习,详细地说出Flutter的技术特性,以及其采用了何种语言进行开发。

2.详解介绍Flutter的架构,并且说明其架构中每层的功能。

3.说明开发人员进行Flutter开发时主要用到的目录有哪些,编写的代码文件保存在哪个目录下。

4.通过命令行的方式创建一个名为“flutter_demo”的项目。

5.通过Android Studio的可视化界面创建一个名为“flutter_demo”的项目。


本章将详细讲解Dart语言,对想系统学习Dart语言的读者来说,建议认真地学习本章的所有内容。如果你已经掌握了一些其他编程语言,可以跳过本章的部分内容。本书基于Dart 2.8.0进行讲解。

Dart语言是谷歌公司发布的一种开源编程语言,其最开始的目标是成为下一代Web开发语言,目前已经用于Web、服务器、移动应用以及物联网等领域的开发,可以说已经覆盖全平台。Dart语言在设计之初,就吸取了其他编程语言(如Java、C、JavaScript等)的优点,所以对已经掌握其他编程语言的读者来说,入门非常简单。

Dart语言是一门纯对象的编程语言。这意味着Dart语言在运行时所处理的值都是对象,甚至包括一些其他语言常见的基本类型的值(如数值、布尔值等)以及函数都是对象,无一例外。

而且Dart语言对所有数据都是统一处理的,这大大方便了所有与语言相关的人员,如语言的设计者、语言的实现者以及重要的开发人员。

举一个例子,集合类能够操作任意类型的数据,使用者无须关注自动转箱、拆箱的问题。也就是说,开发人员无须关注底层的细节实现,这样开发人员就彻底地解放了。Dart语言采用统一对象模型的同时,简化了系统实现者的任务。

面向对象的核心思想是关注对象的行为而非对象的内部实现。遗憾的是,这一原则往往被忽略或误解。Dart语言往往通过以下几种方式来维护这个原则。

(1)Dart语言没有final方法,允许重写除内置操作符外的所有方法。

(2)Dart语言是基于接口而不是基于类的。所以在Dart语言中,任何类都隐含一个接口,能够被其他类实现,而不管其他类是否使用了同样的底层实现,除了数值、布尔值和字符串类。

(3)Dart语言把对象都进行了抽象的封装,确保所有外部操作都通过存取的方式来改变对象的状态。

(4)Dart类的构造函数允许对对象进行缓存,或者从子类创建,因此使用构造函数并不意味着绑定了一个具体的实现。

类型可选照顾了那些不愿意与类型系统“打交道”的开发人员。因此,完全可以把Dart语言当成一门动态的类型语言使用。Dart语言的具体定义如下:

虽然类型可选,但只要代码中有类型注解,就意味着代码有了额外的文档,所有的开发人员都能从中受益。对于代码中存在的类型不一致或者遗漏,Dart编译器会告警,但不会报错。同时,Dart编译器也不会拒绝一段类型不一致或者遗漏的代码。因此,使用类型不会限制开发人员的工作流程,类型不一致或者遗漏的代码依然可以用来测试和实验。

介绍完Dart语言的特性之后,下面我们来开启学习Dart语言的“大门”。程序员接触某种新语言或者新技术的时候,都无法逃脱Hello World的“真香定律”。所以,我们了解了Dart语言的一些特性之后,就从使用Dart语言输出“Hello World”开始入门,如代码清单2-1所示。

代码清单2-1 Hello World

void main(){
    print("Hello World");
}

我们可以通过第1章安装的Dart语言开发工具IntelliJ IDEA运行这段代码,也可以在环境变量中配置Dart SDK后,通过命令行运行这段代码,如图2-1所示。

图2-1

添加环境变量之后,执行命令dart helloworld.dart,稍等片刻就会输出结果。如你所愿,控制台输出的内容就是“Hello World”,如图2-2所示。

图2-2

变量与常量几乎是所有语言的编程基础,也是最基本的编程知识之一。下面,我们就来简单地介绍如何使用Dart语言定义变量与常量。

Dart语言使用var定义一个实例变量,这一点与JavaScript定义变量的方式一致。通过var定义一个变量,如果定义的时候没有给变量赋值,那么默认变量的值为null。

例如,我们定义一个叫作apple的字符串变量,如代码清单2-2所示。

代码清单2-2 变量的使用

void main(){
  var apple="apple";
  print(apple);
}

 

注意 

Dart语言是一门强类型语言,与JavaScript这种弱类型语言有明显的区别:对于Dart语言,在第一次赋值的时候,如果你已经将某个变量赋值为字符串类型,那么在之后的代码中就不能把这个变量更改为其他类型;而对于JavaScript,可以随意改变类型。这一点需要特别注意。但是假如你在使用Dart语言时,定义了一个变量后需要改变其类型,可以使用后文讲解的dynamic关键字。

 

Java语言定义常量时使用的关键字为final,Dart语言定义常量时也会用到final关键字,同时,也可以使用const关键字进行定义常量,如代码清单2-3所示。

代码清单2-3 常量的定义

void main(){
  final nums=300;
  const number=200;   
  print(nums);
  print(number);
}

那么,这里就有一个问题了,const与final的区别到底是什么?我们再来看一段代码就明白了,如代码清单2-4所示。

代码清单2-4 const与final的区别

const time='2020-05-03';
const time=DataTime.now();//这行代码在编译器中会报错
final time='2020-05-03';
final time=DataTime.now();//这行代码不会报错

从代码清单2-4来看,const与final的共同点是初始化后都无法更改;两者的区别是,const值在编译时会检查值,而final值在运行时才检查值。所以,不能给const定义的常量赋值为不确定的值。

Dart语言提供了一些基本的内置类型,具体如下:

(1)数值类型;

(2)布尔类型;

(3)字符串类型;

(4)列表类型;

(5)键值对类型;

(6)动态类型与Object;

(7)符号字符;

(8)符号。

在Dart语言中,数值类型包括整数类型(int)和浮点数类型(double)。需要特别注意的是,Dart语言中没有float类型。我们先来看一段代码,如代码清单2-5所示。

代码清单2-5 int与double的区别

//第一段代码
double a=300000000000;
a=1.2;
//第二段代码
int b=22222222222222;
b=11.1;//这行代码会报错

从上面的代码可以看出,int类型的数值是不能赋值为浮点数的;而double类型的数值可以赋值为整数,它会自动转换。这是因为int类型与double类型之间存在继承关系,前文已经说过,在Dart语言中,一切皆对象,如代码清单2-6所示。

代码清单2-6 int与double的继承关系

abstract class int extends num
abstract class double extends num

而且,在代码清单2-5中,我们把int b赋值为一个非常大的数,可以看出在Dart语言中,整数可以是任意大小的。不过,需要注意的是,使用非常大的整数是不可取的,因为整数一旦超出一定的范围,操作整数时就不能直接获得底层硬件提供的优势。

 

注意 

虽然在Dart语言中一切皆对象,但是不能定义int的子类型,也不能用另一个类型实现int。这些限制是一种妥协,即Dart语言以牺牲可以继承或实现任意类型为代价来实现高性能。

同样,double也不能定义其子类型。另外,如果两个浮点数的底层位模式所表示的浮点数相同,则它们相同。在大多数情况下,这意味着如果两个浮点数相等,则它们相同,但也有例外:

(1)浮点数−0.0与0.0相等但不相同;

(2)NaN(非数值)与自身不相等但相同。

 

布尔(bool)类型有两个值true和false,它们是布尔类型的成员。通常,我们将布尔类型用在if等判断语句之中。不过,使用布尔类型时有以下3点需要特别注意。

(1)在程序中使用bool表示布尔类型。

(2)布尔值是编译时常量。

(3)在Debug模式下,可以通过断言函数assert()判断布尔值。如果不为true,就会引发异常并终止程序。

在Dart语言中,布尔类型不像其他语言那样可以强制将表达式的值转化为布尔值,如代码清单2-7所示。

代码清单2-7 不要这样做

getName(){
  print('人民邮电出版社');
  return 123;
}
void main(){
  if(getName()){//这行代码会报错
    print('获取了返回值');
  }
}

我们用编译器运行这段代码,会发现错误提示,如图2-3所示。

图2-3

可以看到,在Dart语言中,即使是int类型也不能转换为布尔类型,开发人员要特别注意这一点。而且,Dart语言本身也不支持内置强制类型转换。如果要判断函数是否有返回值,可以像代码清单2-8这样判断。

代码清单2-8 判断函数是否有返回值

if(getName()!=null)print('获取了返回值');

同样,在Dart语言中,不能定义布尔类型的子类型,也不能用另一个类型实现布尔类型。

接着,我们来看看在Dart语言中,字符串能用哪些方式创建。首先来看代码清单2-9所示的代码。

代码清单2-9 创建字符串的方式

var press=" POSTS & TELECOM PRESS ";
var press2=' POSTS & TELECOM PRESS ';
var press3=''' POSTS & 
    TELECOM
    PRESS''';
var press4=" POSTS & TELECOM "
    "PRESS";
var press2=r' POSTS & TELECOM PRESS \n';

从上面的代码来看,有3种创建字符串的方式。

(1)使用单引号、双引号创建字符串。

(2)使用3个单引号或者双引号创建多行字符串。

(3)使用r创建原始字符串。

除了要掌握字符串的创建方式,还需要熟练地掌握其操作,我们先来看一下在Dart语言中操作字符串的几种方式。

(1)运算符操作:+、*、==、[]等。

(2)插值表达式:${name}。

(3)常用的字符串属性:length、isEmpty、isNotEmpty等。

(4)常用的方法:contains()、subString()、replaceAll()、split()、trimRight()、trimLeft()、trim()、toUpperCase()、toLowerCase()、lastIndexOf()、indexOf()、endsWith()、startsWith()等。

除了第(2)项,其他3项是不是都与Java语言的方法差不多呢?所以,运算符操作、字符串属性、方法、不用死记硬背,使用的时候可以查开发文档,而且IDEA也会有提示。下面我们来详细地看看这些常用操作的代码,如代码清单2-10所示。

代码清单2-10 字符串操作

var str1='hello';
var str2='world';
var str3=str1+' '+ str2;//字符串加操作
print(str3);
print(str3.toUpperCase());//全部转换为大写
print(str3.toLowerCase());//全部转换为小写
print(print(str3.startsWith('Hello'));)//是否包含子字符串
print(str3.length);//输出字符串长度
print(str3.split(' '));//按空格' '分割字符串,并转换为列表
print(str3.replaceAll(' ', '我是空格'));//将字符串替换

运行以上代码之后,输出效果如图2-4所示。

图2-4

 

注意 

我们需要注意字符串操作的要点:第一,字符串里单引号嵌套单引号,或者双引号嵌套双引号,必须加入反斜杠(\)进行转义;第二,推荐单引号嵌套双引号,或者双引号嵌套单引号,这样混合使用比较方便。

 

另外,在Dart语言中,字符串拼接也有许多方式,如代码清单2-11所示。

代码清单2-11 字符串拼接方式

var str='hello ' 'world';//用单引号拼接
var str1="hello " "world";//用双引号拼接
var str2="hello " 'world';//用单、双引号拼接
print(str);
print(str1);
print(str2);

无操作符的字符串拼接方式也是可行的,但是为了保证代码的可读性,建议还是使用加号或者通过多行的方式进行字符串拼接。

接下来,我们需要重点掌握Flutter开发中常用的字符串操作——插值表达式,使用过jQuery的前端开发人员对此应该并不陌生。插值表达式的使用方式如代码清单2-12所示。

代码清单2-12 字符串插值操作

var name='Li YuanJing';
print("My name is ${name}");

与Java一样,在Dart语言中,列表表示集合,不过它也表示数组,集合与数值在这里是同一个概念。创建列表的方式如代码清单2-13所示。

代码清单2-13 创建列表的方式

//通过直接赋值创建
var list=[1,2,3];
//通过new创建
var list=new List();
//创建一个不可变的列表
const list=const[1,2,3];

在实际的开发中,除了创建列表,还需要频繁地访问列表中的内容,这里可以通过索引进行访问,如list[1]。同样,也可以通过索引改变其内容,如list[1]=6。但如果改变const定义的列表,则肯定会报错,前文已经提到过。接下来,我们来看代码清单2-14所示的代码。

代码清单2-14 创建列表

var list=new List()
  ..add(1)
  ..add(2)
  ..add(3);

这段代码其实就是代码清单2-13中通过直接赋值创建列表的详细实现,不过严格来说,Dart创建列表完全不需要通过List类的工厂构造函数。但是,作为一个近似理解的概念,我们并没有理解错。

接下来,我们来了解列表中的常用方法,包括add()、length()、remove()、insert()、indexOf()、sublist()、forEach()、shuffle()等。

下面,我们通过一个例子来具体学习这些方法,如代码清单2-15所示。

代码清单2-15 使用列表的方法

var list=["apple","banana","cherry"];
print(list.length);//输出列表的长度
list.add("bayberry");//末尾添加"bayberry字符串
print(list);
list.remove("apple")//删除apple字符串
print(list);
list.insert(1, 'dates');//在1索引插入dates字符串
print(list);
print(list.indexOf("cherry"));//获取cherry字符串所在位置
print(list.sublist(2));//去除前两个元素后的新的列表
list.forEach(print);//遍历并输出列表
list.shuffle();//打乱列表顺序
print(list);

运行这段代码,输出效果如图2-5所示。

图2-5

在Dart语言中,映射(Map)同样表示键值(key-value)对,以键值对的形式存储值(value),键(key)和值都可以是任意类型的对象,但是每个key只能出现一次。

所有映射基本都是在花括号内的一系列用逗号分隔的键值对,在每个键值对中,先是键,然后是冒号,最后是值,即“key: value”,具体代码如代码清单2-16所示。

代码清单2-16 创建映射

Map map={'name':'liyuanjing', 'age':'27'};

与List一样,映射变量都实现了Map接口。我们可以把Map的{}看成new Map()的简写。例如,我们可以把代码清单2-16所示的代码详细地写成代码清单2-17所示的代码。

代码清单2-17 以new的方式创建映射

Map map =new Map()
    ..['name']='liyuanjing'
    ..['age']='27';

在上面两段代码中,我们都没有设置键值对详细的类型。因为Map类型有两个泛型类参数,所以当没有明确指定类型的时候,系统会自动地理解为Map<dynamic,dynamic>,而不是其他语言理解的Map<String,String>。

接着,我们来看看如何使用其方法进行简单的操作,如代码清单2-18所示。

代码清单2-18 操作映射数据

Map map=new Map();
map['name']='liyuanjing';
map['age']=27;
map.remove('name');//删除某个键为name的数据
print(map);
map.clear();//清空键值对
print(map);

在Dart语言中,一切皆对象,而这些对象的父类都是Object。

在上面的映射中,没有明确键值对类型时,编译器会自动根据值明确类型,如代码清单2-19所示。

代码清单2-19 3种赋值操作

var name1='liyuanjing';
Object name2='liyuanjing';
dynamic name3='liyuanjing';

上面这3种赋值操作都没有问题,编译器也不会报错,但是我不建议这么做。在实际的开发中,还是要尽量为变量确定一个类型,这样才能提高程序的安全性,也能加快程序的运行速度。但是如果你仍然要使用dynamic,那么它就会告诉编译器:“我们不用做类型检测,并且知道自己在做什么。”

在Dart语言中,虽然所有类型都继承自Object,但是如果此时调用了一个不存在的方法,那么系统会抛出NoSuchMethodError异常。

接着,我们来看另外两种赋值操作,如代码清单2-20所示。

代码清单2-20 另外两种赋值操作

dynamic name='liyuanjing';
obj['age']=27;

上面的代码在编译时也不会报错,但是实际运行时会抛出NoSuchMethodError异常。所以为了避免这种错误,开发人员进行这样的赋值操作时,应该用is或者as进行判断,如代码清单2-21所示。

代码清单2-21 is与as的使用方式

dynamic map={'name': 'liyuanjing', 'age':'27'};
if(map is Map<String,String>){
  print('类型与你想的一致');
}
var map2=map as Map<String,String>;

在上面的代码中,is是判断类型时使用的,as是转换类型时使用的,is!与is的功能刚好相反。一般来说,如果在is测试之后还有一些关于对象的表达式,可以把as当作is测试的一种简写形式。

符号字符是Dart语言提供的采用UTF-32编码的字符串,它可以通过这些编码直接转换成表情包与特定的文字。

前文介绍的字符串采用UTF-16编码,所以符号字符是一种特殊的字符串,有自己相对独立的声明方式。因为后续用到符号字符的地方比较少,所以我们就用官方的开发文档示例(见代码清单2-22)给读者展示其效果。

代码清单2-22 符号字符的代码实现

main() {
  var clapping = '\u{1f44f}';
  print(clapping);
  print(clapping.codeUnits);
  print(clapping.runes.toList());
  Runes input = new Runes(
        '\u2665  \u{1f605}  \u{1f60e}  \u{1f47b}  \u{1f596}  \u{1f44d}');
  print(new String.fromCharCodes(input));
}

运行这段代码,输出效果如图2-6所示。

图2-6

符号用来表示程序中声明的名称,使用#作为开头,后面跟着一个或多个用点分隔的符号或运算符,如代码清单2-23所示。

代码清单2-23 符号的代码实现

#MyClass #i #[] #com.example.liyuanjing

在实际的项目中,基本用不到这个内置类型。不过,就算用不到,了解一下总是没有坏处的。这里列出了几种运算符(包括非用户自定义运算符):

对于常用的运算符,这里不再赘述。但是我们需要了解Dart语言中特有的运算符。

在其他语言中也有三目运算符,它算比较常见的运算符。但是在Dart语言中,三目运算符有其独特的实现方法,而且这种实现方法在后面的Flutter开发中用得也是比较多的。三目运算符通常与状态管理结合使用,用于判断Flutter组件的状态,基本用法如代码清单2-24所示。

代码清单2-24 三目运算符1

var number=1;//改动这行代码,看看输出效果
var number2=2;
var isBool=number ?? number2;
print(isBool);

在Dart语言中使用这种简单的三目运算符非常方便,它通过??返回值。同样,Dart语言也支持常规的三目运算符,如代码清单2-25所示。

代码清单2-25 三目运算符2

int a = 200;
var b = a > 10 ? 1 : 2;
print(b);

“~/”是Dart语言中的取商运算符,其返回一个整数,具体的使用方式如代码清单2-26所示。

代码清单2-26 取商运算符

int c=20;
print(c~/11);//输出1

我们都知道,在大多数编程语言中,类的实例对象是无法通过“+”进行运算操作的,但在Dart语言中就不一样了,我们可以实例化两个对象,然后将它们相加,以代码清单2-27为例。

代码清单2-27 自定义类“+”操作符

class Point{
  var x,y;
  Point(this.x,this.y);
  operator +(p)=>new Point(x+p.x, y+p.y);//自定义类"+"操作符
  @override
  String toString() {
  // TODO: implement toString
     return "x="+x.toString()+";y="+y.toString();
  }
}
void main() {
  Point s_one=new Point(10, 10);
  Point s_two=new Point(20, 20);
  print(s_one+s_two);
}

可以看到,operator+( p)=>new Point(x+p.x, y+p.y)自定义了类“+”操作符,然后,我们就可以用操作符“+”完成两个点类对象的相加操作。你是不是觉得Dart语言非常不可思议呢?让我们来看看其输出结果,如图2-7所示。

图2-7

级联操作符非常类似于程序的链式调用,如果你熟悉JavaScript,那么你可能对级联操作并不陌生。在本书后续章节的Flutter项目中,也会经常用到级联操作符,可以通过“..”进行调用,使用方式如代码清单2-28所示。

代码清单2-28 级联操作符

String fruits=(new StringBuffer()
  ..write('apple')..write('banana')..write('apricot'))
    .toString();
print(fruits);

Java语言一般会创建各种实体类,这些实体类还提供了get和set方法,供开发人员进行操作。同样,Dart语言也支持get和set方法。

但是与Java不同的是,在Dart语言中,如果属性是公开的,那么,可以直接通过[类.属性]访问,或者通过[类.属性=某值]设置值,这样的调用方法其实就是默认调用了get与set方法,以代码清单2-29为例。

代码清单2-29 默认调用get与set方法

class Point{
  var x,y;
  Point(this.x,this.y);
  operator +(p)=>new Point(x+p.x, y+p.y);
  @override
  String toString() {
  // TODO: implement toString
     return "x="+x.toString()+";y="+y.toString();
  }
}
void main() {      
  Point s_one=new Point(10, 10);
  s_one.x=2000;
  s_one.y=4000;
  print(s_one.x);
  print(s_one.y);
}

如代码清单2-29所示,在Dart语言中,只要属性不是私有的,就可以直接使用get和set方法,而且不需要手动定义,也不用重写。但是如果用“_”将属性设为私有,例如上面的x、y,代码中设置为var _x,_y,那么默认的get与set方法就失效了,我们就需要手动设置方法来提供对应的get和set方法。

而且,由于在Flutter中禁止使用反射机制,因此在本书成书之时,还没有出现和Java中一样的get×××、set×××等通用方法名。不过,Dart语言的get和set方法名可以是任意的,但是还是建议按规范命名,以提高代码的可读性。

 

注意 

在Dart语言类的set和get方法中,不要调用自身类的方法,因为Dart语言有层级树的概念,递归调用会导致Stack Overflow(堆栈溢出)异常。

 

异常是指代码中意外发生的错误事件。在Dart语言中,可以抛出异常或者捕获异常,如果没有捕获异常,就会和其他编程语言一样,程序会终止。与Java相比,所有Dart异常都是没有被终止的,可以继续传递。一般情况下,所有方法都不会声明或者抛出异常,而需要手动捕获。

在Dart语言中,异常捕获与Java非常类似,但是其某些方面比Java还要强大。例如,在Dart语言中,可以抛出任何类型的异常,如代码清单2-30所示。

代码清单2-30 抛出异常

throw Exception('我是个异常');

在Dart语言中,任意对象都可以作为异常被抛出,并不要求被抛出的对象是某个特殊异常类的实例或子类。需要注意的是,在Dart语言中,throw是一个表达式,并不像在Java中那样是一个语句。上面代码的运行效果如图2-8所示。

图2-8

有时候需要在Dart语言中捕获异常,以确保程序的健壮性。

为了捕获异常,使用try语句。try语句由多个部分组成。首先是一条可能抛出异常的语句,然后是一个或者多个catch子句,以及一个finally子句,当然也可以省略这两种子句中的某一种。其中,catch子句为捕获的异常定义处理逻辑;finally子句用于定义异常捕获处理结束后需要做什么,不论异常是否发生,finally子句都会运行。异常结构体如代码清单2-31所示。

代码清单2-31 捕获异常结构体

try{
  ...(1)
}on String catch(e){
  ...(2)
}catch(e){
  ...(3)
}finally{
  ...(4)
}

在上面的结构体中,(1)中的代码就是捕获特定类型的异常,在(2)中,我们预料到可能会抛出一个字符串类型的异常,如果不是字符串类型的异常,则(3)将捕获任意可能出现的异常,(4)中不管有没有异常发生,只要有这个结构体,就会被运行。

 

注意 

虽然throw与try-catch语句是Dart语言中常用的异常处理方式,但其实还有一种异常处理方式——rethrow。捕获的异常经检查之后,如果我们发现本地不需要处理异常,并且异常应该在调用链中向上传播,那么rethrow就可以发挥作用了,它在catch子句中将捕获的异常重新抛出。不过rethrow基本用不到,了解一下就可以了。

 

Dart语言支持3种形式的循环,分别是for循环、while循环和do-while循环。

在Dart语言中,同时支持两种传统的for循环,分别为for循环和for-in循环。建议在开发中使用for-in循环,因为for-in循环避免了C语言以及一些低级语言中常见的差一错误,如代码清单2-32所示。

代码清单2-32 for-in循环的使用

for(int i in [1,2,3,4])print(i);
for(var i in [1,2,3,4])print(i);

从代码清单2-32可以看出,在for-in循环中,不管是声明int类型变量还是声明var实例变量,代码都可以运行,而且避免了普通for循环索引造成的循环越位。而且,除了列表,只要是Dart语言中实现了Iterable接口的类,都可以使用for-in循环进行遍历。

当然,在Dart语言中,依然可以使用经典的for循环,如代码清单2-33所示。

代码清单2-33 经典for循环的使用

for(int i=1;i<=100;i++)print(i);
for(var i=1;i<=100;i++)print(i);

代码清单2-33中两行代码的作用都是输出1~100的数字,只有当其中的表达式的判定结果为false时,for循环才会结束。

大多数编程语言中最常用到的一种循环是while循环,在Dart语言中也是一样的。while循环会判断一个条件,如果条件成立,就会运行循环语句;如果条件不成立,就会退出循环,如代码清单2-34所示。

代码清单2-34 while循环的使用

int i=1;
while(i<=100){
  print(i);
  i++;
}

同样,上面while循环的作用是输出1~100的数字,可以看出while循环是先判断条件,然后运行循环体。

do-while循环恰好跟while循环相反,它是先运行循环体,再判断条件是否成立,如代码清单2-35所示。

代码清单2-35 do-while循环的使用

int i=1;
do{
  print(i);
  i++;
}while(i<=100);

Dart语言中还会经常用到选择语句——switch。switch语句提供了根据表达式的不同取值从多个case子句中选择相应处理逻辑的方式。每个case子句中的值都对应一个switch语句中表达式的值。所以,使用switch语句必须预先知道其表达式的所有可能值,而且每个case子句的值都是编译时常量,它们的所有值的类型也是一致的。

例如,我们可以根据一个人的名字,输出他的职业,如代码清单2-36所示。

代码清单2-36 switch语句的用法

var grade='周杰伦';
switch(grade){
  case '周杰伦':
    print('歌手');
    break;
  case '屠呦呦':
    print('科学家');
    break;
  default:
    break;
}

在Dart语言中,一切皆对象,所以函数也是对象,并且函数的对象类型为Function,这一点与JavaScript类似。在JavaScript中,函数还可以作为参数传递;在Dart语言中也一样,函数能保存在变量中,也能作为参数和函数的返回值。

不管是Dart语言,还是Flutter项目,都具有一个顶层函数——main()函数,这一点与Java以及C语言等编程语言一样。main()函数是程序运行的入口,它的返回值类型为void,并且具有列表的可选参数。我们来对比一下Dart语言与Flutter项目的main()函数,如代码清单2-37所示。

代码清单2-37 main()函数

//Dart语言
void main(){
}
//Flutter项目
void main()=>runApp(MyApp());

可以看出,两者的main()函数基本一样。

顾名思义,可选参数就是可以不传入这些参数,也可以传入这些参数。代码清单2-38列出了一些可选参数的函数样式。

代码清单2-38 可选参数

void setUser({String name,int age}){
//...
}

使用上面的函数时,可以不传入参数,也可以只传入name或者age,或者两者都传入。在Dart语言中,通过{}声明可选参数。

有可选参数,就必然有必选参数。必选参数的函数定义有两种,如代码清单2-39所示。

代码清单2-39 必选参数

import 'package:meta/meta.dart';//必须引入这个包,才能使用修饰符@required
void setUser(String name,int age){
  //...
}

void setUser({@required String name,int age}){
//...
}
setUser(name: 'liyuanjinglyj');//调用第二个函数

必选参数就是必须要传入的参数,可以直接定义函数的参数,也可以通过修饰符@required进行修饰。

在函数的定义中,我们还可以使用[]定义可选位置参数,如代码清单2-40所示。

代码清单2-40 可选位置参数

void setUser({String name,int age,[String company]}){
  if(company!=null){
    print('这个人有公司备注,公司为:${company}');
  }
}

如果你使用过Python语言,估计会非常喜欢它的参数提示功能,以及它的默认参数。而本书之前的代码中,凡是用到{}的,都需要在调用函数时写入其参数键值对,如代码清单2-41所示。

代码清单2-41 调用可选参数函数

setUser(name: 'liyuanjinglyj');

这里的参数前面必带参数名,这样的提示功能一目了然。所以不管是不是必选参数,推荐读者开发Flutter项目时尽量使用{},遇到必选参数,可以额外使用@required修饰符。而对于默认参数,只需要给它一个默认值。例如,对前面的方法略微做一下改变,如代码清单2-42所示。

代码清单2-42 默认参数函数

void setUser({String name='liyuanjinglyj',int age=27}){
  //...
}
setUser();//调用函数

这样定义之后,在调用函数时就可以不填写任何参数,它会有默认值。

2.10节的开头就说明了,Dart语言中的函数可以作为参数传递,而且前文介绍列表时就用到过将函数作为参数进行传递的例子。这里以代码清单2-43为例。

代码清单2-43 列表遍历

List list=[1,2,3,4];
list.forEach(print);

其中的forEach()传入了一个print函数。除了传递print函数,也可以传递自定义函数,如代码清单2-44所示。

代码清单2-44 传递自定义函数

void printName(String name){
  print(name);
}

void main(){
  var fruits=['apple','banana','apricot'];
  fruits.forEach(printName);
}

代码清单2-43和代码清单2-44都是用来遍历列表的,但是代码清单2-44中把自定义函数作为参数传递给其他函数。

同样,函数也可以传递给一个变量,然后把这个变量作为函数来调用,如代码清单2-45所示。

代码清单2-45 函数作为变量

var night= (good){
  print(good+' night');
};
night('good');

这段代码将一个输出good night的函数赋值给一个变量,之后这个变量就可以作为函数使用。

Dart语言通过括号后面的参数列表调用函数。当没有使用{}时,可以直接通过几个参数来调用函数;当使用{}时,就需要给出参数的名称,以键值对的形式调用函数。但是,我们还是需要注意前文提到的get和set方法,这些方法提供了独立表征的宝贵财富。

2.4节讲解运算符时提到了级联操作,它是相对于对象进行的。但学习了函数之后,我们知道,函数其实也是对象,所以它也可以进行级联操作。

也就是说,当需要对一个对象执行一系列的操作时,级联是非常有用的。下面来看代码清单2-46所示的代码。

代码清单2-46 级联操作符与“.”操作符的区别

print('Hello'.length.toString());
print('Hello'..length.toString());

读者可以在编译器上运行一下上面两行代码,这里直接给出运行效果,如图2-9所示。

图2-9

可以看到运行第一行代码输出的是5,运行第二行代码输出的是Hello。可以看出,“.”操作符返回的是函数的返回值,而级联操作符就像普通的方法调用,只是它返回的值不是方法调用的返回值,而是当前对象。

前文一直都是顺序运行程序,但其实大多数Flutter项目都是并发的。而且Dart语言与JavaScript语言有一个共同点,它们都是单线程的,如果代码中直接出现同步代码,就会阻塞线程。因此,如果你在GitHub上查看开源Flutter项目,就会发现它们的程序中有大量异步操作。而在Dart语言中,异步操作是用Future对象来执行的,并且使用时还需要搭配async以及await关键字。

从字面意思来解释,Future表示未来或者将来,也就是说,它代表将来运算结果的对象,结果可能在未来某个时刻知道。

Future本身也是一个泛型对象,程序中大多不会单独使用Future,而是使用Future<T>,运算返回的结果对象就是T。如果返回结果不可用,或者没有返回结果,Future类型就会是Future<void>。

在Dart语言中,Future由dart:async库提供,如果返回Future函数,将会发生以下两件事情。

(1)这个函数加入待完成的队列并且返回一个未完成的Future对象。

(2)当这个操作结束时,Future会返回一个值或者返回错误。

当然,单独使用Future的情况非常少,往往需要搭配then()方法使用。then()方法接收一个onValue闭包作为参数,该闭包在Future成功完成时被调用。这里以代码清单2-47为例。

代码清单2-47 Future与then()方法的基本用法

Future<File> copy(File file);
s_file.copy(file)
  .then((f){//...})
  .catchError((error)=>{//...})
  .whenComplete()=>{//...};

代码清单2-47定义了一个用于文件复制的Future函数,当复制成功之后,copy()方法会返回一个真正的File对象,这个对象会传入then()方法的闭包中,其中参数f就是返回的File对象,如果复制失败会运行catchError()方法,无论复制文件的任务是成功还是失败,最后都会运行whenComplete()进行回调。这一点对有前端开发经验的开发人员来说应该很好理解,类似于前端的Promise。

如代码清单2-47所示,Future开始工作后,有成功处理、错误处理,以及后续的任务处理,任务相当繁重。为了减轻使用异步操作的工作量,Dart语言为异步函数提供了async关键字。函数体可以使用async进行操作,如代码清单2-48所示。

代码清单2-48 async用法

Future<int> printNum() async=>22;

使用async能多方面简化Future的异步任务。这里如果调用printNum()函数,函数并不会立即运行,而是会安排在将来的某个时间段运行。而async真正的价值在于与await搭配使用。

例如,当碰到比较耗时的任务时,可以通过async与await将任务放到延迟的运算队列,先处理不需要延迟的运算,再处理耗时的运算。注意,await必须搭配async使用,否则会报错。

我们先来看一个例子,假设不使用async与await,如代码清单2-49所示。

代码清单2-49 嵌套调用

task1('task1').then((task1Result){
  task2('task2').then((task2Result){
    task3().then((task3Result){
        //...task4
            //...task5
                //...task6
    });
  });
});

需要调用任务1(task1)、任务2(task2)、任务3(task3),这3个任务都是异步的,但是必须依次执行。这样会形成“回调地狱”,回调地狱这种代码出现得多了,代码会非常不美观,而且不易于理解。甚至当有100个任务时,可能会造成同一屏幕的代码由于层层缩进而显示不全。

那么async与await的优势就非常明显了,可以像代码清单2-50这样修改代码清单2-49所示的代码。

代码清单2-50 async和await

tasks() async{
  try{
    String task1Result=await task1('task1');
    String task2Result=await task2(task1Result);
    String task3Result=await task3(task2Result);
    //...task4
    //...task5
    //...task6
  }catch(e){
    print(e);
  }
}

从修改后的代码中可以发现,await必须被包裹在async里面,如果没有async,单独使用await肯定会报错。所以,这点需要明确记住,而且后续的项目中会大量用到Flutter异步网络请求。

在Dart语言中,如果简单地声明一个方法而不提供它的实现,这种方法被称为抽象方法。一个抽象方法本身属于一个抽象类,抽象类与Java语言一样都是通过abstract关键字进行声明的。下面我们来看一个例子,如代码清单2-51所示。

代码清单2-51 抽象类的定义

abstract class Point{
  get x;
  get y;
  void add();
}

在代码清单2-51中,有3个抽象方法,分别为get方法的x、y和add()方法。Point被显式地声明为抽象类。如果你在编译器中删除abstract关键字,那么编译器会发出警告。

接着,我们来实现这个抽象类,如代码清单2-52所示。

代码清单2-52 抽象类的实现

class xyzPoint extends Point{
  var z;
  @override
  void add() {
      print(x+y);
  }
  @override
  // TODO: implement x
  get x => throw UnimplementedError();

  @override
  // TODO: implement y
  get y => throw UnimplementedError();
}

 

注意 

在Dart语言中,抽象类同样不能被实例化,因为它缺失部分实现。对抽象类进行实例化,会产生abstractClassInstantiationError错误,Dart解析器也会发出警告。

 

在Dart语言中,每个类都隐含地定义了一个接口,此接口描述了类的实例具有哪些方法。不过,Dart语言虽然有接口,但没有接口声明。Dart语言的设计者在设计之初就觉得这是不必要的,因为我们始终可以定义一个抽象类来描述所需的接口,使用过Java的开发人员应该很清楚这一点。

先来看代码清单2-53。

代码清单2-53 接口的定义与操作

abstract class Point{
  get x;
  get y;
}
class xyzPoint implements Point{
  //...这里是x、y、z的实现代码
}

在代码清单2-53中,xyzPoint并不是Point的子类,它没有继承Point的任何方法与成员。implements的作用只是在接口中建立预期的关联,而不是共享实现。

换句话说,Dart语言并不关心对象是如何实现的,而只在意它支持哪些接口,这是Dart语言与其他有类似构造的语言的明显区别。前文讲解的is也是用来判断类与类的接口的。

 

注意 

如果类中的方法也是接口中的方法,那么实现这个接口的子类时都必须重写其方法,而且要加上@override关键字。

 

Dart语言和Java语言一样都是支持继承操作的。不过,在Flutter中的继承只能是单继承,当继承一个类之后,子类不仅可以通过@override关键字来重写父类的方法,还可以使用super来调用超类中的方法。不过,需要注意的一点是构造方法不能被继承。另外,Dart语言中也没有公有与私有的访问修饰符,所以子类可以访问超类中的所有方法与属性。

下面我们来实现一个既有继承又有接口的类,如代码清单2-54所示。

代码清单2-54 继承与接口的实现

abstract class Animal{
  void printName();
}

class Food{
  void printFood(){
      print(’food’);
  }
}

class Dog extends Animal implements Food{
  @override
  void printName() {
  // TODO: implement printName
  }
}

在代码清单2-54中可以看到,Dart语言的继承与Java语言的继承一样,必须实现其方法,不然会报错,而继承时不必重写其方法,当然,继承的方法依旧可以重写。

假如我们重写其继承的方法,它会优先调用子类的实现还是父类的实现呢?不妨来看代码清单2-55。

代码清单2-55 继承与接口的比较

class Dog extends Food implements Animal{
  @override
  void printFood() {
      print('bone');
  }
  @override
  void printName() {
      // TODO: implement printName
  }
}
void main() {
  Dog dog=new Dog();
  dog.printFood();
}

运行这段代码,其输出结果为bone,可见如果继承类没有重写其父类的方法,那么会调用其父类的实现;如果重写了其父类的方法,那么会调用自己重写的方法实现。

mixin是Dart语言独有的混入语法特性,它的出现是为了解决多继承问题。相信有Java经验的开发人员都或多或少开发过GUI程序,但是你或许曾经发现,假如有一个Widget包含很多个子Widget,那么它肯定会继承Collection集合,同时它是一个组件,所以它肯定也会继承Widget。这样就会导致同一个类继承多个父类,而这么做往往得不偿失。例如,我现在修改父类,就需要修改子类,这样会增加大量的维护、类型转换等工作。而Dart语言的mixin就是专门解决这种问题的,灵感来自Lisp语言。

说得更简单一点,mixin是一个可以把自己的方法提供给其他类,而不用成为其父类的类,它以非继承的方式来复用类中的方法。在Dart语言中使用mixin时需要用到关键字with,如代码清单2-56所示。

代码清单2-56 mixin示例

abstract class Animal{

  factory Animal._(){
    return null;
  }

  void printAnimalName(){
    print('我是一个动物');
  }
}

abstract class Food{
  factory Food._(){
    return null;
  }

  void pringFod(){
    print('我是一个食物');
  }
}

abstract class Fruits{
  void printFruitsName();
}

class apple extends Fruits{
  @override
  void printFruitsName() {
    // TODO: implement printFruitsName
  }

}

class Dog extends Fruits with Animal,Food{
  @override
  void printFruitsName() {
    // TODO: implement printFruitsName
  }
}

void main(){
  Dog()
    ..printAnimalName()
    ..pringFod();
}

如果你继承某个抽象类,那么你就必须重写其方法,而对于通过mixin混入的类,不必强制重写其方法,所以我们得出关于mixin的3个重要结论。

(1)mixin可以实现类似于多重继承的功能,但是实际上mixin和多重继承又不一样。多重继承中相同的函数运行并不会存在“父子”关系。

(2)mixin可以抽象和复用一系列特性。

(3)mixin实际上实现了一条继承链。

现在就出现了一个问题:假如同时使用接口继承mixin,并且它们的@override方法都一样,其优先级究竟会怎样呢?不妨再来看代码清单2-57。

代码清单2-57 接口、继承、mixin优先级测试

class Lion{
  void printName(){
    print('我是狮子');
  }
}

class Tiger{
  void printName(){
    print('我是老虎');
  }
}

class Leopard{
  void printName(){
    print('我是豹子');
  }
}

//分别取消注释测试AnimalOne、AnimalTwo
class AnimalOne extends Leopard with Lion,Tiger{
  //@override
  //void printName() {
      //print('我是动物1');
  //}
}
class AnimalTwo extends Leopard with Lion implements Tiger{
  //@override
  //void printName() {
      //print('我是动物2');
  //}
//}

void main(){
  AnimalOne()..printName();
  AnimalTwo()..printName();
}

运行这段代码,我们会发现优先输出的是类自身重写的方法,如果把类自身重写的方法注释后再运行,会发现其运行的顺序依次是mixin、extends、implements。运行结果如图2-10所示。

图2-10

接着,我们来学习Dart语言的泛型。在此之前,我介绍过Future<T>泛型示例,也讲解过List,其实List既是列表,也是泛型。例如,可以写成List<T>,这就是最明显的泛型写法。先来举一个简单的例子,如代码清单2-58所示。

代码清单2-58 List泛型示例

void main(){
  List animal=new List<String>();
  animal.addAll(['老虎','狮子','豹子','秃鹰']);
  animal.add(1234);
}

代码清单2-58定义了一个List<String>类型的泛型,这样在后续存入的时候,就必须存入字符串。但是读者可能看到了,这段代码最后添加了1234到animal泛型变量之中,这在其他语言之中肯定会报错,但是在Dart语言中不会,在Dart语言中只会在运行时报错。

那么,这里就有一个疑问:为什么要在程序中使用泛型?不妨先来看代码清单2-59。

代码清单2-59 不使用泛型

abstract class AnimalTiger{
  Object getName();
  void setName(String name);
}

abstract class AnimalLion{
  Object getName();
  void setName(String name);
}

可以看到,代码清单2-59定义了两个动物的抽象类,它们的方法都一样。但是这个时候有一个需求:需要写出所有动物的抽象类,并且它们的方法都和上面一样。难道要一行一行输入代码吗?这显然不现实,此时需要用泛型转换一下,如代码清单2-60所示。

代码清单2-60 使用泛型

abstract class Animal<T>{
  Object getName();
  void setName(String name);
}

这样,通过泛型就可以在只定义一个抽象类的情况下写出所有动物的抽象类,非常方便。而且,我们还可以通过泛型限制参数,如代码清单2-61所示。

代码清单2-61 通过泛型限制参数

class Animal{}
class Tiger extends Animal{}
class Lion extends Animal{}

class MyAnimal<T extends Animal>{

}

这样,我们就可以像使用List<T>泛型一样使用类泛型,也实现了限制其类型的行为。因此,在程序中使用泛型是非常便捷的。

Java语言通过import导入各种类型的开发包,而Dart语言将这些导入的开发包称为库,每段Dart程序都是由被称为库的模块化单元组成的。例如,2.2节输出的“Hello World”也可以被看成一个库。

毫无疑问,我们在使用库时,除了自己编写库程序,更多的是使用别人造好的“轮子”进行开发。和Java一样,Dart语言也通过import导入库,如代码清单2-62所示。

代码清单2-62 导入http库

import 'package:http/http.dart';

例如,要进行网络开发,就需要导入http库,但是http库可能会与其他库产生命名冲突,那么如何解决命名冲突呢?具体如代码清单2-63所示。

代码清单2-63 避免命名冲突

import 'package:http/http.dart' as htp;

从代码清单2-63可以看到,可以通过as重命名的方式来避免命名冲突。还有一种情况:现在导入了http库,但是想显示/隐藏其中的某些库函数或成员。具体如代码清单2-64所示。

代码清单2-64 显示/隐藏http中的库函数或成员

import 'package:http/http.dart' show http;
import 'package:http/http.dart' hide http;

可以通过show来显示某些库函数或成员,也可以通过hide来隐藏某些库函数或成员。

当然,如果我们导入的是自己编写的本地库,那么直接导入文件名即可,如代码清单2-65所示。

代码清单2-65 导入自己编写的本地库

import 'helloworld.dart';

这段代码就导入了最开始写的helloworld.dart文件。假如我把我的库上传到网络,而其他人不想下载我的库,只想直接使用网络上的库,该怎么办呢?我们也可以直接导入网络库,如代码清单2-66所示。

代码清单2-66 导入网络库

import 'http://helloword//hello.dart';

虽然Dart导入语句时可以使用任意URI,但是不建议在开发中这么做,因为只要库的位置发生变化,就会影响你的代码,这种URI导入方式适用于只求速度但不求完美的实验性任务。而真正严谨的代码,需要更多规范性,如代码清单2-62~代码清单2-65中的4种导入方式就比较好。

如果你尝试下载某些库到本地,并查看其代码,你可能会发现,某些库的顶部有一个library关键字,后面跟着一个库名字,如代码清单2-67所示。其实library就是用来定义这个库的名字的,但library定义库的名字并不影响导入,因为import用的字符串是URI。

代码清单2-67 library

library http;

有时候,一个库如果过于庞大,可能会需要把库拆分到多个文件中,而不是保存在一个文件中。这时就需要使用part进行库的拆分,如代码清单2-68所示。

代码清单2-68 part拆分库

library game;
part 'hundouluo.dart';
part 'hejindantou.dart';
part 'cikexintiao.dart';

每个part都指向了一个part所对应的URI,这些URI与导入语句遵循同样的规则,而且所有的part都共享同一个作用域,即导入它们的库的内部命名空间,而且包含所有导入。如果通过part进行拆分导入,那么part导入的单个模块的声明方式如代码清单2-69所示。

代码清单2-69 part文件头部声明

//hundouluo.dart文件开头
part of game;

1.Dart语言的核心是什么?它是面向什么编程的?

2.Dart语言使用什么关键字来声明变量?Dart语言使用什么关键字来声明常量?Dart语言中常量的两种声明方式有什么区别?

3.在Dart语言中,List是数组还是列表?

4.给定一个年份,自己创建一个世界杯键值对Map(key:年份,value:举办地点),通过forEach循环判断该年份是否举办了世界杯。如果举办了,那么输出举办世界杯的地点;如果没有举办,那么输出“该年份没有举办世界杯”。

5.使用try-catch捕获一个异常,并在捕获异常结束后输出“捕获完成”。

6.创建一个可选变量的函数。

7.给定一个水果字符串,通过switch语句判断水果的类型并输出。

8.详细说明Dart语言接口、继承与mixin的区别,并指出其优先级顺序。

9.定义一个List<dynamic>泛型,并添加多种类型的数据,然后使用for循环(非for-in循环)输出所有数据。

10.详细说明为什么习题9中定义的List<dynamic>泛型可以添加多种类型的数据,并且运行时不会报错。

11.在一个dart文件中定义一个加法函数,通过库导入;在另一个dart文件中,使用这个加法函数。

12.假如现在有两个Person对象,它们的成员有name(名字)和age(年龄)。你需要在程序中直接通过“+”运算符算出这两个Person对象中的年龄和,并生成一个新的Person对象,然后通过toString()输出它们的年龄和。(提示:使用自定义类“+”操作符以及toString()方法。)


相关图书

树莓派开发实战(第3版)
树莓派开发实战(第3版)
React Native移动开发实战 第3版
React Native移动开发实战 第3版
深入浅出React Native
深入浅出React Native
React Native移动开发实战 第2版
React Native移动开发实战 第2版
App自动化测试与框架实战
App自动化测试与框架实战
30天App开发从0到1:APICloud移动开发实战
30天App开发从0到1:APICloud移动开发实战

相关文章

相关课程