美文网首页Android技术知识Android开发Android开发经验谈
给Java开发者的Flutter开发基础---Dart语言

给Java开发者的Flutter开发基础---Dart语言

作者: 皮球二二 | 来源:发表于2018-12-20 14:03 被阅读23次

    接近半年没有在简书冒泡了。这段时间一是忙于使用云信IM开发相应项目,二是整理和收集相关Flutter的相关资料进行学习。国内关于Flutter的资料还是太过于稀少,以至于我只能去YouTube和Udemy上看英文版视频去学习,其间的感受可想而知。。。不过不可否认的是,有些视频讲解的内容还是很好的,不是那种简简单单的零基础学习。
    言归正传,我们还是开始今天的学习。
    相关代码已经上传到github,欢迎大家star、follow

    Dart语言是什么

    Dart是谷歌开发的计算机编程语言,亮相于2011年10月10至12日在丹麦奥尔胡斯举行的GOTO大会上。2015年5月Dart开发者峰会上,亮相了基于Dart语言的移动应用程序开发框架Sky,后更名为Flutter。
    为什么Flutter会选择Dart?我觉得主要基于以下三点:
    a) 热重载。我想每个Android开发者都对漫长的编译时长感到很无奈,往往选择上厕所或者倒杯水舒缓一下心情。但是Dart不一样,直接给你一个热重载,瞬间就能看到效果。虽然之前阿里也出过Android开发热重载的相关插件Freeline,但是效果不仅一般,而且插件本身开发难度较高,导致项目停摆1年多,已经失去使用意义。所以总的来说,Dart的热重载绝对是一个跨时代的功能
    b) Dart可以在没有锁的情况下进行对象分配和垃圾回收。就像JavaScript一样,Dart避免了抢占式调度和共享内存(因而也不需要锁),所以Dart可以更轻松地创建以60fps运行的流畅动画和转场。这个在你使用Dart开发出App之后必定会深有感触
    c) Dart语言特别容易学习。Dart语言与Java语言非常类似,这也是本篇为什么以“给Java开发者”来开篇的原因。

    话不多说,开始学习吧。


    开始

    1. Dart的一个简单例子

    这是使用Dart语言来实现最基础的计算器加减乘除功能的范例。我们先宏观的了解一下Dart语言大体风格。

    void main() {
      print("加法结果为:${operation(10, 5, add)}");
      print("减法结果为:${operation(10, 5, sub)}");
      print("乘法结果为:${operation(10, 5, multi)}");
      print("除法结果为:${operation(10, 5, div)}");
    }
    
    int add(int a, int b) {
      return a+b;
    }
    
    int sub(int a, int b) {
      return a-b;
    }
    
    int multi(int a, int b) {
      return a*b;
    }
    
    int div(int a, int b) {
      return a~/b;
    }
    
    int operation(int a, int b, int method(int a, int b)) {
      return method(a, b);
    }
    

    如果你之前学习过Java或者Kotlin语言(当然,部分Dart语法可能与其他语言类似,不过我没学过,不在此深究),那么Dart给你的第一感觉便是:哇,这个看上去似曾相识
    Dart语言的基本类型也是诸如int、String等寻常关键字。同Kotlin一样,也可以使用${表达式}将表达式放入字符串中。Dart的访问控制符没有显式的展现出来,这个我们会在稍后进行详细介绍
    同Kotlin一样,在Dart中我们可把一个函数当做一个变量传入到另外一个函数中

    只要你有程序开发基础,我想上面的范例对你来说应该没啥问题。下面我们从细节上开始来详细了解Dart语言吧,这里我有一个声明:过于基础的知识点我只会一笔带过,时间应该花费在重要的知识点上

    2. 变量、类型

    Dart的几种内置的数据类型如下:数值型- num、布尔型-bool、字符串-String、列表-List、键值对-Map、其他类型-RunesSymbols。而数值型仅有intdouble,不像其他语言分的很细

    num num1 = 1;
    num num2 = 1.1;
    int int1 = 1;
    double double1 = 1.1;
    

    如果变量使用num进行声明,则可以随意在使用中转换为intdouble类型;但如果使用int或者double进行明确的声明,那么就不能随意转换了

    num1 = 1.1;
    num2 = 1;
    //  int1 = 1.1;  错误
    

    除了以上所说的数据类型,Dart还有vardynamicconst三种数据类型
    var可以用来声明任意类型,Dart会根据其被赋予的数值的数据类型进行自动推导

    var var1 = "String";
    var var2 = 1;
    var var3 = 1.1;
    var var4 = true;
    

    如果你仅使用var声明一个变量但是并未对其进行赋值,那么你可以在使用过程中将其更改为任意数据类型的值

    var var5;
    var5 = 1;
    var5 = 1.1;
    

    但如果你使用var声明变量的时候已经对其赋予指定数据类型的值,那么其数据类型就不可以更改了,因为此时已经决定了它是什么类型。var修饰的变量一旦被编译,则会自动匹配var变量的实际类型,并用实际类型来替换该变量的申明

    var var6 = 1;
    //  var6 = 1.1;  错误
    

    dynamic被编译后,实际是一个Object类型,只不过编译器会对dynamic类型进行特殊处理,让它在编译期间不进行任何的类型检查,而是将类型检查放到了运行期

    dynamic dynamic1 = 1;
    dynamic1 = 1.1;
    dynamic1 = "";
    dynamic1 = true;
    

    正因为类型检查放到了运行期,所以在使用dynamic的时候需要倍加小心

    //  dynamic1++;  错误,编译期可以通过,但是运行时报错
    

    再来看看const。Dart中有两种数据常量数据类型,constfinal。与final不同的是,const是编译时常量。什么是编译时常量,来看这么个例子
    String类里面有一个判断其是不是为空的函数isEmpty。在代码编译过程中是没法知道字符串""是不是为空的,只有当代码运行到此行之后,才能通过isEmpty函数的调用知道。所以const就不满足这一类型的常量声明,只能使用final

    final int int3 = "".isEmpty ? 10 : 6;
    //  const int int4 = "".isEmpty ? 10 : 6;  错误
    

    同理,下面这个例子就可以。因为代码在编译期就知道8是肯定大于6的,所以这个值就能被确定下来

    const int int4 = 8 > 6 ? 8 : 6;
    

    可以使用const进行初始化的对象也可以使用final进行赋值,反之则不行

    final int5 = 8 > 6 ? 8 : 6;
    

    任何数据类型,包括int或者double这种看似基本类型的,如果没有赋默认值,Dart都会用null来作为其默认值。这是不是比Java还要面向对象?

    int int7;
    print(int7);
    

    这里补充一个不相干的知识点。print可以打印任意Object类型的对象,Stringdoubleintbool的父类都是Object
    print("Hello World");
    print(3);
    print(1.1);
    print(true);
    print(null);
    

    Dart中操作符大体上与其他语言差不多,这里只介绍一些不常见的操作符

    1. ~/。Java里面如果int/int的话,得到的也是int,这就是通常所说的取整。如果要得到浮点型数值的结果,则需要将其中一个数值变成浮点型数值才行,但是Dart不需要这样
    var double2 = 7 / 3;
    print("double2:$double2");
    

    有什么办法可以得到int呢?那就需要用到~/

    var int2 = 7 ~/ 3;
    print("int2:$int2");
    
    1. as强转操作符。这个跟Kotlin是一样的。这边要是转换格式不匹配,则会报错
    //  int int8 = num1 as int;  运行时报错
    double double3 = num1 as double;
    

    所以在使用之前最好判断一下

    if (num1 is int) {}
    
    1. ??=。空赋值操作符
    int int9;
    int9 ??= 11;
    print("int9:$int9");
    
    1. ?. 。类似于Kotlin的非空判断
    int a10;
    print(a10?.toString());
    a10 = 10;
    print(a10?.toString());
    
    1. 级联符号.. 允许您在同一个对象上进行一系列操作。 除了函数调用之外,还可以访问同一对象上的字段。其实相当于Java的链式调用
    CircleShape shape = new CircleShape()
      ..radius = 3
      ..color = 1;
    

    Dart里没有private/protected/public等权限修饰符,这就意味着默认情况下函数或者常量、变量都是可访问的。但是Dart还是有私有权限设置的办法的,只需要将需要修饰的函数或者常量、变量加上_前缀即可。但是这里比较坑的一点是,_并不是从class级别去限制,而是从package级别去限制
    在同一个包下面创建一个私有属性

    class PrivateTest {
      String _private = "private";
    }
    

    使用正常

    print(PrivateTest()._private);
    

    将该类移至到其他包下


    //  print(PrivateTest2()._private2);  错误,无法访问private
    

    3. 控制流和异常

    这个很简单,不多啰嗦了

    int age = 30;
    if (age < 30) {
      print("young");
    } else if (age > 33) {
      print("old");
    } else {
      print("Just so so");
    }
    
    for (int i = 0; i < 7; i++) {
      print(i);
    }
    var list2 = <String>["1", "2", "3", "4"];
    for (String value in list2) {
      print(value);
    }
    
    int a3 = 0;
    while (a3 < 10) {
      print("End?");
      a3++;
    }
    
    do {
      print("End2?");
      a3--;
    } while (a3 != 0);
    

    switch的用法有一个地方需要单独说一下。Dart提供了从一个case转入其他case的功能,只需要使用continue关键字加上自定义的标签来完成

    int score = 70;
    switch (score ~/ 10) {
      case 9:
        print("Wonderful");
        break;
      case 8:
        print("Great");
        break;
      case 7:
        print("Good");
        continue KeepTrying;
        break;
      case 6:
        print("Just so so");
        continue KeepTrying;
        break;
      KeepTrying:
      default:
        print("Keep trying");
        break;
    }
    

    异常捕获。写法与Java基本类似,但是还是有点小区别

    try {
      String a;
      print(a.length);
    } on NoSuchMethodError catch (e) {
      print(e.toString());
    } catch (e) {
      print(e.toString());
    }
    

    oncatch的区别在于是否关心异常的实例


    4. 字符串

    在任何一门语言中,字符串都是被单独拿出来的小重点,因为他启到一个承上启下的作用,后面我们将开始接触到类与函数的概念。刚才我们知道Dart中Stringint同属对象,虽然情况与Java等有所不同,但是我们依然重点单独讲一下它
    Dart中可以使用单引号或双引号声明字符串。在Java和Kotlin中都不可以这样,单引号只能声明为一个char

    String string2 = "Hello World";
    String string3 = 'Hello World';
    

    字符串拼接的方式很多:
    使用空格来拼接

    String string4 = "Hello" "Hello" "Hello";
    

    使用+来拼接

    String string5 = "Hello" + "Hello" + "Hello";
    

    使用换行来拼接

    String string6 = "Hello"
        "Hello"
        "Hello";
    

    使用${表达式}来拼接。这个跟Kotlin是一样的

    String string7 = "$string2";
    

    剩下的,如何使用String中的函数,倒是没什么好说的,大家都能看得懂

    String string1 = "Hello World";
    string1.contains("Hello");
    string1.endsWith("World");
    string1.indexOf("e");
    string1.isEmpty;
    string1.length;
    string1.lastIndexOf("l");
    string1.replaceRange(0, 5, "Hi");
    string1.substring(0, 5);
    string1.split(" ").length;
    string1.trim();
    string1.toLowerCase();
    string1.toUpperCase();
    string1.toString();
    

    5. 函数

    开始进入重点部分了

    Dart的函数基本上跟Java是一样的,除了没有权限修饰符

    int func1(int a, int b) {
      return a + b;
    }
    
    void func4() {
      print("fun4");
    }
    

    函数调用

    func1(1, 2);
    

    有语法糖可以让单行函数体变得更优雅

    int func2(int a, int b) => a + b;
    

    可以省略函数返回类型,默认返回null

    func3(int a, int b) => a + b;
    
    print("func3():${func3(1, 2)}");
    

    可选参数
    这个概念在Java跟Kotlin都是没有的。既然是可选,那就是参数可以不传
    可选参数分为2种,可选位置参数可选命名参数
    可选位置参数严格根据函数的位置传入参数,它有个很明显的标志[]。来看下可选位置参数的写法,你可以选择只传c或者同时传c、d,但是不可以只传d不传c

    int func5(int a, int b, [int c, int d]) {
      if (c != null && d != null) {
        return a + b + c + d;
      } else if (c != null) {
        return a + b + c;
      } else if (d != null) {
        return a + b + d;
      }
      return a + b;
    }
    
    print(func5(1, 2));
    print(func5(1, 2, 3));
    print(func5(1, 2, 3, 4));
    

    可选命名参数相对灵活一点,你可以选择传递任何一个你想传的参数,它有个很明显的标志{}。在传入的时候只需要指定下对应的参数名,没有顺序限制也没有可选位置参数那样传参前置条件

    int func6(int a, int b, {int c, int d}) {
      if (c != null && d != null) {
        return a + b + c + d;
      } else if (c != null) {
        return a + b + c;
      } else if (d != null) {
        return a + b + d;
      }
      return a + b;
    }
    
    print(func6(1, 2));
    //  print(func6(1, 2, 3, 4));  错误,可选参数必须要指定对应的参数名
    print(func6(1, 2, c: 3));
    print(func6(1, 2, d: 4));
    print(func6(1, 2, c: 3, d: 4));
    

    默认参数值
    在函数的参数上面使用=号给一个常量值。如果没有传入该值,代码在运行时就使用刚才给的值

    void func7(int a, int b, [int c = 10, int d]) {}
    void func8(int a, int b, {int c = 10, int d}) {}
    

    函数对象
    Dart中函数也是一个对象,可以通过var或者Function来声明

    void func9() {
      print("func9");
    }
    
    Function function = func9;
    function();
    

    函数对象可以作为一个入参,也可以作为一个返回值对象返回

    void func10(Function function) {
      function();
    }
    
    func10(func9);
    

    当函数作为一个返回值对象返回时,我们也称其为闭包。闭包定义在其它函数内部,能够访问外部函数的局部变量,并持有其状态

    Function func11(int value1) {
      return (int value2) {
        return value1 + value2;
      };
    }
    
    Function function11 = func11(1);
    print(function11(2));
    

    6. 类型List(列表)、Set(集合)和Map(映射)

    这节其实是对函数概念的理解的一个升华。List/Set/Map使用起来其实很简单的,函数名与Java几乎雷同

    先来看看List

    创建List对象的方式很多,常见的是使用[]创建列表。这个可不是Java里的数组

    List list1 = ["Ronaldo", 33, "Messi", 30];
    

    使用泛型来限制列表可添加数据类型

    List list3 = <int>[33, 30];
    

    此种写法又是在运行时进行数据类型检查,所以要小写添加的数据的类型是否匹配

    //  list3.add(true);  错误,编译通过,运行出错
    

    创建固定长度的列表

    List<String> list5 = new List(5);
    

    创建可改变长度的列表

    List<String> list7 = new List();
    list7.add("1");
    list7.add("2");
    list7.add("3");
    list7.add("4");
    list7.add("5");
    list7.add("6");
    list7.length = 10;
    list7[9] = "9";
    //  list7[99] = "9"; 错误,长度只有10
    list7.length = 13;
    list7[12] = "12";
    list7.length = 15;
    list7[14] = "14";
    

    在初始化固定长度后的List中添加数据

    List<String> list6 = new List()..length = 10;
    list6.add("1");
    list6.add("2");
    list6.add("3");
    list6.add("4");
    list6.add("5");
    list6.add("6");
    list6.add("7");
    list6.add("8");
    

    利用List类中的工厂构造函数来创建

    List<String> list8 = List<String>.from(["Ronaldo", "Messi"]);
    

    为所有List中的元素统一赋初值

    List<String> list9 = List<String>.filled(3, "");
    

    用生成器给所有元素赋初始值

    List<String> list10 = List<String>.generate(3, (int index) {
      return "";
    });
    

    遍历列表的方式
    虽然之前指定了添加的数据类型,但是泛型在前与在后效果是不一样的

    List list3 = <int>[33, 30];
    list3.forEach((dynamic value) {
      print("$value");
    });
    

    泛型在前的话就可以直接使用指定类型进行遍历了

    List<String> list2 = ["Ronaldo", "Messi"];
    list2.forEach((String value) {
      print("$value");
    });
    

    其他List类中的函数,跟之前一样,应该都能看得懂

    list2.add("Dybala");
    list2.length;
    list2.contains("Dybala");
    list2.clear();
    list2.elementAt(0);
    
    // 对现有元素进行扩展
    List<String> temp = ["Piatek", "Mandzukic"];
    temp.expand((String element) {
      return [element, element];
    });
    
    list2.insert(2, "Messi");
    
    List<String> iterable = <String>["Pogba", "Griezmann"];
    //  List iterable = <String>["Pogba", "Griezmann"];  错误,类型不一致不能添加
    list2.addAll(iterable);
    list2.addAll(<String>["Harry Kane", "Modric"]);
    
    // 判断列表中的任一元素是否满足指定条件
    bool any = list2.any((String element) {
      return element == "Dybala";
    });
    
    // 判断列表中的全部元素是否满足指定条件
    bool every = list2.every((String element) {
      return element == "Dybala";
    });
    
    list2.first;
    
    // 获取列表中第一个满足指定条件的元素
    try {
      var firstWhere = list2.firstWhere((String element) {
        return element == "Dybala1";
      });
    } catch (e) {
      
    }
    
    var range = list2.getRange(0, 2);
    
    print(list2.indexOf("Griezmann"));
    // 从第几个元素开始查找
    print(list2.indexOf("Griezmann", 1));
    
    list2.isEmpty;
    
    // 查找满足指定条件的元素索引
    int index = list2.indexWhere((String element) {
      return element == "Messi";
    });
    
    // 转成字符串
    print(list2.join());
    
    list2.map((String value) {
      return "person $value";
    }).forEach((String value) {
      print(value);
    });
    
    list2.removeLast();
    
    list2.remove("Messi");
    
    list2.removeAt(1);
    
    // 列表按照指定逻辑进行拼接
    var reduce = list2.reduce((String value, String element) {
      return "${value} + ${element}";
    });
    print(reduce);
    
    // 获取列表中start、end索引间的集合
    var tempList = list2.sublist(2);
    
    // 查找列表中唯一一条满足指定条件的元素,如果元素数量大于1则报错
    String singleWhere = list2.singleWhere((String string) {
      return "Harry Kane" == string;
    });
    print(singleWhere);
    
    // 查找列表中所有满足指定条件的元素
    list2.where((String string) {
      return string.substring(0, 1).toLowerCase() == 'm';
    }).forEach((String string) {
      print(string);
    });
    
    // 查找列表中所有满足指定条件的元素。与where不同的是,retainWhere直接将不满足的元素从原始List中去除,而where则不会破坏原数据
    list2.retainWhere((String string) {
      return string.substring(1, 2).toLowerCase() == 'o';
    });
    
    // 排序
    list2.sort((String a, String b) {
      return a.codeUnitAt(0) > b.codeUnitAt(0) ? 1 : 0;
    });
    

    使用{}创建Map。Dart中Map使用方式基本与Java类似

    Map<String, int> map = {
      "Juventus": 1,
      "Napoli": 2,
      "Inter": 3,
      "A.C. Milan": 4
    };
    

    Map也有相应的工厂函数来实现构建

    Map<String, int> map2 = new Map.fromIterables(["a", "b"], [1, 2]);
    
    Map<String, String> map3 = new Map.fromIterable(["a", "b"], key: (element) {
      return element;
    }, value: (element) {
      return element;
    });
    

    其他Map类中的函数,跟之前一样,应该都能看得懂

    map["Lazio"] = 5;
    
    print(map["Juventus"]);
    
    Map<String, int> tempMap = {"Roma": 8};
    map.addAll(tempMap);
    
    map.containsKey("Real Madrid");
    
    map.isNotEmpty;
    map.isEmpty;
    
    map.keys;
    map.values;
    
    map.length;
    
    map.map((String key, int value) {
      return MapEntry("team: $key", value);
    }).forEach((String key, int value) {
      print("key: $key value: $value");
    });
    
    map.remove("Inter");
    
    map.removeWhere((String key, int value) {
      return key == "Napoli";
    });
    
    map.update("Roma", (int value) {
      return 2;
    });
    
    map.clear();
    

    Set是没有顺序且不能重复的集合,所以不能通过索引去获取值

    Set<String> set = new Set.from(["Ronaldo", "Messi"]);
    
    Set<String> set2 = new Set();
    set2.add("Italy");
    set2.add("Italy");
    set2.addAll(["England", "France"]);
    
    set2.forEach((String value) {
      print("value: ${value}");
    });
    
    set.first;
    set.last;
    
    set.contains("Ronaldo");
    set.containsAll(["Ronaldo", "Messi"]);
    
    set.difference(set2).forEach((dynamic value) {
      print("$value");
    });
    
    //  set.clear();
    
    set.elementAt(0);
    
    set.length;
    
    set.take(2).toList();
    
    set.union(set2).forEach((dynamic value) {
      print("$value");
    });
    

    ListSetMap有一些通用的函数。其中的一些通用函数都是来自于类IterableListSetIterable类的实现。虽然Map没有实现Iterable, 但是Map的属性keysvalues都是Iterable对象


    7. 类和泛型

    其实类和泛型的概念很简单,因为几乎与Java一样
    先看看类的声明

    class ClassTest {
      ClassTest()
    }
    

    如果没有声明构造函数,那么初始化的时候使用的是默认构造函数。
    Object类是所有类的父类
    在Dart语言中,子类构造函数必须继承父类的构造函数
    若调用的是默认构造函数,则无需显式声明继承关系
    所以完整的描述ClassTest类应该是

    class ClassTest {
      ClassTest() : super() {}
    }
    

    子类可以自由选择继承父类的哪个构造函数,只需要在自身构造函数后加:号,在:后面指定父类的构造函数

    class ClassParentTest {
      ClassParentTest(String value) {}
    
      ClassParentTest.FromString(String value) {}
    }
    
    class ClassChildTest extends ClassParentTest {
      ClassChildTest(String value) : super.FromString(value) {}
    }
    

    当然也重定向到同类的另一个构造函数上,但是不可以有额外的函数体

    class ClassChildTest extends ClassParentTest {
      ClassChildTest(String value) : super.FromString(value) {}
    
      ClassChildTest.FromString(String value) : this(value);
    }
    

    类的使用跟Java一样

    ClassTest classTest = new ClassTest();
    

    类中未初始化的实例变量的默认值都为null,我们可以像在Java中那样通过构造函数去初始化

    class Caculator {
      int x;
      int y;
    
      Caculator(int x, int y) {
        this.x = x;
        this.y = y;
      }
    }
    

    来一个Dart特有的构造函数语法糖,这样初始化是不是看的更简洁一点

    class Caculator {
      int x;
      int y;
    
      Caculator(this.x, this.y) {}
    }
    

    还可以在构造函数体运行之前初始化实例变量

    class Caculator {
      int x;
      int y;
    
      Caculator(int x, int y)
        : this.x = x,
          this.y = y {}
    }
    

    因为Dart没法重载构造函数,所以提供了命名构造函数来解决这个问题

    class Caculator {
      int x;
      int y;
    
      Caculator.FromAnother(this.x, this.y) {}
    }
    

    命名构造函数的使用

    Caculator caculator = Caculator.FromAnother(1, 2);
    

    工厂构造函数,它是实现单例的一个好选择

    class Shape {
      String desp;
    
      static final Map<String, Shape> _cache = new Map();
    
      factory Shape.Type(String type) {
        if (_cache.containsKey(type)) {
          return _cache[type];
        }
        if (type == "circle") {
          Shape shape = new Shape.Circle("this is a circle");
          _cache[type] = shape;
          return shape;
        } else if (type == "square") {
          Shape shape = new Shape.Square("this is a square");
          _cache[type] = shape;
          return shape;
        } else {
          Shape shape = new Shape.Unknown("this is an unknown shape");
          _cache[type] = shape;
          return shape;
        }
      }
    
      Shape.Circle(this.desp) {}
    
      Shape.Square(this.desp) {}
    
      Shape.Unknown(this.desp) {}
    }
    

    类中的函数的使用跟Java也是几乎一样

    class Caculator {
      int x;
      int y;
    
      Caculator(this.x, this.y) {}
    
      int add() {
        return x + y;
      }
    
      int subract() {
        return x - y;
      }
    }
    

    gettersetter是特殊的函数,可以读写访问对象的属性,每个实例变量都有一个隐式的getter,适当的加上一个setter,可以通过实现gettersetter创建附加属性

    class Caculator {
      int x;
    
      void set setX(int x) {
        this.x = x;
      }
    
      int get getX {
        return x;
      }
    }
    

    怎么初始化类中final类型的变量呢,可以这样

    class CircleShape {
      int radius = 0;
      int color = 0;
      final int size;
      final int price;
    
      CircleShape() : size = 3, price = 1; // 通过此种方式对final值进行初始化
    }
    

    级联,刚才我们已经介绍过了

    CircleShape shape = new CircleShape()
        ..radius = 3
        ..color = 1;
    

    静态类

    class StaticClass {
      // 静态变量
      static String staticValue = "";
      // 静态函数
      static void staticFunction() {}
    }
    

    想让类生成的对象永远不会改变,可以让这些对象变成编译时常量,定义一个const构造函数并确保所有实例变量是final的。这是实现单例的一个好办法

    class StaticClass {
      const StaticClass();
      static final StaticClass value = new StaticClass();
    }
    

    抽象类

    abstract class Continent {
      String name = "Continent";
    
      Continent(String name) {
        this.name = name;
      }
    
      // 抽象函数
      String getContinentName();
    
      void desp() {
        print("This is Continent ${getContinentName()}");
      }
    }
    

    继承抽象类

    class Asia extends Continent {
      Asia(String name) : super(name) {}
    
      @override
      String get name => super.name;
    
      @override
      String getContinentName() {
        return "Asia";
      }
    
      @override
      void desp() {
        print("Hello");
        super.desp();
      }
    }
    

    每个类都有一个隐式定义的接口,包含所有类和实例成员。Java里面接口就是接口,与Dart不同

    class Europe implements Continent {
      @override
      String name;
    
      @override
      void desp() {
        print("This is Continent ${getContinentName()}");
      }
    
      @override
      String getContinentName() {
        return "Europe";
      }
    }
    

    使用with关键字后面跟着一个或多个扩展类名,这相当于同时继承了2个类。但是,如果相同变量或者函数名类型不一致,则不可以mixin

    class Club {
      String clubName;
      int _year;
      int color;
    
      void set year(int year) {
        this._year = year;
      }
    
      int get year {
        return this._year;
      }
    
      void cFunction() {}
    }
    
    class Sponsor {
      String sponsorName;
      int _year;
    
      void set year(int year) {
        this._year = year;
      }
    
      int get year {
        return this._year;
      }
    
      void sFunction() {}
    }
    
    class Person extends Club with Sponsor  {
      void a() {
        color;
        clubName;
        cFunction();
        sFunction();
      }
    }
    

    泛型跟Java也基本上差不多

    class Utils<T, R> {
      T valueA;
      R valueB;
    }
    

    8. 异步

    这可是Dart中的重点和难点
    Dart是一个单线程的语言,遇到有延迟的运算(比如IO操作、延时执行)时,按顺序执行运算会发生阻塞,用户就会感觉到卡顿,于是Dart采用异步处理来解决这个问题。当遇到有需要延迟的运算时,将其放入到延迟运算的队列中去,把不需要延迟运算的部分先执行掉,最后再来处理延迟运算的部分。

    async和await
    async关键字声明该函数内部有代码需要延迟执行
    await关键字声明运算为延迟执行,然后return运算结果,返回值为一个Future对象
    要使用await,必须在有async标记的函数中运行,否则这个await会报错
    因此,整个流程只需要记住两点

    1. await关键字必须在async函数内部使用
    2. 调用async函数必须使用await关键字

    下面是一个网络请求的例子

    Future<String> httpRequestTest() async {
      var httpClient = HttpClient();
      HttpClientRequest request = await httpClient
          .getUrl(Uri.parse("http://polls.apiblueprint.org/questions"));
      HttpClientResponse response = await request.close();
      if (response.statusCode == 200) {
        String value = await response.transform(utf8.decoder).join();
        return value;
      }
      return null;
    }
    

    通过then()来设置异步回调
    被添加到then()中的函数会在Future执行得到结果后立马执行(then()函数没有被加入任何队列,只是被回调而已)

    httpRequestTest().then((String value) {}).catchError(onError);
    

    通过then()可以实现Future的链式调用
    如下例,addAddress函数返回的是另外一个异步函数

    Future<Function> addAddress(int value) async {
      return (int x) async => value + x;
    }
    

    通过两次then()处理异步结果,可以在一行代码里得到最终返回值

    addAddress(10).then((Function function) {
      return function(20);
    }).then((dynamic value) {
      return value;
    });
    

    如果不使用then()函数是如何处理,我们来看看

    Future<int> normalUse() async {
      Function function = await addAddress(10);
      int value = await function(20);
      return value;
    }
    
    normalUse().then((int onValue) {});
    

    由此可见,对应普通调用方式,链式调用简单多了

    在Event队列中,事件以先进先出顺序执行。来看如下的实验,delayed1等待3s,delayed2等待2s

    Future<String> delayedFunc1() {
      return new Future.delayed(Duration(seconds: 3), () {
        print("Finish delayed1");
        return "Finish delayed1";
      });
    }
    
    Future<String> delayedFunc2() {
      return new Future.delayed(Duration(seconds: 2)).then((_) {
        print("Finish delayed2");
        return "Finish delayed2";
      });
    }
    }
    

    我们放到异步函数里面来测试一下

    void sequence() async {
      print(DateTime.now());
      await delayedFunc1();
      print(DateTime.now());
      await delayedFunc2();
      print(DateTime.now());
    }
    

    使用了await之后,虽然delayedFunc1延迟3s执行,delayedFunc2延迟2s执行,但是依然是FIFO的顺序

    有没有想过如果不放在异步函数里面会有什么效果?我们将其改成这样

    void sequence() {
      print(DateTime.now());
      delayedFunc1();
      print(DateTime.now());
      delayedFunc2();
      print(DateTime.now());
    }
    

    来看看结果。sequence函数很快就执行完了。似乎我们理解了其中的含义:函数本身并不是一个异步操作,若不加await则不会等待当前函数执行完成后再执行下一个


    Dart线程中有一个消息循环机制(event loop)和两个队列(event queuemicrotask queue
    event queue包含所有外来的事件:I/O,mouse events,drawing events,timers,isolate之间的message等;microtask queue只在当前任务队列中排队,优先级高于event queue。Dart事件循环执行两个队列里的事件。当main执行完毕退出后,event loop就会以FIFO(先进先出)的顺序执行microtask,当所有microtask执行完后它会从event queue中取事件并执行。如此反复,直到两个队列都为空
    当事件循环正在处理microtask的时候,event queue会被堵塞。这时候app就无法进行UI绘制,响应鼠标事件和I/O等事件

    让我们以一个实际例子来了解一下microtaskevent的流程。初看这个很感觉复杂,如果你第一遍不能理解,请再理解一遍

    void sequence2() {
      print('main #1 of 2');
      scheduleMicrotask(() => print('microtask #1 of 3'));
    
      new Future.delayed(
          new Duration(seconds: 1), () => print('future #1 (delayed)'));
    
      new Future(() => print('future #2 of 4'))
          .then((_) => print('future #2a'))
          .then((_) {
        print('future #2b');
        scheduleMicrotask(() => print('microtask #0 (from future #2b)'));
        new Future(() => print('future #2d (a new future)'));
      }).then((_) => print('future #2c'));
    
      scheduleMicrotask(() => print('microtask #2 of 3'));
    
      new Future(() => print('future #3 of 4'))
          .then((_) => print('future #3a'))
          .then((_) => new Future(() => print('future #3b (a new future)')))
          .then((_) => new Future(() => print('future #3c (a new future)')))
          .then((_) => print('future #3d'));
    
      new Future(() => print('future #4 of 4'))
          .then((_) => new Future(() => print('future #4a (a new future)')));
    
      new Future(() => print('future #5 of 5'));
    
      scheduleMicrotask(() => print('microtask #3 of 3'));
    
      print('main #2 of 2');
    }
    

    如果你的结果与此图一致,恭喜你,你完全搞懂了这个流程

    我们来分析一下

    1. 首先执行主线程,其次执行Microtask,最后才是Event
    2. 开始Event,添加Future2、3、4。先来到2,按then顺序打印。2这里新建了一个Microtask 0和Future 2d。因为return的依然是当前Future,所以2c依然跟着当前Future打印,新建的那个Future 2d被添加到Event最末尾
    3. 当前Event中的Future 2执行完毕,来了一个插队Microtask 0。执行新建的Microtask 0。
    4. Microtask执行完毕,继续回到Event。继续顺序打印3,3b被添加到Event最末尾,因为return的是新Future,所以跟之前的流程不一样,此时3c与3d暂不存在
    5. 继续顺序打印4,4a被添加到Event最末尾
    6. 继续顺序打印5,至此回过头来再检查Event中有没有剩余未执行的事件
    7. 按照添加的顺序2d开始执行,然后是3b,此时又添加了新的Future 3c到最后,3b执行完毕之后是4a
    8. 这波执行完毕之后,执行3c,异步回调得到3d
    9. 之前延迟的delay到现在才被添加进来执行
    10. 至此当前所有事件执行完成

    如果文字部分解释还不满足你,我们来看图

    在执行Event之前,Main与Microtask依次执行完毕 Future2执行完之后,执行Microtask。Microtask插队了 Future3执行 Future4执行 最终剩余事件依次执行

    至此,async和await学习完毕,下面还有另外一个挑战Stream


    Stream是一个异步数据源,它是Dart中处理异步事件流的统一API
    集合可以理解为“拉”模式,比如你有一个List,你可以主动地通过迭代获得其中的每个元素,想要就能拿出来。而Stream可以理解为“推”模式,这些异步产生的事件或数据会推送给你(并不是你想要就能立刻拿到)。这种模式下,你要做的是用一个listener(即callback)做好数据接收的准备,数据可用时就通知你。
    Stream有3个工厂构造函数:fromFuturefromIterableperiodic,分别可以通过一个FutureIterable或定时触发动作作为Stream的事件源构造Stream
    下面的代码就是通过一个List构造的Stream

    List<int> datas = new List(10000000);
    second(Stream.fromIterable(datas));
    

    我们可以通过async* + yield返回Stream对象

    Stream<String> getStreamData(Iterable<int> values) async* {
      for (int value in values) {
        await Future<String>.delayed(Duration(seconds: 1));
        yield "$value";
      }
    }
    

    通过listen()函数订阅Stream上发出的数据(即事件)
    下面的代码会先打印出从Stream收到的每个数字,最后打印一个‘Done’
    Stream中的所有数据发送完时,就会触发onDone的调用,但提前取消订阅不会触发onDone

    streamData.listen((String onData) {
      print("streamData $onData");
    }, onDone: () {
      print("onDone");
    }, onError: (dynamic error) {
      print("$error");
    });
    

    还可以通过listen的返回者subscription对象设置onDataonDone的处理
    下面的代码与前面的示例代码作用相同

    Stream<String> streamData2 = getStreamData(<int>[1, 3, 5, 7, 9]);
    
    StreamSubscription<String> subscription = streamData2.listen(null);
    subscription.onData((String onData) {
      if (int.parse(onData) > 5) {
        subscription.cancel();
      }
      print("streamData $onData");
    });
    subscription.onError((dynamic error) {
      print("$error");
    });
    subscription.onDone(() {
      print("onDone");
    });
    

    listen中的参数为null,也就是没有订阅者。通过listen的返回者subscription对象设置了onDataonDone的处理,这才有了订阅者
    如果在发出事件的同时添加订阅者,那么要在订阅者在该事件发出后才会生效。如果订阅者取消了订阅,那么它会立即停止接收事件
    上面一个例子最后会打印出1、3、5、7,9因为被cancel了所以不会打印

    Stream有两种订阅模式:单订阅(single)和多订阅(broadcast)。单订阅就是只能有一个订阅者,而广播是可以有多个订阅者
    Stream默认处于单订阅模式,所以同一个Stream上的listen和其它大多数函数只能调用一次,调用第二次就会报错。但Stream可以通过transform()函数(返回另一个Stream)进行连续调用。通过Stream.asBroadcastStream()可以将一个单订阅模式的Stream转换成一个多订阅模式的StreamisBroadcast属性可以判断当前Stream所处的模式

    streamData2.isBroadcast
    

    单订阅在订阅者出现之前会持有数据,在订阅者出现之后就才转交给它。而多订阅模式,可以同时有多个订阅者,当有数据时就会传递给所有的订阅者,而不管当前是否已有订阅者存在。但是多订阅模式如果没有及时添加订阅者则可能丢数据,不过具体取决于Stream的实现。
    下面的一个例子就是一旦有了第一个订阅者,然后再延迟添加第二个订阅者就会漏数据

    Stream<String> streamData21 = streamData2.asBroadcastStream();
    new Timer(Duration(seconds: 1), () {
      streamData21.listen((String onData) {
        print("streamData21 $onData");
      });
    });
    new Timer(Duration(seconds: 5), () {
      streamData21.listen((String onData) {
        print("streamData22 $onData");
      });
    });
    

    看看结果,此时后订阅的数据就丢失了。

    你也可以选择自定义Stream

    StreamController<int> streamController = new StreamController();
    streamController..add(1)..add(2)..add(3)..add(4)..add(5);
    streamController.close();
    
    Stream<int> stream = streamController.stream;
    stream.listen((int onData) {
      print(onData);
    });
    

    注意这里close就意味着事件结束了,所以多订阅模式会收不到数据,而单订阅模式则可以

    Stream和一般的集合类似,都是一组数据,只不过一个是异步推送,一个是同步拉取,所以他们都很多共同的函数,比如any函数

    Stream<String> streamData3 = getStreamData(<int>[1, 3, 5, 7, 9]);
    streamData3.any((e) => int.parse(e) > 2).then((bool value) {
      print(value);
    });
    

    Stream也有自己通用的数据转换函数transform()
    把一个Stream作为输入,然后经过计算或数据转换,输出为另一个Stream。另一个Stream中的数据类型可以不同于原类型,数据多少也可以不同

    Stream<String> streamData4 = getStreamData(<int>[1, 3, 5, 7, 9]);
    var transformer = new StreamTransformer.fromHandlers(
        handleData: (String data, EventSink<String> sink) {
      sink.add("data:$data");
      sink.add("data2:$data");
    });
    streamData4.transform(transformer).listen(print);
    

    最后梳理一下Stream与Future的异同
    StreamFuture是Dart异步处理的核心API。Future只能表示一次异步获得的数据,而Stream表示多次异步获得的数据,比如界面上的按钮可能会被用户点击多次,所以按钮上的点击事件(onClick)就可有理解为一个Stream
    Stream是流式处理,比如IO处理的时候,一般情况是每次只会读取一部分数据(具体取决于实现)。这和一次性读取整个文件的内容相比,Stream的好处是处理过程中内存占用较小
    来对比分别使用StreamFuture实现读文件的两种写法

    Future<String> readText() async {
      File file = new File("1.txt");
      return await file.readAsString();
    }
    
    void readText2() {
      File file = new File("1.txt");
      Stream<List<int>> stream = file.openRead();
      stream.transform(utf8.decoder).transform(LineSplitter()).listen(
          (String element) {
        print(element);
      }, onError: (dynamic error) {
        print("onError");
      }, onDone: () {
        print("onDonw");
      });
    }
    

    9. 库

    Dart的库管理比Java和Kotlin都要强大很多

    导入dart库里面的包

    import 'dart:math';
    

    导入项目为DartDemolib目录下的包

    import 'package:DartDemo/PrivateLibrary.dart';
    

    导入相对路径下的包

    import '../src/SrcLibrary.dart';
    

    解决变量名冲突的办法是将引入的库加上别名。这个跟Kotlin的处理方式是一样的

    import '../lib/PrivateLibrary.dart' as Private2;
    

    不完全导入。只导入showFunction函数

    import 'package:DartDemo/ShowLibrary.dart' as ShowLibrary show showFunction;
    

    不完全导入。只导入除hideFunction函数之外的所有函数

    import 'package:DartDemo/HideLibrary.dart' as HideLibrary hide hideFunction;
    

    库的拆分
    Part2Library.dartPartLibrary.dart的一部分

    // Part2Library.dart
    
    part of 'PartLibrary.dart';
    
    void part2LibraryFunction() {
      print("Part2LibraryFunction");
    }
    
    // PartLibrary.dart
    
    part 'Part2Library.dart';
    

    part中,import进来的库是共享命名空间的,所以我们没有再导入Part2Library.dart

    import 'package:DartDemo/PartLibrary.dart';
    

    延迟加载

    import 'package:DartDemo/DeferredLibrary.dart' deferred as deferredLibrary;
    

    我暂时还没有体会到延迟加载与非延迟加载在使用上有何区别

    void deferred() async {
      await deferredLibrary.loadLibrary();
      deferredLibrary.deferredFunction();
    }
    

    可以通过重新导出部分库或者全部库来组合或重新打包库,一个库管理提供多个库的导入支持。这个在库的管理上比较省心

    import "dart:convert";
    export "dart:convert";
    
    void reExportingFunction() {
      print("reExportingFunction");
    }
    
    import 'package:DartDemo/ReExportingLibrary.dart';
    

    我们来具体说一下第三方库如何导入
    首先找到这个pubspec.yaml文件,这个如同我们Android的build.gradle,所有导入的文件(图片等)还有版本库都在里面管理

    这里有我们项目的一些信息,Dart SDK的版本,第三方的依赖包版本

    第三方库一般在dartlang里寻找
    比如我要找dio这个网络请求框架,我可以搜索它

    可以在里面看到它目前的版本号是1.0.12,要使用它的话我们可以在dependencies里添加描述

    dependencies:
      dio: '1.0.12'
    

    有时候为了让Dart自己寻找最适合我们项目的版本,你可以写上any。这个在包版本冲突上算是比较好的解决方案

    dependencies:
      dio: any
    

    最后在右上角点击Get dependencies导入

    你可以在pubspec.lock文件里查看到项目使用的某个库的版本

    同样我们可以对包版本范围进行限制:指定一个最小和最大的版本号
    这表示在2.x.x版本都是支持的,但是必须要大于2.1.0

    '>=2.1.0 <3.0.0'
    

    还有一种是指定最小版本,比其大的都支持

    english_words : ^3.0.0
    

    包下载成功会有如下显示

    使用很简单,导入即可

    import 'package:dio/dio.dart';
    
    Dio dio = new Dio();
    dio.get("https://www.baidu.com/").then((Response<dynamic> response) {
      print(response.data);
    });
    

    至此,所有Dart的基本概念简单介绍完了。若有不清楚的可以私信或者添加评论,有时间我会来跟你讨论的

    参考文章

    为什么 Flutter 会选择 Dart ?
    flutter实战5:异步async、await和Future的使用技巧
    Dart与消息循环机制[翻译]
    理解Dart 异步事件流 Stream
    Dart 语法要点汇总

    相关文章

      网友评论

        本文标题:给Java开发者的Flutter开发基础---Dart语言

        本文链接:https://www.haomeiwen.com/subject/ngrdkqtx.html