标签: Flutter

  • 「编程笔记」Dart中重新加载FutureBuilder的一种方法

    在实战过程中,我们常常遇到需要刷新界面的需求,比如搜索时出现了网络错误,我们为用户提供一个Retry按钮,用户点击后,我们希望整个FutureBuilder重新加载一下,比如下图:

    本页面通过一个FutureBuilder加载搜索结果,同时遇到网络错误时显示以上界面,代码的大致结构如下:

    Widget build(BuildContext context) {
    Widget build(BuildContext context) {
      return FutureBuilder(
    
        future: getSearchResult(),
        builder: (context, snapshot) {
          // Show user the data
          if (snapshot.hasData) {
            return showDataPage();
          } else if (snapshot.hasError) {
            if (isNetworkError(snapshot.error)) {
              return NetworkErrorPage();
            } else {
              return UnknownErrorPage();
            }
          }
          return LoadingPage();
        },
      );
    }

    这时,假设我们的NetworkErrorPage的位置需要添加一个按钮,实现FutureBuilder重新刷新,一种简单的方法是直接调用该FutureBuilder所在界面的setState()方法,每当FutureBuilder的父WidgetsetState()方法被调用,都会使得FutureBuilder重新获取异步数据,代码大致如下:

    Widget build(BuildContext context) {
      return FutureBuilder(
        builder: (context, snapshot) {
          // Show user the data
          if (snapshot.hasData) {
            return showDataPage();
          } else if (snapshot.hasError) {
            if (isNetworkError(snapshot.error)) {
              return ElevatedButton(
                onPressed: () {
                  setState(() {});
                },
                child: Text('Try Again'),
              );
            } else {
              return UnknownErrorPage();
            }
          }
          return LoadingPage();
        },
      );
    }

    但如果我们按照上述结构实现刷新的工作,我们会发现,虽然在我们点击按钮之后,刷新的工作有在进行,但是页面并不会在点击Try Again按钮后重新回到LoadingPage界面,而是等到加载好之后直接更新界面为showDataPage或者Error的相关界面,这是因为snapshot的hasDatahasError变量仅仅在每次future任务完成后才会刷新。

    如果我们想实现点击Try Again按钮之后实时回到LoadingPage界面的话,可以使用snapshot.connectionState进行判断,具体代码如下:

    FutureBuilder(
        future: _future,
        builder: (context, snapshot) {
          if (snapshot.connectionState != ConnectionState.done) {
            return _buildLoader();
          }
          if (snapshot.hasError) {
            return _buildError();
          }
          if (snapshot.hasData) {
            return _buildDataView();
          }     
          return _buildNoData();
    });

    通过以上方法,即可实现点击按钮后立即回到加载界面,直到下一次加载完成之后再次更新界面的效果。

  • 「编程笔记」关于Dart类构造函数

    构造函数的形式

    无参数构造函数

    在Dart中,每一个类(Class)都有一个不包含任何参数的默认构造函数,当你调用[ClassName]()时,就会调用默认的构造函数。Dart会为每个类自动添加默认的构造函数,但你也可以显式的声明你的构造函数,例子如下

    class A {
      String? name;
      // Constructor
      A() {
        name = 'classA';
      }
    }
    void main() {
      A aIns = A();
      print(aIns.name); // classA
    }
    

    上面的构造函数被调用时,会更新实例的成员变量。

    同时注意到在声明成员变量name的时候,我们使用了?符号,代表name的值是允许为空的,如果删除?符号,本段代码将会报错,编译器会提示你没有在类初始化的时候为name这个成员变量赋值,报错提示如下:

    Non-nullable instance field 'name' must be initialized.
    Try adding an initializer expression, or add a field initializer in this constructor, or mark it 'late'.dartnot_initialized_non_nullable_instance_field

    其中一个解决办法是,在声明成员变量name的时候使用late关键字:late String name; 这么做相当于你告诉编译器,我现在暂时可能没有对name变量进行赋值,但是我确定在将来我要使用它之前,肯定会给他赋值,只不过不是现在。这样,编译器就不会强制要求你在构造时立即初始化这个变量。

    但这时可能有同学会问:“我明明在A的构造函数中已经为成员变量A赋值了classA,为什么说我没有为name赋值?”,这里需要注意的是,如果我们想要让Dart编译器知道我们已经在构造函数中初始化了某个成员变量,就需要另一种写法。

    带参数构造函数

    class A {
      String name;
      // Constructor
      A(this.name) {}
      // Also could be write as:
      // A(this.name);
    }
    void main() {
      A aIns = A('hi');
      print(aIns.name); // hi
    }
    

    当然,上面代码中的构造函数已经不属于无参数构造函数了,其构造参数中包含一个位置变量。当然,你也可以为其添加命名变量。

    有两点需要提及一下,Dart允许类的构造函数中,快速的对成员变量进行赋值,要做到这一点,只需要使用this关键字即可,比如上方代码中的构造函数A(this.name)就代表传入的第一个位置参数赋值给name这个成员函数。同样的,您也可以在命名参数中使用this,比如A({this.name}); 这种情况下,调用构造函数的格式变为 A(name: 'YOUR_NAME_HERE')

    命名构造函数

    我们可以发现,上方提到的两种构造函数中,构造函数都是直接使用类的名称,比如类的名称是Book,那么构造函数的名称也是Book,这在Dart中属于 unnamed constructor(未命名构造函数),这种构造函数可以直接用类名调用,比较方便,但是一个类只能有一个未命名的构造函数,这里涉及到Dart语言的设计,Dart语言的设计已经决定了Dart不支持方法/函数重载,也就是说两个名称相同但是输入的参数列表不同的函数不允许同时出现。因此,构造函数显然也不能通过不同类型的输入重载,您可以阅读关于Dart不支持方法重载的相关文章,加深理解。

    这里就需要介绍Dart的命名构造函数了。就如其名字一样,命名构造函数允许你设定这个构造函数的名字,进而可以实现多个不同的构造函数,代码如下

    class A {
      late String name;
      A.fromNumber({required int number}) {
        name = number.toString();
      }
      A.fromString({required this.name});
    }
    void main() {
      A aIns = A.fromNumber(number: 114514);
      print(aIns.name); // 114514
      aIns = A.fromString(name: 'string');
      print(aIns.name); // string
    }
    

    注意,子类不会继承父类的命名构造函数,如果您想要子类在初始化的时候调用父类的命名构造函数,则需要手动进行调用super.[yourNamedConstructor]()

    工厂构造函数

    在实际开发过程中,有时我们希望一个类的构造函数并不是每次都返回一个新构造的示例,比如,有时我们希望从内存中读取已有的示例,或者是我们想返回该类的某个子类示例,此时可以运用factory关键字实现工厂构造函数,工厂构造函数可以返回此类或者此类的子类的示例。

    class Person {
      String name;
      factory Person.fromSex(String sex, String name,
          {int salary = 0, int beautyIndex = 0}) {
        if (sex == 'male') {
          return Male(name, salary);
        } else if (sex == 'female') {
          return Female(name, beautyIndex);
        }
        return Person(name);
      }
      Person(this.name);
      void printInfo() {
        print('name: $name');
      }
    }
    class Male extends Person {
      int salary;
      Male(super.name, this.salary);
      @override
      void printInfo() {
        super.printInfo();
        print('salary: $salary');
      }
    }
    class Female extends Person {
      int beautyIndex;
      Female(super.name, this.beautyIndex);
      @override
      void printInfo() {
        super.printInfo();
        print('beautyIndex: $beautyIndex');
      }
    }
    void main() {
      var person = Person.fromSex('female', 'Linda', beautyIndex: 5);
      print(person.runtimeType);
      person.printInfo();
    }
    // Output:
    // Female
    // name: Linda
    // beautyIndex: 5

    值得注意的是,工厂构造函数不得访问this,也就是说工厂函数不能直接访问成员变量。如果你想在工厂构造函数中返回本类实例,可以先在工厂构造函数中构建实例,然后返回你新构建的实例。

    其实在这里,目前我自己也存在着一定的疑问,比如,虽然factory构造函数可以返回内存中的实例或者是子类的实例,但是,实际操作过程中,即使返回的是子类实例,我们也无法直接访问子类实例的变量和函数,而还是只能访问父类的变量和函数。比如上述代码,即使我们可以发现最终person变量的runtimeTypeFemale,但是当我们尝试添加print(person.beautyIndex);这行代码的时候,编译器会报错,提示person实例没有beautyIndex成员变量。直观上来说,大概是编译器因为Person.fromSex()方法返回的是Person类的变量,所以后续的类型推断和错误检查都会以Person类为基础。这么做也有道理,因为Person.fromSex()有可能返回的是Person类自己的实例。有没有什么办法,既可以实现动态的返回子类型,同时又可以允许我们自由的读取子类型的变量呢?

    以下抛砖引玉的提供两个方法,第一个,也是最直接的方法,是在父类中增加子类所用到的成员变量,同时将其标记为可空,例如,上述代码中,可以在Person类中添加一行int? beautyIndex; 然后子类重载这个变量即可。这种方法显然不是很好,当子类越来越多,我们需要添加到父类的变量也就越来越多,这就意味着每次功能更新都需要修改父类。这不符合对修改关闭原则。

    另一种方法是进行类型检查(typecheck)和类型转换(type cast),也就是如果我们确定了工厂构造函数返回了某个子类的示例,我们可以将这个实例进行特定的类型转换,将其转换到某个子类。

    factory实现单例模式

    工厂构造函数除了上面的用法,还可以用于实现单例模式,代码如下

    class Single {
      static final Single _singleton = Single._internal();
      factory Single() {
        return _singleton;
      }
      Single._internal();
    }
    void main() {
      var a = Single();
      var b = Single();
      print(identical(a, b)); // true
    }
    

    通过以上特点,你可以通过class实现类似于但更方便于enum的效果,代码如下:

    class AppleDevice {
      static final iMac = AppleDevice._internal('iMac');
      static final macBook = AppleDevice._internal('Macbook');
      static final iPhone = AppleDevice._internal('iPhone');
      static final iPad = AppleDevice._internal('iPad');
      factory AppleDevice.fromDeviceType(String devideType) {
        if (devideType == 'pc') {
          return iMac;
        } else if (devideType == 'laptop') {
          return macBook;
        } else if (devideType == 'pad') {
          return iPad;
        } else {
          return iPhone;
        }
      }
      String _name;
      AppleDevice._internal(this._name);
    }
    void main() {
      AppleDevice a1 = AppleDevice.iMac;
      AppleDevice a2 = AppleDevice.iPhone;
      AppleDevice a3 = AppleDevice.fromDeviceType('pc');
      print(a1 == a2); // false
      print(a1 == a3); // true
    }
    

    上述代码通过首先通过staticfinal关键字,创建了不同的AppleDevice实例来当作不同的枚举类型使用,又通过factory函数,实现了根据不同的数据判断出需要的不同的“枚举类型”(实际上是一个AppleDevice实例)。这种方法不但实现了枚举的基本功能,后期还可以根据自己的需要不断的为其添加功能,扩展新好于Dart中的基本枚举类型。

    值得一提的是,Dart2.7更新之后,已经支持使用extensions on关键字对于枚举类型的功能扩展,您可以阅读Dart枚举类型扩展的相关的文章,了解extenstions的用法。但是毋庸置疑的是,当你需要一个多功能的枚举类的时候,使用class实现应该能更好的满足你。

    Dart类成员的初始化

    在Dart中,类成员的初始化一共有4种方法,分别是:

    • 在类的声明定义(Declaration)中进行初始化
    • 通过构造函数的参数进行初始化
    • 通过构造函数的初始化列表进行初始化
    • 在类的构造函数的定义内部进行初始化

    需要注意,最后一种方法只适用于非final类成员。

    类的声明定义中初始化

    你可以在编写Dart类的时候直接指定某个变量的值,代码如下:

    class A{
    int a = 10;
    }

    Dart构造函数的快捷用法

    初始化列表

    除了使用this关键字以外,Dart还允许您使用初始化列表对成员变量进行初始化,代码如下

    class A {
      late String name;
      late int id;
      A(String str, int number)
          : name = str,
            id = number;
    }
    void main() {
      A aIns = A('class a', 114514);
      print(aIns.name); // class a
      print(aIns.id); // 114514
    }
    

    指定父类构造函数

    默认情况下,在子类的构造函数没有指定调用之前,子类会调用父类的默认未命名构造函数,如果你想让子类指定使用父类的某个构造函数,并且需要传递参数,则可以在序列化列表之后选择特定的父类构造函数,代码如下:

    class A {
      late String name;
      late int id;
      A.fromData(String str, int number)
          : name = str,
            id = number;
    }
    class B extends A {
      int bId;
      B(int number)
          : bId = number,
            super.fromData('class b', 114514);
    }
    void main() {
      B ins = B(123);
      print(ins.name); // class b
      print(ins.id); // 114514
      print(ins.bId); // 123
    }
    

    如上,我们不但使用了上方所讲的初始化列表的语法,同时还添加了super.fromData(...) 这一行,而这一行的实际作用便是让B中的构造函数指定使用其父类(也就是A类)的fromData构造函数