likes
comments
collection
share

Flutter入门概览2-Dart篇

作者站长头像
站长
· 阅读数 26

本系列文章共有三篇,计划分别从Flutter、Dart、工具三个方面介绍,快速形成对Flutter开发的一个综合概念。

本文主要介绍Dart相关知识。

Flutter语言使用 Dart,开发IDE使用VSCode、Android Studio 或 IntelliJ IDEA,个人推荐使用VSCode,因为前端无缝切换。

语言概要

Dart语言有点像C语言和JS语言。按照官网的说法,Dart继承了C、Java、JS以及其他语言相同的语句和表达式语法,让开发者感到亲切。

Dart是强类型语言,在开发时会按照JIT模式,编译成App后时会按照AOT模式。

// dart函数,跟c一模一样
void main() {
  print('Hello, World!');
}

基本语法基本看一个官方的 Dart语言概览 就差不多,后面边写边看。

通过DartPod可以在线运行Dart代码。

⚠️注意:Dart中所有数据结构都是对象。

语言基础

变量

dart-变量

  • var,变量,声明一个变量,变量类型需要能够自动推导出
  • 类型,类型变量,例如num, list, String,当变量不能自动推导出,一般显式给出类型
  • const,常量,编译时常量编译期间就确定,不能再改变
  • final,终值,运行时常量,一旦创建不能再改变,用于运行时动态创建的常量,只能赋值一次,多用于构造函数中
  • late,声明一个延迟初始化的非空变量,该变量在声明时未初始化,在使用时需要初始化。一般用于显式声明一个非空变量但不初始化(不用late不能通过静态检查)和延迟初始化变量。
  • Object,Dart中任何类型都是Object的子类型,因此可以用Object表示所有类型。
  • dynamic,类似TS中的any,可以表示任何类型,dynamicObject声明的变量,类型可以更改,运行时才会确定。dynamic一般用在解析JSON字符串的场景。Object一般用在需要使用多态特性实现基类和子类中方法的调用。另外,dynamic可以表示null,Object需要使用 Object?表示可以为空。dynamic还有一个作用,编译阶段允许对其进行任何值的访问而编译不报错,但是运行时如果没有则会报错,这块详看下面示例说明。
  • ?,空修饰符,允许变量为空,Dart语言为空安全方式,正常变量不允许为空,如果可能为空必须使用?声明,跟在修饰符后面表示变量可为空
// var,声明name变量,该变量会自动推导出String,且该变量可修改
var name = 'Bob';

// 类型,显式声明name变量为String类型,效果同var,变量可修改
String name = 'Bob';

// const 常量,声明后不能在改变
const bar = 1000000;
const int bar = 1000000;  // 可加类型说明

// final 终值,赋值后不能再改变
final name = 'Bob'; // Without a type annotation
final String nickname = 'Bobby';  // 可加类型说明

//late
// 声明一个非空变量,但不在声明时初始化,不使用late无法通过静态检查
late String description;
// 延迟初始化
late String temperature = readThermometer(); // 使用时才会调用 readThermometer 函数


// ? ,空修饰符
String? name = null;  // 此时name可以为空,类型为 String or null。
// error, var? name = null; // 不能使用 var? ,因为无法推断出类型



// Object,可以表示任何类型,类型可以改变
Object name = "123";
name = 123;
name = true;
print(name);

// 使用 Object? 允许为空
Object? name = "123";
name = null;
print(name);

// 使用dynamic 表示任何类型,且可以表示null
dynamic name = "123";
name = 123;
name = null;
print(name);

// 编译阶段允许对dynamic进行任何值的访问而编译不报错
// 可用于一些比较灵活的场景,但是需要自己确保运行时不报错
dynamic name = "123";
print(name.a); // 编译阶段是可以通过的,但是运行时会报错。


// dynamic 主要还是用于 JSON类型转换
Map<String, dynamic>

运算符

Operators

操作符特别值得一说的主要是空合运算符和级联运算符:

  • ??,空合运算符(空值合并运算符)
    // 当name != null时,使用name,否则选取 'unkown'
    var test = name ?? 'unknown';
    
    // ??=,当b为空时则赋值为value,否则继续使用b
    b ??= value; // 等同于 b = b ?? value;
    
    
  • ....?,级联运算符(cascade),一种简化写法,可以在同一对象上执行一系列操作
    // 使用级联运算符可以在之前对象上继续操作
    var person = Person()
      ..name = "Tom"
      ..age = 18;
      
    // 等同于
    var person = Person();
    person.name = "Tom";
    person.age = 18;
        
    

其他:

  • 类型运算符:
    • as,类型假定,用于编译通过,不会做类型转换,用于静态检查(如果对象不是假定的类型,运行时会报错)
    • is,判断是否是某个类型
    • is!,判断是否不是某个类型,与is正好相反
    // as,假定 employee 为 Person
    (employee as Person).firstName = 'Bob';
    
    
    // is,判断是否是某类型
    if (employee is Person) {
      // Type check
      employee.firstName = 'Bob';
    }
    
    // is!,判断是否不是某类型,与is正好相反
    if (employee is! Person) {
      // Type check
      employee.firstName = 'Bob';
    }
    
    
  • 条件访问运算符:
    • ?.,对象访问属性,如果对象为空则返回null,不会触发错误
    • ?[],数组访问元素
    // ?.
    foo?.bar  // foo为空时整个表达式返回null,不会报错;不为空时正常赋值
    
    // ?[]
    list?[0]  // 当数组list为空时不会报错
    
    
  • !,将表达式视为不为空类型(运行时如果为空则会报错)
    // 把foo视为非空对象,如果为空则运行时会报错
    foo!.bar
    

注释

  • //,单行注释
  • /* */,多行注释
  • ///,文档注释,放在文件顶部用于文档说明
    /// A domesticated South American camelid (Lama glama).
    ///
    /// Andean cultures have used llamas as meat and pack
    /// animals since pre-Hispanic times.
    ///
    /// Just like any other animal, llamas need to eat,
    /// so don't forget to [feed] them some [Food].
    class Llama {
    }
    
    注:[Food] 表示一个类跳转,当使用dart doc生成文档时,会自动变成该类的点击跳转
    
    

注解

Metadata

  • @deprecated,标记废弃,无任何说明文案
  • @Deprecated,标记废弃,可以指定一条说明文案,推荐总是使用这个
  • @override,重写父类方法,方法名不正确时编译器会提示,避免意外创建新方法,
  • @pragma,编译优化相关
  • 自定义注解,定义一个class就可以当注解
    
    
    class Television extends TV {
      // 标记废弃,但是没有说明文案
      @deprecated
      void turnOn() {
      }
      
      // 标记废弃,可以带一条说明文案
      @Deprecated('Use turnOn instead')
      void activate() {
        turnOn();
      }
      
      // 重写父类方法
      @override
      void tvName() {
      }
    }
    
    
    
    
    // 自定义注解,
    // 1. 建立一个class
    class Todo {
      final String who;
      final String what;
    
      const Todo(this.who, this.what);
    }
    
    // 2. 使用@className标注,参数即为class的参数 
    @Todo('Dash', 'Implement this function')
    void doSomething() {
      print('Do something');
    }
    
    

库导入

Libraries & imports

  • import,导入库或文件。
  • dart:,导入内置库,使用 import 'dart:io';
  • package:,导入三方库,示例:import 'package:test/test.dart';
  • as,给库或文件指定别名,示例:import 'package:lib2/lib2.dart' as lib2;
  • show,只导出指定的类、方法、变量
  • hide,与show相反,导出除了hide指定以外的所有
  • library,放在库根文件的顶部,用于声明库
// dart: 引入dart核心库
import 'dart:io';

// package: 引入三方库
import 'package:test/test.dart';


// as,使用as指定别名,当有重名冲突时可以比较方便
import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;


// show,指定导出类、函数、变量
import 'package:lib1/lib1.dart' show foo;  


// hide,指定 不导出的 类、函数、变量
import 'package:lib2/lib2.dart' hide foo;

// 在库的根文件顶部声明,一般用不上
library my_cli;

数据类型

在Dart中所有类型都是Object,数字、布尔、字符串、对象、函数等等都是Object的子类。

Dart支持以下10种数据类型:

  1. Numbers,numintdouble,数字类型
  2. Strings,String,字符串
  3. Booleans,bool,布尔
  4. Lists,List,有序列表
  5. Sets,Set,无序集合
  6. Maps,Map,映射
  7. Records,Record,记录类型,Dart中的元组概念(Dart3.0引入)
  8. null,Null,空类型
  9. Symbols,Symbol,标识符,类似JS中的Symbol
  10. Runes,符文类型,处理Unicode符号

代码风格

包管理工具Pub

Pub是Dart的包管理工具,使用pubspec.yaml 作为依赖项管理,pub.dev是包站点。同web的npm 或者iOS的cocoapods。

// 安装包依赖
dart pub get

// 添加transmogrify包
dart pub add transmogrify

// 升级包
dart pub upgrade transmogrify

Dart SDK安装

安装、更新Dart SDK,具体参考:获取 Dart SDK

// 使用brew 安装 dart
brew tap dart-lang/dart
brew install dart

// 更新dart到最新版本
brew upgrade dart

// 切换本地Dart SDK版本
// 1. 先安装对应Dart SDK版本
brew install dart@2.12
// 2. 切换版本,取消当前版本链接,链接目标版本
brew unlink dart@<old> && brew unlink dart@<new> && brew link dart@<new>

// 查看已经安装的Dart版本
brew info dart

异步

Dart是单线程语言,类似JS,通过异步编程模型(Event Loop)来实现异步操作,这块跟JS比较相似。核心概念是 async/await/Future/Stream。

Flutter入门概览2-Dart篇

EventLoop

Event Loop的执行过程:

Flutter入门概览2-Dart篇

Dart有两个事件队列:

  • Microtask Queue,微队列,优先级最高,Event Loop会优先检查MicroTask队列,用于非常简短且需要异步执行的内部动作(比如Stream的异步通知),该队列一般交给Dart处理,开发者很少用到。
  • Event Queue,事件队列,包含所有的外部事件,比如 I/O,绘制事件,Timer,isolate之间的通信等等,Async/Future/Stream基本上都使用Event Queue。

Async/Await

Dart的异步借鉴了JS的概念,async/await的用法基本同JS中使用。await函数会返回一个Future对象。

注意点:

  1. await必须在async函数中,这点同js。
  2. await的报错只能通过trycatch解决,不能await后面跟catchError
// Future类似JS中Promise的概念
Future<String> _test() async{
  return 'hello test';
}

// await必须在async函数中
void _incrementCounter() async {
  final res = await _test();
  print(res);
}


Futrue

Futrue的使用基本上与Promise相同。

一个Future对象会有以下两种状态:

  • pending:表示Future对象的计算过程仍在执行中,这个时候还没有可以用的result。
  • completed:表示Future对象已经计算结束了,可能会有两种情况,一种是正确的结果,一种是失败的结果。

Futrue核心方法:

  • then,获取执行成功的结果,同JS中then
  • catchError,获取Future和then中的异常,捕获来自try-catch异常、throw错误等,同JS中的catch
  • whenComplete,无论成功或失败总是会被调用,同JS中的finally
// then,catchError,whenComplete
  Future(() {
      return "1";
    }).then((value) {
      print('then$value');
    }).catchError((error) {
      print("catchError$error");
    }).whenComplete(() {
      print("whenComplete");
    });
    
 // Future与async配合使用
void _test() async {
  final res = await Future(() => "1");
  print('res$res');
}

Futrue其他API:

  • Future.value(),Creates a future completed with value,用于需要把数据包装成Future对象的场景,该Future为成功状态,会触发then
  • Future.error(),Creates a future that completes with an error,用于需要把数据包装成Future对象的场景,该Future为失败窗台,会触发catchError
    // 用于需要把数据包装成Future对象的场景
    Future<String> fetchDataFromCache(String key) {
      if (cachedData.containsKey(key)) {
        return Future.value(cachedData[key]);
      } else {
        return Future.error("Data not found in cache");
      }
    }
    
  • Future.wait(),等待执 行多个Futrue,等待所有Future执行完,类似JS中的Promise.all,但有所不同。当所有Future都成功时会执行then,但是如果有一个错误,依然会等待所有的执行完成,然后走catchError,捕捉到第一个错误(如果有多个,也只会捕捉到第一个报错),这是默认行为(因为eagerError默认为false,如果设置为true,则遇到错误会直接执行catchError,但是其他Future依然会执行)。
        // 考虑这段代码
        Future.wait([
          Future(() {
            print("future 1");
            return "1";
          }),
          Future(() {
            print("future 2");
            throw "2 throw";
            // return "2";
          }),
          Future(() {
            print("future 3");
            return "3";
          }),
          Future.delayed(Duration(seconds: 3), () {
            print("future 4 Delayed");
          }),
        ], eagerError: false)
            .then((res) {
          print("then $res");
        }).catchError((error) {
          // catchError只会捕捉到第一个错误,其他错误会抛弃
          print("catchError $error");
        });
        
        // 默认 eagerError == false,此时遇到throw,依然会执行完所有Future,然后走catchError
        // 此时输出:
        flutter: future 1
        flutter: future 2
        flutter: future 3
        flutter: future 4 Delayed
        flutter: catchError 2 throw
        
        // 设置 eagerError == true,此时输出:
        flutter: future 1
        flutter: future 2
        flutter: catchError 2 throw  // 遇到错误会立即走到catchError,但是其他的future依然会执行
        flutter: future 3
        flutter: future 4 Delayed
    
  • Future.any(),执行多个Future,先执行完先返回,类似JS的Promise.any()
  • timeout,设置Future的超时时间,超过时间后会抛出 TimeoutException
        // 示例代码
        Future<String> _testTimeout() async {
          await Future.delayed(Duration(seconds: 3));
          return "_testTimeout";
        }
      
      void _incrementCounter() async {
        Future(() async {
          final res = await _testTimeout();
          print("Future res $res");
        }).timeout(Duration(seconds: 1)).then((value) {
          print("then $value");
        }).catchError((error) {
          print("catchError $error");
        });
      }
      
        // 点击按钮执行 _incrementCounter(),输出:
        flutter: catchError TimeoutException after 0:00:01.000000: Future not completed
        flutter: Future res _testTimeout
    
  • delayed(),延迟执行,时间对Timer的包装
    Future.delayed(const Duration(seconds: 1), () {
      print('One second has passed.'); // Prints after 1 second.
    });
    
  • Furture.microtask(),创建一个Future在MicroTask队列执行。
  • Completer,通过该Class可以手动控制Future完成,即Completer包装了一个 Future对象
    • Completer(),构造一个 Completer 对象
    • completer.future,返回一个Future对象
    • complete(),表示 Completer 完成,会触发then
    • completeError(),表示 Completer 报错,会触发catchError
      Future(() {
        // 创建 Completer,手动控制Future
        Completer<int> completer = Completer<int>();
        Timer(const Duration(seconds: 2), () {
          print("timer 2s");
          // 手动控制 Future完成
          completer.complete(2);
          // or
          // completer.completeError(Error()); // 走到 catchError
        });
        
        // 返回 completer的future,
        return completer.future;
      }).then((value) {
        print(value);
      }).catchError((onError) {
        print(onError);
      });
    

并发

Flutter入门概览2-Dart篇

Isolate - 并发

虽然有了异步队列机制,但是当涉及到比较耗时的操作时,为了发挥cpu多核的优势,dart创建了isolate的机制来实现并发。

isolate是Dart中的多线程概念,但不完全等于线程或进程,是一种更高级的抽象封装。isolate的核心就是每个isolate都是独立的,之间不共享任何数据,这点有点像进程的概念。最后isolate的数据传递是直接返回的内存块,不是新的拷贝。所以取了一个isolate的概念,就像孤岛,彼此之间没有数据共享,不会牵涉到多线程之间的相关操作。

Xcode工程测试,每新建一个Isolate,就会创建一个线程,如下图DartWorker34和DartWorker35。其中DartWorker33一直存在,推测该线程负责创建Isolate对应的DartWorker线程。另外,Isolate创建开始运行后,观察发现会创建多于1个线程,最后会被释放剩下一个DartWorker线程。基本符合Isolate的描述,即Isolate是对线程和进程的抽象,在不同平台会依据平台创建线程或进程。

另外,在Flutter平台推荐使用foundation中computeAPI,是对isolate在flutter中的封装。

Flutter入门概览2-Dart篇

总结下,isolate特点:

  • isolate是基于对底层系统的线程和进程的高级封装,不完全等于线程/进程的概念
  • 每个isoate都是独立的,isolate之间不共享对象,使用消息机制进行通信
  • isolate之间传递的数据是内存块,不是值的拷贝,被称为 closure,closure中的数据是受限制的,不是所有的数据结构都被允许,比如Socket就无法被传递,具体参考send()文档确定可以传递的类型
  • isolate在web上不被支持,web上依然需要使用web worker
  • 在Flutter平台上,推荐使用compute(),该API内部封装了isolate

isolate生命周期

Flutter入门概览2-Dart篇

main isolate的运行模型:

Flutter入门概览2-Dart篇

isolate相关的API:

  • spawn,创建新的Isolate
  • ReceivePort/SendPort,通过ReceivePort实现消息通讯,ReceivePort负责接收消息,SendPort负责发送消息 ,SendPort是由ReceivePort创建的。
  • run,对spawn和ReceivePort的封装,简化调用,建议直接使用run方法
  • spawnUri,创建新的isolate,与spawn不同的是,spawnUri可以指定新的Isolate代码文件路径,该文件中必须有一个main函数,注意Flutter不支持该API
  • exit,立即同步终止当前isolate

// Demo 1,通过 spawn/ReceivePort/SendPort 建立 isolate单向通信

var recv = ReceivePort();
// 
Isolate.spawn<SendPort>((SendPort port) { 
  port.send("hello isolate send!");
}, recv.sendPort);

// 这里可以简单使用 first 只接受第一个事件,然后停止监听
final res = await recv.first;
print("recv: $res");

// 或者使用 recv.listen 监听
// recv.listen((message) { 
//   print("recv: $message");
// });


// Demo 2. 通过Isolate.run()方法,建议直接使用这个方法
final res = await Isolate.run(() {
  return "hello isolate run!";
});
print("recv:$res");


// Demo3, Isolate.spawnUri,使用场景时执行指定文件的dart代码
// 注意Flutter不支持,这里只作为了解
ReceivePort rp = new ReceivePort();
Isolate newIsolate = await Isolate.spawnUri(new Uri(path: "./other_task.dart"), ["hello, isolate", "this is args"], rp.sendPort);


// Demo4,Isolate的异常处理
  const String downloadLink = '下载链接';
  final resultPort = ReceivePort();

  await Isolate.spawn(
    readAndParseJsonWithErrorHandle,
    [resultPort.sendPort, downloadLink],
    onError: resultPort.sendPort,
    onExit: resultPort.sendPort,
  );

  // 获取结果
  final response = await resultPort.first;
  if (response == null) { // 没有消息
    print('没有消息');
  } else if (response is List) { // 异常消息
    final errorAsString = response[0]; //异常
    final stackTraceAsString = response[1]; // 堆栈信息
    print('error: $errorAsString \nstackTrace: $stackTraceAsString');
  } else { // 正常消息
    print(response);
  }

flutter中的并发

在flutter中推荐使用compute() API,该API封装了Isolate.run(),支持web平台。

使用起来非常简单。

import 'package:flutter/foundation.dart';

// compute简单使用
final res = await compute((message) {
  return 'Hello compute!';
},'hello');
print(res);

awesome-dart

awesome-dart

参考资料

转载自:https://juejin.cn/post/7377277576652111891
评论
请登录