从前面章节的介绍可以看出,Dart语言的基本语法、面向对象程序设计思想继承了C、 Java、JavaScript及其他语言相同的语句和表达式语法。但是Dart语言作为一个基于编译器 的优化编程语言,主要用于为不同的环境(如Android、iOS、Web和Desktop)开发应用程序, 它也支持泛型、Future、async和await等高级编程技术。 5.1 泛型 泛型是程序设计语言的一种特性,它允许程序员在强类型程序设计语言中编写代码时并 不在类型定义部分直接指出明确的类型,而是用E(Element)、T(Type)、V(Value)或K(Key) 等单字母表示,只有在使用前才明确指定类型。相当于将类型参数化,从而既提供了编译时类 型安全检测机制,又提高了代码复用率和软件开发效率。 Rec0501_01 5.1.1 泛型的定义 泛型即泛类型,也就是类型并不需要在声明时决定,而是延迟到使用时决 定。例如,Dart语言的API文档中用“List”表示List类型,其中的 就是表示List是一个泛型类型。所以在实际使用时,可以使用如下代码明确List中数 组元素的数据类型。 1 Listlist =List(); 2 list.add('abc'); 3 list.add(1); //报错 上述第1行代码明确List数组中只能存放String类型的数据元素。第2行代码将“abc” 的值添加到list中,而第3行代码执行时会报错,因为1是int类型,而不是String类型。 从上述代码可以看出,虽然List数组中的元素值类型是可选的,但是编写代码时程序员 也可以选择不指定类型,这样List数组中的元素值就可以是不同的数据类型。如果程序员希 望编写代码时清晰地表明预期类型,则可以使用第1行代码的格式传入具体的类型参数。也 就是可以把泛型看作是类型的变量,而为泛型类型指定具体类型时,就类似给变量赋值。 例如,定义一个可以表示任何类型信息的类,该类中包含1个成员属性、1个构造方法和1 个自定义方法。实现代码如下。 1 class Info{ 2 T value; 3 Info(this.value); 72 4 void showPrint() { 5 print("你目前输入的$value 是$T 类型"); 6 } 7 } 上述第1行表示定义一个泛型类,该类的成员属性value的类型也指定为T,并且 第5行代码用“$T”引用T的值。下面分别指定T为String类型和int类型,实现代码如下。 1 Info StringInfo =Info('nixiaoniu'); //泛型指定数据类型为String 2 StringInfo.showPrint(); 3 Info intInfo =Info(200); //泛型指定数据类型为int 4 intInfo.showPrint(); 从上述第1行和第3行代码可以看出,使用泛型可以指定类中的成员属性类型为String、 int等多种不同的类型。其实,泛型也可以减少重复代码,它允许程序员在许多类型之间共享 一个接口和实现。 Rec0501_02 5.1.2 泛型的使用 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参 数。参数化类型的目的是告诉编译器要处理实例的类型,从而在处理其他类 型时做出提示,并保证编译时的类型安全。参数化类型可以在类、接口和方法的创建中表现, 分别称为泛型类、泛型接口和泛型方法。 1 泛型类 泛型类和普通类的区别就是类名后有类型参数声明,声明类型参数可以有一个或多个,多 个参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称 的标识符,一般用E、T、V 或K等字母表示。在类名中声明参数类型后,内部成员、方法就可 以使用声明后的参数类型。例如,前面示例的Info就是一个泛型类,它在类名后声明了 一个类型参数T,它的成员属性value就可以使用T表示成员类型。 2 泛型接口 泛型接口和泛型类一样,泛型接口在接口名后添加类型参数,接口声明类型后,接口的成 员属性和方法就可以直接使用这个类型。 例如,定义一个可以操作MySQL、SQLServer和MongoDB的接口OperateDB,实现步骤 如下。 (1)定义数据库类。 为了操作MySQL、SQLServer和MongoDB等不同类型的数据库,可以用下列代码定义 MySQL、SQLServer和MongoDB数据库。 1 class MySQL { 2 //定义MySQL 数据库类 3 } 4 class MsSQL { 5 //定义SQL Server 数据库类 6 } 7 3 7 class MoSQL { 8 //定义MongoDB 数据库类 9 } (2)定义操作数据库的接口。 由于操作的数据库可能为MySQL、SQLServer或MongoDB,所以可以用泛型定义一个 可以操作不同数据库的接口,实现代码如下。 1 abstract class OperateDB{ 2 T currentRecord; //当前记录 3 T getRecord(int index); //取指定记录 4 bool insertRecord(int index, T mySQL); //插入指定记录 5 } 上述代码中的currentRecord为成员属性,getRecord()和insertRecord()为成员方法,接 口声明类型后的T可以直接用于成员属性和方法。 (3)实现OperateDB接口。 只有具体实现OperateDB接口后,才可以具体操作指定的数据库。实现代码如下。 1 class DBimplements OperateDB{ 2 DB() { 3 print('正在操作的数据库为:$T'); 4 } 5 @override 6 T currentRecord; 7 @override 8 T getRecord(int index) { 9 //方法体 10 } 11 @override 12 bool insertRecord(int index, T mySQL) { 13 //方法体 14 } 15 } 实现OperateDB 接口时需要覆写接口中的所有属性和方法,上述第2~4行定义实现 OperateDB接口的DB类的构造方法;第5~6行代码覆写currentRecord属性;第7~10行和 11~14行代码分别覆写getRecord()和insertRecord()方法。 (4)实例化对象操作MySQL、SQLServer和MongoDB数据库。 只有实例化MySQL、SQLServer或MongoDB数据库对象后,才能具体操作MySQL、 SQLServer或MongoDB数据库,实现代码如下。 1 DB dbMySQL =DB(); 2 DB dbMsSQL =DB(); 3 DB dbMoSQL =DB(); 74 上述第1行代码实例化操作MySQL数据库对象;第2行代码实例化操作SQLServer数 据库对象;第3行代码实例化操作MongoDB数据库对象。代码运行结果如图5.1所示。 图5.1 泛型接口操作数据库 泛型接口比较实用的使用场景就是用作策略模式的公共策略。上面案例的OperateDB 就是一个泛型接口,MySQL、SQLServer和MongoDB的数据库操作都是按照OperateDB接 口约束实现的。 3 泛型方法 泛型方法是指使用泛型的方法,调用该方法时可以接收不同类型的参数。根据传递给泛 型方法的参数类型,编译器适当地处理每一个方法调用。泛型方法可以约束一个方法使用同 类型的参数、返回同类型的值,可以约束里面的变量类型。泛型方法可以定义在泛型类中,也 可以定义在普通类中。 例如,定义一个可以显示数组元素的泛型方法,并且数组元素可以是任何类型的数据。实 现代码如下。 1 class PrintList { 2 void show(Listname) { 3 name.forEach((value) { 4 print(value); 5 }); 6 } 7 } 上述第2行代码定义的show()为泛型方法,按如下代码格式实例化不同数组元素类型的 对象,就可以调用show()方法输出数组元素。 1 PrintList printList =PrintList(); 2 Listlist1 =[1,2,3,4,5]; 3 Listlist2 =["中国","美国","日本","朝鲜","德国"]; 4 Listlist3 =[{'first': '阿里巴巴'} , { 'second': '腾讯', 'fifth': '百度'}, { 'fifth': '百度'}]; 5 printList.show(list1); 6 printList.show(list2); 7 printList.show(list3); 上述第1~3行代码分别定义了存放int、String和Map类型数组元素的List;第4行代码 表示实例化printList对象;第5~7行代码分别调用printList对象的show()方法输出int、 String和Map类型的List数据。 7 5 5.2 异步 异步(Asynchronous,async)与同步(Synchronous,sync)是相对的概念。在传统的单线程 编程中,程序的运行都是同步的,程序按照连续的顺序依次执行,即只有前面的事务处理完毕 后,后面的事务才会继续执行。而异步指的是后一个事务并不一定需要前一个事务处理完毕 就可以继续执行,异步一般需要在多线程编程中实现。Dart语言是单线程编程语言,它没有 主线程和子线程,但是Dart语言类库中有很多返回Future或Stream 类型对象的异步方法支 持异步编程,从而避免程序在执行网络请求、文件读写等耗时操作时阻塞线程,导致程序不能 完成任务。 Rec0502_01 5.2.1 Future Future表示在将来某时获取一个值的方式。当一个返回Future的方法 被调用时,该方法会把要执行的某个事件放入队列,并返回一个未完成的 Future对象。在该事件执行完毕后,Future对象的状态会自动变成已经完成,此时可以通过 then链式调用或async和await获取该事件的返回值,并对返回值进行相应的处理。 1 异步读文件 Dart语言提供了异步读文件机制,即调用File类的异步读文件方法readAsString()读文 件时,并不会阻塞程序代码其他功能模块的执行。 例如,定义1个从指定文件异步读出文件内容的方法,并输出文件内容。实现步骤如下。 (1)定义readFile()方法。 自定义的readFile()方法用于从指定位置异步读出文件内容。实现代码如下。 1 Future readFile(String filePath) { 2 File file =File(filePath); //创建File 文件对象 3 return file.readAsString(); 4 } 上述第3行代码中readAsString()方法的返回值是Future类型。Dart语言通过File对 象的readAsString()方法异步读取文件,该方法返回一个Future对象,表明该操作 返回的是一个未来的对象,而Future有一个then方法,该方法的原型代码如下。 1 Futurethen( 2 FutureOronValue( 3 T value 4 ), { 5 Function onError} 6 ); then()方法用来注册将来完成时要调用的回调方法,该方法有以下两个参数。 ① Callback(成功返回的回调方法):因为调用then()方法的对象是Future类型,该 Future类型对象是一个潜藏的value(正常返回值)或者error(返回错误),如果Future类型的 对象成功完成,则会执行onValue这个Callback。 76 ② onError(失败返回的回调方法):它是一个可选的命名Function参数,这个方法只有 在Future返回失败的时候才会被执行。onError也包含两个参数,第一个是Exception,第二 个是可选参数StackTrace。 (2)定义main()方法。 调用readFile()方法读出temp/info.txt文件内容的实现代码如下。 1 void main() { 2 print("start"); 3 Future info =readFile("temp/info.txt"); 4 info.then( 5 (value) { 6 print(value);} 7 ); 8 print('end'); 9 } 上述第4~7行代码用then链式调用获取Future对象的返回值value,并输出value值, 即读出的文件内容。图5.2所示为程序的运行结果。从中可以看出,当执行到第3行代码时, 调用的readFile()方法不是阻塞的,而是把自己放入队列,不会暂停程序代码的执行,程序继 续执行第8行代码;当readFile()方法读完文件后,执行第4~7行代码并输出文件内容。其 实,虽然readFile()方法需要消耗一定时间执行完成,但通过Future的异步处理机制,当它在 读文件时,并不会影响程序的其他代码执行,即在读完文件之前可以正常处理其他事务,这也 就是图5.2所示输出结果中先输出第8行代码执行结果的原因。如果需要在读完文件后才能 执行第8行代码,可以将第8行代码放到第7行代码之前,这样输出图5.3所示的结果。 图5.2 Future读文件(1) 图5.3 Future读文件(2) 当然,在进行访问网络、读写文件这些耗时操作时,也可能发生网络连接不上、要读写的文 件不存在等问题,也就是调用then()方法的Future返回值发生错误,此时可以使用如下代码 处理。 1 void main() { 2 print("start"); 7 7 3 Future info =readFile("temp/info.txt"); 4 info.then( 5 (value) { 6 print(value); 7 print('end');}, 8 onError: (e) { 9 print(e);} 10 ); 11 } 上述第8~9行代码定义了一个Future对象失败返回的回调方法onError,并指定了出错 异常参数Exception,一旦出错,输出异常信息。 2 同步读文件 Dart语言同样也提供了同步读文件机制,即调用File类的同步读文件方法readAsStringSync()。 用readAsStringSync()方法读文件时,需要等文件读完后才能执行后面的功能模块。 例如:定义1个从指定文件同步读出文件内容的方法,并输出文件内容。实现步骤如下。 (1)定义readFileSync()方法。 自定义的readFileSync()方法用于从指定位置同步读出文件内容。实现代码如下。 1 String readFileSync(String filePath) { 2 File file =File(filePath); 3 return file.readAsStringSync(); 4 } 如果readAsStringSync()方法同步读文件成功,则该方法的返回值为String类型。 (2)定义main()方法。 调用readFileSync()方法读出temp/info.txt文件内容。实现代码如下。 1 void main() { 2 print('start'); 3 String info =readFileSync("temp/info.txt"); 4 print(info); //输出读出的文件内容 5 print('end'); 6 } 上述代码执行后的运行效果如图5.3所示,即必须等第3行代码执行完成后,才能执行后 Rec0502_02 续第4行、第5行及后面的代码。 5.2.2 async和await Futurn处理异步任务时,需要注册回调方法才能处理异步任务和返回的 结果,这种程序的可读性比较差,尤其是多层回调方法层层嵌套的时候。为了解决这个问题, Dart语言提供了await和async机制,让异步任务的执行跟同步代码的执行顺序一致。 例如,用async和await机制实现异步读文件,并产生图5.3所示的输出效果。main()方 法的代码修改如下。 78 1 void main() async { 2 print('start'); 3 String info =await readFile("temp/info.txt"); 4 print(info); 5 print('end'); 6 } 上述第1行代码用async标记main()方法是一个异步方法;第3行代码用await表示后 面语句返回的是Future对象,并等待Future异步执行任务的结果,直到执行任务结束获得返 回结果后,才继续执行后面的程序代码。 async用来表示定义的方法是异步执行的,该方法会返回一个Future对象;await后面也 是一个Future对象,表示等待该异步任务完成。只有异步任务执行完成后才会继续执行后面 的任务代码。简单地说,async的作用就是标记一个方法是异步方法;await的作用就是等待 异步任务的结果。综上所述,使用async和await实现代码的异步执行机制包含以下4个 要点。① await只能在标记了async的异步方法中使用,否则报错。 ② 当使用async作为方法名后缀声明时,说明这个方法的返回值是一个Future类型。 ③ 当执行到该方法中用await标注的代码时,会暂停该方法其他部分的代码执行。 ④ 当await标注的代码引用的Future类型返回值执行完成,await标注的代码后的下一 行代码会立即执行。 Rec0502_03 5.2.3 Stream Stream 和Future是Dart语言实现异步处理机制的核心API。用Future 实现异步处理时,所有异步操作的返回值都有Future标注,但是Future只能 表示一次异步获得的数据。而Stream 实现异步事件流的处理表示多次异步获得的数据。比 如,用前面介绍的File类的readAsString()方法实现异步读和readAsStringSync()方法实现 同步读,都是一次性把整个文件的内容读取出来,如果文件很大,就会导致内存占用太大而影 响程序性能等问题。而采用Stream 方式读文件内容时,一般情况下每次可以读取一部分数 据,并进行相应的处理。 例如,定义1个用Stream 方式从指定文件读出文件内容的方法,并输出文件内容,实现 步骤如下。 (1)定义getContent()方法。 自定义getContent()方法用于从指定位置读出文件内容。实现代码如下。 1 Stream getContent(String filePath) { 2 File file =File(filePath); 3 return file.openRead( ); 4 } 上述第3行代码的openRead()方法返回一个Stream>类型的数据,也就是 一个存放了int类型数组的数据流。获得一个Stream 实例对象后,就是通过listen()方法订 阅Stream 上发出的数据(即事件),有事件发出时,就会通知订阅者进行相应的事件处理。 listen()方法的原型代码如下。 7 9 1 StreamSubscriptionlisten( 2 void onData(T event), 3 { Function onError, 4 void onDone(), 5 bool cancelOnError } 6 ); listen()方法一共有4个参数,其中有1个必选参数和3个可选参数。onData用于处理收 到数据时的回调方法,它是必选参数;onError用于处理遇到错误时的回调方法;onDone用于 处理结束时的通知回调方法;cancelOnError用于处理遇到第一个Error时是否取消监听,默 认为false,即不取消监听。 (2)定义main()方法。 调用getContent()方法读出temp/info.txt文件内容的实现代码如下。 1 void main() { 2 print('begin'); 3 Stream stream =getContent("temp/info.txt"); 4 stream.listen( 5 (value) { 6 print(value); }, 7 onError: (e) { 8 print(e);}, 9 onDone: () { 10 print('end'); } 11 ); 12 } 上述第4~11行代码表示订阅stream 事件流,其中第4~5行表示处理收到数据时的操 作,由于stream 流中存放的是int类型的数组,所以会将info.txt中的内容转换为ASCII值的 数组元素后输出,运行结果如图5.4所示;第7~8行表示处理遇到错误时的操作;第9~10行 表示处理结束时的操作。 图5.4 Stream 读文件 任何一个应用程序都需要有一个美观、易用的用户界面,才能吸引更多的用户使用和推 广。基于Fluter框架开发的应用程序用户界面都是由一个或多个Widget元素组合而成的。 在Fluter开发中,这些组成用户界面的Widget元素既可以理解为原生应用程序开发中的用 户界面组件(UIComponent),也可以理解为原生应用程序开发中的用户界面布局(UI Layout)。本章结合具体的应用案例介绍FluterSDK提供的基本界面组件的使用方法和应 用场景。 6.1 概述 进行移动应用开发时,严格的设计规范和个性化的设计风格,从某种程度上会影响产品的 用户流量和体验效果。谷歌自2011年开始重视产品的设计,2014年在I/O大会上推出了一 种视觉设计语言———MaterialDesign(原质化设计),它既遵循优秀设计的经典原则,也结合创 新理念和新技术。同时宣布旗下的电脑、可穿戴设备、电视等设备都可以使用MaterialDesign 作为视觉规范,甚至还鼓励开发者在iOS平台上也使用它。MaterialDesign并不是简单的扁 平设计,而是一种注重卡片式设计、纸张的模拟,使用强烈对比色彩的设计风格。它的目标包 括以下3点。 (1)创造性(Create)。创造一种视觉语言,将经典的优秀设计原则与技术和科学的创新 和可能性相结合。 (2)统一性(Unify)。开发一个单一的底层系统,让用户在不同的平台、设备和输入方法 之间具有统一的用户体验效果。 (3)定制性(Customize)。在统一规范的基础上突出设计者自己产品的个性化效果和品 牌特征。 6.1 tralApp 1.Maei MaterialApp是Fluter开发中最常用的符合MaterialDesign设计理念 的入口Widget,它封装了应用程序,实现MaterialDesign需要的一些基本Rec0601_01 Widget。下面介绍MaterialApp的常用属性。 1 home home属性用于指定进入应用程序后显示的第一个页面,即用来定义打开当前应用程序 时显示的界面。该属性值为Widget类型。例如, 显示图6. 下列代码运行后,1所示的界面。