Flutter实战指南:Java程序员如何快速上手Dart语言

发表时间: 2021-01-24 18:37

作为一个Java从业者,并且之前对Flutter甚至安卓、IOS开发无任何经验,我花了两周多的时间基本完成一个Flutter APP的开发;从这些天的开发过程来看,Flutter的语法对于Java程序员来说应当是比较容易理解和接受的,可能也是因为我对Vue等前端开发也算比较熟悉的原因罢。

Flutter使用的是Dart语言,其本身是基于Dart封装好的一套SDK;因此在真正使用或学习Flutter之前,建议还是先对Dart的语法进行一个大概的学习,这样更加有助于提升开发效率,避免多走弯路。

我会主要基于我做这个项目的过程来做一个总结,先对Dart语法与Java不一样的地方进行一些分析,然后再针对Flutter的一些知识及容易出问题的地方进行总结,当然由于我本身目前只是做了一这个Flutter的项目,对于Dart及Flutter的理解上可能会有不足,欢迎大家一起讨论。后续我也会针对自己新的理解对这一系列的文章做更新。

本文主要是从Java与Dart的不同点出发,来讲解Dart语言的一些语法与特性,感兴趣的可以继续阅读。

1. 关键字

Flutter关键字(相对Java)

其它与Java多数是类似的,或者不常使用,使用到时请查阅相关资料。

2. 数据类型

2.1 基础数据类型对应关系(常用)


基础数据类型对应

说明:

包装类型:Dart没有包装类型,int等类型即相当于Java对应的int类型又相当于其对应的包装类型Integer,因此,我们使用以下语法也是可以的:

int i = 0;  print(i.toString());  

我们看到i是int类型,可以直接通过i.的方式调用一些数据类型的方法;

同样,以下语法也是正确的:

print(1.toString());
  • 数字类型与字符串不能直接相加(而这在Java中是允许的),如以下方式会报错:
  • String a = 1 + 'test';  

    必须使用以下方式:

    String a = 1.toString() + 'test';  

    Extension扩展:我们可以通过extension关键字来给数字类型或者Dart中的其它类型扩展方法,如在我的项目中,有一些图片地址需要拼接,因此可以往给String定义一个get方法:

    extension StringExtension on String {  String get img {    var image = this;    if (image.isEmpty) {      return image;    }    if (!image.startsWith("http")) {      image = "http://file.ttcn.vip" + image;    }    if (!image.contains("~")) {      image = image + "~webp";    }    return image;  }}

    然后在使用的地方,可以引入这个文件,然后直接通过"test".img这样的方式调用:

    import 'package:tt_app/common/extend/string_extend.dart';Image image = Image.network(type['image'].toString().img, width: 26.rpx, height: 26.rpx, fit: BoxFit.contain); 

    同理,我们也可以给Widget增加扩展,这个涉及到Flutter的相关组件,后续再细述。

    2.2 集合类

    Dart中集合类主要使用List、Map;

    List

    相当于Java中的List,他与Java中的List不一样的是,直接通过new的方式可以创建List对象,而Java必须要通过ArrayList等来实例化;

    List list = new List();  List list = [];  

    List也如java一样支持泛型:

    List<String> list = new List<>();List<dynamic> list = [];  

    注意上面的dynamic代表列表中可以放所有类型的元素。

    List主要有以下常用方法:

    List list = [1, 2]; // 计算长度print(list.length);  // 增加元素list.add(1); // 批量增加元素 List list1 = [3, 4];  list.addAll(list1); // 获取指定位置元素var item = list.elementAt(0);  item = list[0]; // 定位元素int idx = list.indexOf("test"); // 删除元素 list.remove("test"); list.removeAt(0); // 子数组 list.sublist(0, 2);  // 元素转换类型List<String> list = list.map((e) => e.toString()).toList();// 拼接String str = list.join(','); // 直接相加List list = list1 + list2;  // 遍历list.forEach((e) => print(e));// 上面写法也可以直接简化:list.forEach(print);// 其它。。。

    Map

    Map也与Java中的Map对应,与List一样直接通过New Map就可以完成实例化,如

    Map map = new Map();  Map<String, String> map1 = new Map();  Map<String, String> map2 = {}; 

    使用上与Java非常类似。

    Map主要用到他的赋值及取值,通过[]操作符进行:

    // 初始化Map map = {"test": "a"};   Map map1 = new Map();  // 元素赋值与取值map['test'] = 'a';  print(map['test']);  // 判断是否存在某个元素 bool b = map.containsKey('test');  bool b1 = map.containsValue('a');  // 批量增加map.addAll({"a": "b"});// 为空判断 bool b = map.isEmpty;  bool b1 = map.isNotEmpty; // 删除元素 map.remove('test'); // 其它... 

    3. 常用操作符

    3.1 为空判断 ??及?.

    为空判断主要使用??及?.两个操作符。

    ??用于表示如果前一部分值为空,那么表达式的结果为后部分的值,否则为这前一部分的值,如:

    String i; String b = i??'test';  // 值为test  String i1 = 'a'; String b1 = i1??'test'; // 值为a  

    ?.表明如果对象不为空,则执行对象的方法,如

    int i;  print(i?.toString());  // 打印null int i = 1; print(i?.toString()); // 打印1

    很多时候,我们可以将两者结合起来,处理为空时赋默认值的情况:

    int i;print(i?.toString()??'test');

    3.2 级联运算符 ..

    有时候我们希望连续调用一个对象的多个方法,按常规方式我们可以这样处理:

    class Test {  void test() {    print('test');  }  void test1() {    print('test1');  }  void test2() {    print('test2');  }}main() {  Test test = new Test();  test.test();  test.test1();   test.test2(); }

    这种方式可以通过..操作符来进行简化:

    main() {  Test test = new Test();  test    ..test()    ..test1()    ..test2();}

    又能够少敲点代码了。

    4. 函数

    4.1 命名函数

    java中的函数必须要在类中定义(或者匿名方式定义),而dart的函数更类似于js的函数,可以脱离类来进行定义。如我们可以创建一个test.dart的文件,里面直接写函数的实现:

    void test() {    print('test');  }

    在需要使用的文件中引入该函数所在的文件就可直接使用:

    import 'package:test.dart';main() {    test();  }

    4.2 匿名函数

    函数也可以采用匿名方式定义,如:

    // 接收参数类型为函数void test(a) {    a();  }main() {    test(() => print('test'));          // 也可以不使用=>    test(() {        print('test');      });}

    注意上面的=>与{},=>用于函数体中只有一条语句的情况;如果有多条语句必须使用{},而且不能使用=>。

    如果是有返回值,=>会将表达式的结果直接当成返回值,如:

    void test(a) {  print(a());}main() {  test(() => 'test');  // 与下面是等价的  test(() {    return 'test';  });}

    注意看到test方法接收的参数是a,如果我们传入a不是函数,在编译时不会出错,但运行时会出错,如:test('test'); 会报以下异常:Unhandled Exception: NoSuchMethodError: Class 'String' has no instance method 'call'.

    因此我们定义函数的时候,最好带上参数的类型:

    void test(Function a) {  print(a());}main() {  test(() => 'test');  // 也可以不使用=>  test(() {    return 'test';  });  // 这句在编辑器中就会直接提示异常   test('test');}

    4.3 带参函数原型定义 typedef

    使用Function限制类型后,有些场景还无法满足,假设我们test方法接收的函数a有一个必须的参数,如:

    void test(Function a) {    print(a('test'));    }

    如果我们还是跟上面一样的调用:

     test(() {    return 'test';  });

    编辑器不会报错,但运行的时候会报错。

    这时候我们可以通过typedef 来定义带参的函数,如:

    typedef String Test(String str); // typedef 后的String 表示的是函数的返回类型,括号中的是函数的入参。void test(Test a) {  print(a('test'));}main() {  test((str){    return str;   });}

    这个时候我们调用的时候传的函数不带参,那么编辑器就会直接提示错误。

    注意定义匿名函数时,参数前后的()是不可省略的,即使函数没有任何入参。

    4.4 函数其它说明:

    关于返回类型,可以写成具体类型或不写;如void test(){}或者test(){},注意不写时有些错误在编译时检查不出来。因此建议能带类型的时候还是带上类型。

    可变参数:可以通过[]来限定,如:

    void test([String a, String b]) {  print(a);  print(b);}main() { test();   // 打印两个null test('1'); // 打印1 null test('1', '2');  // 打印1 2  }

    命名可变参数:如果可变参数数量极多,可以通过大括号来定义:

    void test({String a, String b}) {  print(a);  print(b);}

    调用时可以按key: value的方式传入,如:

    main() { test();   // 打印null test(a: '1'); // 打印1 null test(b: '2');  // 打印null 2   test(a: '1', b: '2'); // 打印1 2  }

    需要注意的是,如果函数同时有固定参数及可变参数,固定参数需要在可变参数前传入,如:

    void test(String test, {String a, String b}) {  print(test);  print(a);  print(b);}test(); // 会报错 test('test'); // 不会报错,打印test null null  test('test', a: 1); // 打印test 1 null test('test', b: 2); // 打印test null 2 test('test', a: 1, b: 2); // 打印test 1 2 

    如果想给可变参数指定默认值怎么?这时候可以结合:或者=操作符处理:

    void test({String a = '1'}) { // 也可以写成void test({String a: '1'}) {}  print(a);}main() { test(); // 打印1   test(a: "2"); // 打印2  }

    {}传参的方式简直太好用了,尤其是在后续的Flutter开发过程中,我们也会发现Flutter大量组件采用了这种方式。

    注意可变参数的使用过程中,如果参数未定义默认值,那么需要进行非空判断,这时候就可以使用上文所说的?.及??操作符来处理了。

    5. 对象

    5.1 定义

    dart中的对象通过class 关键字进行定义。如:

    class Test {  String test;        void execute() {      print(test);    }}void main() {    Test test = new Test();      test.test = '1';      print(test.test);      test.execute();  }

    定义好对象后我们就可以通过New的方式进行实例化了。实际上,new关键字可以省略:

    Test test = Test();  

    5.2 私有成员变量

    注意Dart中没有public /private/protected等关键字;上面我们按常规定义的变量,在外部是可直接通过实例.的方式访问的。如上面的test.test,即可通过test.test获取其值,也可以使用test.test = '1'; 的方式对其进行赋值。

    那如果我们想要达到Java中的private同样的效果如何处理?只需要在变量前面添加下划线即可:

    class Test {  String _test;        void execute() {      print(_test);    }}

    此时定义的变量只能在类内部使用,外部无法访问到。

    注意变量的作用域,如果是在文件中定义的私有变量,那么可以在文件中使用;类中定义的只能在类中使用;

    如果想要在外部对私有变量进行修改与读取,就只能提供相关的Setter与Getter方法了:

    class Test {  String _test;  get test {    return _test;  }    set test(String test){    this._test = test;    }}main() {  Test test = Test();    test.test = 'b';   print(test.test);  }

    同样的,对于内部使用的方法不想要暴露的,也可以将方法名设置成以下划线开头,这样外部调用便看不到这个方法了。

    5.3 构造函数

    常规定义

    同样的Dart中的类也包含有构造函数,常规的构造函数与Java中一样:

    class Test {  String _test;  Test(String test) {    this._test = test;    }}

    实际上Dart有更加简洁的语法达到上面的效果:

    class Test {  String _test;  Test(this._test);  }

    上面直接通过this.成员变量的方式来表示将传入的参数值赋给对应的成员变量。

    与可变参数结合

    构造函数也可以与上文所说的可变参数结合,如:

    class Test {  String _test;  String b;   String a;    Test(this._test, {this.a, this.b});}

    在实例化的时候我们就可以通过可变函数调用的方式进行实例化:

    main() {  Test t1 = Test("test");    Test t2 = Test('test', b: 'b');    Test t3 = Test("test", a: "a", b: "b");}

    注意可变参数的方式,成员变量不能是私有的,也就是不能以下划线开头。这个时候如果我们想要限制外部修改,就需要使用final关键字来限制了,如:

    class Test {  String _test;  String b;  final String a;  Test(this._test, {this.a, this.b});}

    这样定义后,成员变量a就只能在初始化的时候通过构造函数赋值,而不能在实例化后通过实例.a的方式修改其值。

    命名构造函数

    可以通过类名.的方式使用命名构造函数,如:

    class Test {  String _test;  String b;  final String a;  Test(this._test, {this.a, this.b});  Test.a({this.a}) {    _test = "test";    }}main() {  // 实例化时就可以通过以下方式进行实例化:  Test test = Test.a(a: "a");  }

    注意如果我们没有定义构造函数,那么会包括一个默认无参的构造函数;如果定义了,那么这个无参构造函数就不存在了(与Java是一样的)。

    构造时成员变量赋值

    我们可以在构造函数后通过冒号来为成员变量赋值:

    class Test {  String _test;  String b;  final String a;  Test({this.a})      : _test = "test",        b = "b";}

    也可以使用构造函数传入的参数为成员变量赋值:

    class Test {  String _test;  String b;  Test(Map params)      : _test = params['test'],        b = params['b'];}


    重定向构造函数

    可以通过重定向构造函数来在一个构造函数中调用另外一个构造函数进行实例化:

    class Test {  String _test;  String b;  Test(Map params)      : _test = params['test'],        b = params['b'];  Test.a():this({"test": "test", "b": "b"});}

    5.4 继承、重载等

    基础的使用与Java中很类似,不再展开。

    6. 异步处理

    Dart中的异步处理通过Future进行。Future与TS中的Promise有几分类似。

    6.1 典型应用场景

    一个典型的场景:

    Future<bool> test() {  return Future(() {    sleep(Duration(seconds: 10)); /// 模拟耗时操作        return true;  });}main() {  test().then(print)  .catchError((err) {    print(err);  });  print('1');}

    当我们要异步处理一个耗时很长的操作时(如网络请求),就可以通过以上方式,使用Future来包装住这个耗时过程。这样test方法就会返回一个Future对象,后续我们可以通过then方法实现耗时操作完成时的处理,通过catchError来处理异常。

    6.2 等待异步完成

    我们可以通过Future的then方法来执行在耗时操作完成后需要进行的处理;但某些情况下我们可能要等待这个异步处理完成再继续进行后续的处理; 等待异步操作完成,可以使用await关键字,如:

    Future<bool> test() {  return Future(() {    sleep(Duration(seconds: 10));    /// 耗时操作    return true;  });}void test1() async {  bool b = await test();  // 等待耗时调用完成  print(b);  }main() {  test1();  }

    注意看到我们在test1该当中通过await 来等待test方法执行完成,经过await关键字修饰后,会同步等待test方法执行完成并将表达式的结果赋给指定变量。

    这样在test1方法中就完成了等待test方法执行完成然后处理结果的处理。

    大家可能会注意到test1方法后面跟了一个async关键字。这个关键字是告诉调用方,在调用这个方法的时候自动异步处理,不需要等待这个方法执行完成。await关键字必须使用在被async修饰的方法中,如我们直接在main方法中调用await,编辑器会告诉我们不合法!这也是避免主线程被耗时操作耗死。在UI中这种基本算是一种常规处理了,否则界面会出现假死现象!

    7. 其它

    7.1 插值表达式

    如果我们想将一个变量与其它的一些常量字符串进行拼接,常规方式下我们一般会这么写:

    String test = 'a';  print('A的值:' + test);  

    使用插值可以通过$来在拼接的串中直接使用变量,如:

    String test = "a"; print('test的值:$test'); // 也可以使用表达式int a = 10;  print('A的值:${a.toString()}');  

    7.2 var语法

    var 与js的var类似,表明是一个不确定类型;注意var在赋值后再改变其数据类型就会报错,这点与js是不一样的,如:

    var i = 1;  i = 'test'; // 报错

    多数情况下,我们知道某个变量的类型的时候,不建议使用var,而是直接使用变量的类型如String或者int;最好将var仅用于那些不确定类型的情况,如一个典型的情况,如果map中存储的类型不确定,也有可能要找的key对应的Value为空,那么可以通过以下方式处理:

    Map map = ...; var item = map['field'];  String str = item?.toString(); 

    7.3 print

    打印,相当于Java中的System.out.println(),也相当于JS中的console.log();