Skip to content

flutter 常见面试题 ​

01. Flutter 与其它跨端框架的特点和优势: ​

  1. 快速开发:Flutter 提供了热重载(Hot Reload)功能,可以实时预览应用程序的更改,从而加快开发速度。开发人员可以迅速进行迭代和调试,无需等待重新编译。
  2. 单一代码库:使用 Flutter ,你只需要编写一套代码,即可同时构建 iOS 和 Android 平台的应用程序。这样可以节省开发时间和人力成本,并且简化了维护和更新的过程。
  3. 漂亮的用户界面:Flutter 提供了丰富的 UI 组件和内置的材料设计(Material Design)和苹果风格(Cupertino)的样式,使开发者能够轻松创建漂亮和响应式的用户界面。
  4. 高性能:Flutter 使用自己的渲染引擎,称为 Skia,可以直接绘制应用程序的 UI 。这意味着应用程序的性能更高,响应更快,并且可以保持平滑的动画和过渡效果。
  5. 深度定制:Flutter 允许开发者对每个平台进行深度定制。你可以直接访问平台特定的 API ,以满足应用程序的特定需求,并提供与原生应用程序相似的用户体验。
  6. 强大的开发工具和社区支持:Flutter 提供了丰富的开发工具和插件,如 Flutter DevTools 和 Flutter Inspector,帮助开发者进行调试和性能优化。此外,Flutter 拥有庞大的开发者社区,提供了大量的资源、教程和第三方库,可以加速开发过程。
  7. 广泛的应用领域:Flutter 不仅限于移动应用开发,还可以用于构建桌面应用程序和嵌入式设备应用程序。这使得 Flutter 在多个领域具有广泛的适用性和可扩展性。

02. Flutter 的架构层有哪些? ​

  • Embedder 嵌入器:作为基础层,它提供了平台特定的集成,使得 Flutter 能够在不同的系统上运行。
  • Engine 引擎:使用 C++编写,该层管理核心任务,如图形渲染、文本布局和文件/网络操作。
  • Framework 框架:位于引擎之上,为应用程序开发提供高级类。这包括小部件层,提供了大量的视觉、结构、平台和交互式小部件,渲染层将小部件绘制到画布上,以及提供服务和实用工具的其他几个层。

03. Flutter Cupertino 和 Material 是什么? ​

Flutter Cupertino 和 Material 是 Flutter 框架中的两个设计语言和视觉风格。

  1. Cupertino:Cupertino 是苹果公司的设计语言,用于构建 iOS 风格的用户界面。Flutter 提供了一套名为 "cupertino" 的包,其中包含了苹果风格的 UI 组件、图标和样式。使用 Cupertino,你可以创建具有 iOS 视觉效果和交互行为的应用程序,例如 iOS 设备上常见的滚动效果、导航栏样式和操作表。

  2. Material:Material 是 Google 的设计语言,用于构建现代、响应式和有吸引力的用户界面。Flutter 提供了名为 "material" 的包,其中包含了 Material Design 风格的 UI 组件、图标和样式。使用 Material,你可以创建具有 Material Design 视觉效果和交互行为的应用程序,例如漂浮按钮、卡片、阴影效果和标准的应用栏。

04. Flutter 支持哪些操作系统? ​

Flutter 是一个多功能的框架,支持在多种平台上部署:

  • Mobile 移动平台(Android,iOS)
  • Desktop 桌面平台(Linux,MacOS,Windows)
  • Web 网络浏览器(Chrome,Firefox,Safari 和 Edge)

05. Flutter JIT 和 AOT 之间有什么区别? ​

Flutter 中的 JIT(Just-in-Time)和 AOT(Ahead-of-Time)是两种不同的编译方式,用于将 Flutter 代码转换成可执行的机器代码。

  1. JIT(Just-in-Time)编译:在开发和调试阶段,Flutter 使用 JIT 编译方式。JIT 编译器将 Dart 代码转换为中间代码(IL),然后在运行时动态地将中间代码转换为机器代码。这种编译方式允许热重载(Hot Reload)功能,开发者可以在不重新启动应用程序的情况下即时查看代码更改的结果。JIT 编译器还提供了更快的开发周期和更快的编译时间,但相对而言,生成的代码执行速度可能较慢。

  2. AOT(Ahead-of-Time)编译:在发布到生产环境时,Flutter 使用 AOT 编译方式。AOT 编译器将 Dart 代码预先编译为机器代码,生成二进制文件,无需在运行时进行即时编译。这种编译方式提供了更快的启动时间和更高的执行性能,因为代码已经编译成机器代码,无需再进行运行时的转换。但与 JIT 编译相比,AOT 编译不支持热重载功能,并且可能导致较长的编译时间。

JIT 编译方式适用于开发和调试阶段,提供了更快的开发周期和热重载功能,但执行速度可能较慢。而 AOT 编译方式适用于发布到生产环境,提供了更快的启动时间和更高的执行性能,但不支持热重载功能。在开发过程中,开发者可以充分利用 JIT 编译的便利性和开发速度,而在发布时则可以选择 AOT 编译以获得更好的性能和用户体验。

06. Dart 语言 final 和 const 有什么不同? ​

在 Dart 语言中,final 和 const 是用来声明常量的关键字,但它们有一些不同之处:

  1. final:final 用于声明一个只能被赋值一次的变量。这意味着一旦变量被赋值后,其值就不能再被修改。final 变量在运行时被初始化,可以根据需要进行延迟初始化。final 变量的值可以是在运行时计算得到的结果,但一旦初始化后,就不能再改变。

例如:

dart
final int x = 5;
final String name = 'John';

// 错误的用法,final 变量不能再次赋值
x = 7;
  1. const:const 用于声明一个编译时常量,这意味着变量的值必须在编译时就已知且不可更改。const 变量在编译时被初始化,可以在运行时之前进行优化。const 变量的值必须是编译时常量,如字面量、常量构造函数创建的对象或其他 const 变量的组合。

例如:

dart
// 正确的用法
const int x = 5;
const String name = 'John';

// 错误的用法,const 变量不能再次赋值
x = 7;

final 用于声明运行时常量,其值在运行时初始化且不能更改,而 const 用于声明编译时常量,其值在编译时初始化且不能更改。const 变量的使用更加严格,要求值必须在编译时就已知,因此适用于需要在编译时进行优化和确定的场景。而 final 变量则更适用于需要在运行时确定并且不可更改的常量。

07. Dart 中有哪些访问修饰符? ​

在 Dart 中,有以下几种访问修饰符:

  1. 默认访问修饰符(No modifier):如果没有显式地指定访问修饰符,则默认为包内可见(package-private),即同一个包内的其他文件可以访问。

  2. public:在 Dart 中,默认情况下,所有的成员(变量、函数、类等)都是公开的,即可在任何地方访问。公开成员不使用任何访问修饰符进行标识。

  3. _private:使用下划线 _ 开头的标识符表示私有成员,只能在当前文件中访问。私有成员在其他文件中是不可见的。

例如,下面是一个示例类,演示了访问修饰符的使用:

dart
class Person {
  String name; // 默认访问修饰符,默认为包内可见

  int _age; // 私有成员,只能在当前文件中访问

  void sayHello() {
    print('Hello, $name!');
  }

  int _calculateAge() {
    // 私有方法,只能在当前文件中访问
    // ...
  }
}

在上述示例中,name 是一个默认访问修饰符的成员,可以在同一个包内的其他文件中访问。_age 是一个私有成员,只能在当前文件中访问。sayHello() 是一个公开的方法,可以在任何地方访问。_calculateAge() 是一个私有方法,只能在当前文件中访问。

需要注意的是,Dart 中没有像 Java 那样的 public 和 private 关键字来显式地标识访问修饰符。默认情况下,成员是公开的,使用下划线 _ 开头的标识符表示私有成员。

08. Dart 语言 命名参数、可选参数 是什么? ​

在 Dart 语言中,命名参数(Named Parameters)和可选参数(Optional Parameters)是用于定义函数接受参数的方式。

  1. 命名参数:命名参数允许你通过指定参数名称来传递参数值,而不必按照参数定义的顺序传递。使用大括号 {} 包围参数名称,并在函数调用时使用 参数名: 参数值 的形式进行传递。

    dart
    void printPerson({String name, int age}) {
      print('Person: $name, $age years old');
    }
    
    // 使用命名参数调用函数
    printPerson(name: 'John', age: 30);
    printPerson(age: 25, name: 'Alice');

    在上述示例中,printPerson 函数接受两个命名参数 name 和 age。通过使用参数名称来传递参数值,可以不受参数顺序的限制。

  2. 可选参数:可选参数允许你定义函数接受可选的参数,可以在函数调用时省略该参数。可选参数分为两种类型:位置参数和命名参数。

    • 位置参数:使用中括号 [] 包围参数名称,表示该参数为可选的位置参数。位置参数在函数调用时按照参数定义的顺序进行传递。
    dart
    void printMessage(String message, [String prefix]) {
    if (prefix != null) {
      print('$prefix: $message');
    } else {
      print(message);
    }
    }
    
    // 使用位置参数调用函数
    printMessage('Hello'); // 无前缀
    printMessage('World', 'Prefix'); // 带前缀
    • 命名参数:使用大括号 {} 包围参数名称,表示该参数为可选的命名参数。命名参数在函数调用时通过指定参数名称进行传递。
    dart
    void printPerson(String name, {int age, String address}) {
    print('Person: $name, $age years old, $address');
    }
    
    // 使用命名参数调用函数
    printPerson('John', age: 30, address: '123 Main St');
    printPerson('Alice', address: '456 Park Ave', age: 25);

    在上述示例中,printMessage 函数接受一个位置参数 message 和一个可选的位置参数 prefix。printPerson 函数接受一个位置参数 name 和两个可选的命名参数 age 和 address。

通过使用命名参数和可选参数,可以使函数的调用更加灵活和可读性更高。可以根据需要选择使用命名参数或可选参数,或者同时使用它们来定义函数接受的参数。

09. Dart 语言命名构造函数和工厂函数之间有什么区别? ​

在 Dart 语言中,命名构造函数(Named Constructors)和工厂函数(Factory Constructors)是两种用于创建对象的不同方式,它们有以下区别:

  1. 命名构造函数:命名构造函数是在类中定义的特殊构造函数,通过使用类名后跟一个句点和构造函数名称来定义。命名构造函数用于提供不同的构造方式或创建具有特定初始化逻辑的对象。

    dart
    class Person {
      String name;
      int age;
    
      // 默认构造函数
      Person(this.name, this.age);
    
      // 命名构造函数
      Person.fromBirthYear(this.name, int birthYear) {
        age = DateTime.now().year - birthYear;
      }
    }
    
    // 使用默认构造函数创建对象
    var john = Person('John', 30);
    
    // 使用命名构造函数创建对象
    var alice = Person.fromBirthYear('Alice', 1995);

    在上述示例中,Person 类定义了一个默认构造函数和一个命名构造函数 Person.fromBirthYear。默认构造函数用于直接传递 name 和 age 参数创建对象,而命名构造函数 Person.fromBirthYear 接受 name 和 birthYear 参数,并通过计算得到 age 值。

  2. 工厂函数:工厂函数是通过使用 factory 关键字定义的特殊构造函数,用于创建对象的灵活方式。工厂函数可以返回一个新的对象,也可以返回一个已存在的对象。工厂函数通常用于创建单例对象或根据特定条件决定返回哪个对象。

    dart
    class Logger {
      String name;
      static Logger _instance;
    
      // 私有构造函数
      Logger._internal(this.name);
    
      // 工厂函数
      factory Logger(String name) {
        if (_instance == null) {
          _instance = Logger._internal(name);
        }
        return _instance;
      }
    }
    
    // 使用工厂函数创建对象
    var logger1 = Logger('Logger 1');
    var logger2 = Logger('Logger 2');

    在上述示例中,Logger 类定义了一个工厂函数 Logger,用于创建 Logger 对象。工厂函数通过判断是否已存在对象,来决定返回一个新的对象或一个已存在的对象。这种方式可以实现单例模式,确保只有一个 Logger 对象被创建。

命名构造函数用于提供不同的构造方式或初始化逻辑,而工厂函数用于提供创建对象的灵活方式,可以返回新的对象或已存在的对象。

10. 面向对象编程(OOP)的四个原则是什么? ​

面向对象编程(OOP)的四个基本原则是抽象(Abstraction)、封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism)。这些原则被称为「抽象、封装、继承、多态」(Abstraction, Encapsulation, Inheritance, Polymorphism)或「AEIP 原则」。

这些原则是面向对象编程的基础,它们用于指导设计和组织代码的方式,以实现代码的可维护性、可扩展性和重用性。

  1. 抽象(Abstraction):抽象是将复杂的现实世界问题简化为适合程序处理的模型。通过抽象,我们可以关注对象的关键特征和行为,忽略其细节。抽象可以通过类、接口和抽象类来实现。

  2. 封装(Encapsulation):封装是将数据和操作数据的方法封装在一个单元(类)中,以实现信息隐藏和访问控制。封装通过将相关的数据和方法组织在一起,形成一个独立的模块,并限制外部访问来保护数据的完整性。

  3. 继承(Inheritance):继承是通过创建新的类(子类)来继承现有类(父类)的属性和方法。继承可以实现代码的重用和层次化的组织。子类可以继承父类的属性和方法,并可以添加新的属性和方法,或者重写父类的方法。

  4. 多态(Polymorphism):多态是指同一个方法可以在不同的对象上具有不同的行为。多态允许使用基类或接口类型的引用来引用具体的子类对象,从而实现动态绑定和灵活的代码扩展。

11. 你能提供一下 SOLID 原则的概述吗? ​

SOLID 是由五个单独的原则组成,每个原则都关注不同的方面,但它们共同促进了高内聚、低耦合的代码结构。

以下是每个 SOLID 原则的概述:

  1. 单一职责原则(Single Responsibility Principle,SRP):一个类应该只有一个引起它变化的原因。换句话说,一个类应该只负责一项单一的职责。这样设计的类更容易理解、维护和扩展。

  2. 开放封闭原则(Open-Closed Principle,OCP):软件实体(类、模块、函数等)应该对扩展开放,而对修改关闭。通过使用抽象、接口和多态性,可以在不修改现有代码的情况下扩展系统的功能。

  3. 里氏替换原则(Liskov Substitution Principle,LSP):子类必须能够替换其基类并被客户端代码透明地使用,而不会导致意外的行为。遵循 LSP 可以确保代码的正确性和一致性。

  4. 接口隔离原则(Interface Segregation Principle,ISP):客户端不应该强迫依赖它们不使用的接口。应该将庞大而臃肿的接口拆分为更小、更具体的接口,以便客户端只需知道它们所需的接口。

  5. 依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该依赖于低层模块,它们都应该依赖于抽象。抽象应该依赖于细节,而不是细节依赖于抽象。这通过依赖注入、控制反转等技术来实现,以提高系统的灵活性和可测试性。

当开发人员遵循这些原则时,他们可以设计出更具弹性和可持续性的软件系统,减少代码的脆弱性,并提高代码的质量和可读性。

12. Dart 中 Object、dynamic 和 var 有何不同? ​

  1. Object 是 Dart 中所有类的基类。它是一个通用的类型,可以表示任何对象。所有的 Dart 对象都可以赋值给 Object 类型的变量。由于 Object 是所有类的超类,因此可以使用 Object 类型的变量来调用一些通用的方法,如 toString() 和 hashCode() 。

  2. dynamic 是 Dart 中的一种特殊类型。使用 dynamic 类型声明的变量可以在运行时具有任何类型的值。它被称为动态类型,因为它的类型在编译时不会被静态检查。这意味着可以对 dynamic 类型的变量执行任何操作,而编译器不会发出类型错误。但是,由于缺乏静态类型检查,使用 dynamic 类型可能会导致类型错误和运行时异常。

  3. var 是 Dart 中的一种关键字,用于声明变量而不指定其类型。编译器会根据变量的初始值推断出其类型,并在编译时进行静态类型检查。一旦变量的类型被推断出来,它就被视为具有该类型,不能更改为其他类型。与 dynamic 不同,var 变量在编译时进行类型检查,如果尝试对其执行不兼容的操作,编译器会发出类型错误。

示例:

dart
Object obj = 'Hello';  // Object类型变量可以存储任何对象
print(obj.toString());  // 使用Object类型的变量调用通用方法

dynamic dynamicVar = 10;  // dynamic类型变量可以具有任何类型的值
dynamicVar = 'World';  // 可以将不同类型的值赋给dynamic类型的变量
print(dynamicVar.length);  // 在运行时执行操作,编译器不会进行类型检查

var varVar = 3.14;  // 根据初始值推断变量类型为double
// varVar = 'Dart';  // 不能将不同类型的值赋给var类型的变量
print(varVar.toDouble());  // 编译器进行静态类型检查

总结:

  • Object 是所有类的基类,可以表示任何对象。
  • dynamic 是一种动态类型,可以在运行时具有任何类型的值,但缺乏静态类型检查。
  • var 是一种通过值推断类型的关键字,具有静态类型检查,类型在编译时确定并不可更改。

13. 什么是 Dart 中 cascade 级联和 extension 扩展运算? ​

  1. 级联运算符(Cascade):级联运算符(..)允许在同一个对象上执行多个操作,而无需重复引用该对象。使用级联运算符,可以依次调用同一个对象的多个方法或属性。这在链式调用中特别有用。

    以下是一个使用级联运算符的示例:

    dart
    class Person {
    String name;
    int age;
    
    void introduce() {
      print("My name is $name, and I am $age years old.");
    }
    }
    
    void main() {
    Person person = Person()
      ..name = "John"
      ..age = 30;
    
    person.introduce();
    }

    在上述示例中,通过使用级联运算符..,我们可以在同一个 Person 对象上设置 name 和 age 属性,而无需重复引用 person 对象。

  2. 扩展(extension)是一种机制,允许开发人员向现有的类添加新的功能,而无需修改原始类的代码。它提供了一种在不继承该类的情况下为其添加方法和属性的方式。

    下面是一个示例,展示如何使用扩展为 String 类添加一个新的方法:

    dart
    extension StringExtension on String {
    int get lengthDouble => length * 2;
    
    void printWithExclamation() {
      print(this + "!");
      }
    }
    
    void main() {
    String message = "Hello";
    print(message.lengthDouble); // 输出:10
    message.printWithExclamation(); // 输出:Hello!
    }

    在上述示例中,我们使用 extension 关键字定义了一个扩展,名称为 StringExtension。它扩展了 String 类,并添加了一个名为 lengthDouble 的计算属性和一个名为 printWithExclamation 的方法。在 main 函数中,我们可以直接在 String 对象上使用这些新添加的功能。

14. mixin 混入和 interface 接口在 Dart 中有何不同? ​

  1. mixin(混入):mixin 是一种用于在类中重用代码的机制。通过使用 mixin 关键字,可以定义一个包含一组方法和属性的混入类,并将其混入到其他类中。混入类的成员可以在目标类中被重用,从而实现代码的复用和组合。一个类可以混入多个混入类,但 Dart 不支持多继承。混入类不能直接实例化,只能作为其他类的一部分来使用。

    以下是一个使用 mixin 的示例:

    dart
    mixin Logger {
      void log(String message) {
        print('Logging: $message');
      }
    }
    
    class MyClass with Logger {
      void performAction() {
        log('Action performed');
        // 其他操作
      }
    }
    
    void main() {
      MyClass myObject = MyClass();
      myObject.performAction();  // 输出:Logging: Action performed
    }

    在上述示例中,我们定义了一个 Logger 混入类,它包含一个 log 方法。然后,我们将 Logger 混入到 MyClass 类中,并在 performAction 方法中使用了 log 方法。通过这种方式,MyClass 可以重用 Logger 混入类中的方法。

  2. interface(接口):在 Dart 中,并没有显式的 interface 关键字。相反,每个类都隐式地定义了一个接口。其他类可以通过 implements 关键字实现该接口,并保证实现类需要提供接口中定义的所有方法和属性。这种方式使得类之间可以建立合同关系,并允许多态性。

    以下是一个使用接口的示例:

    dart
    class Animal {
      void makeSound() {
        print('Animal making sound');
      }
    }
    
    class Dog implements Animal {
      void makeSound() {
        print('Dog barking');
      }
    }
    
    void main() {
      Animal animal = Dog();
      animal.makeSound();  // 输出:Dog barking
    }

    在上述示例中,Animal 类定义了一个 makeSound 方法。然后,Dog 类通过 implements 关键字实现了 Animal 接口,并提供了自己的 makeSound 方法。通过将 Dog 实例赋给 Animal 类型的变量,我们可以调用 makeSound 方法,并实现多态性。

15. Dart 中的空安全是什么? ​

Dart 的空安全引入了一套类型系统和编译时检查,以帮助开发者在编译时发现和解决潜在的空引用错误。它通过以下方式提供了更强的代码安全性:

  1. 非空类型(Non-nullable types):Dart 中的变量可以标记为非空类型,表示该变量永远不会为空。使用非空类型可以在编译时捕获可能的空引用错误。
  2. 可空类型(Nullable types):Dart 中的变量可以标记为可空类型,表示该变量可以为空。对于可空类型的变量,必须使用特殊的操作符(如?.和!.)来访问其属性或方法,以确保安全地处理可能为空的情况。
  3. 后置感叹号操作符(Postfix ! operator):当开发者确定一个可空类型的变量在某个特定点不为空时,可以使用后置感叹号操作符!来显式地将其标记为非空,以告诉编译器不再对其进行空值检查。
  4. 非空断言(Non-null assertion):使用非空断言操作符!可以告诉编译器某个变量在某个点不为空,类似于后置感叹号操作符,但不同的是,非空断言不会自动将可空类型转换为非空类型,而是在运行时强制断言。

举例:

dart
void main() {
  // 非空类型
  String name = 'Alice';
  int age = 30;
  double? height;  // 可空类型

  print(name.length);  // 不需要空值检查
  print(age.isEven);   // 不需要空值检查

  // 使用可空类型需要进行空值检查
  if (height != null) {
    print(height);
  } else {
    print('Height is not available.');
  }

  // 使用后置感叹号操作符进行非空断言
  String? nullableName = null;
  String nonNullableName = nullableName!;
  print(nonNullableName);

  // 使用条件表达式进行空值检查
  String? message;
  String output = message ?? 'Default message';
  print(output);
}

在上面的示例中,我们声明了一个非空类型的 name 和 age 变量,它们不允许为空。我们可以直接使用它们的属性和方法,而不需要进行空值检查。

另外,我们声明了一个可空类型的 height 变量,它可能为空。当我们需要使用它时,需要进行空值检查,以确保它不为空。

我们还演示了使用后置感叹号操作符!进行非空断言的情况。在这种情况下,我们将一个可空类型的变量 nullableName 断言为非空,并将其赋值给 nonNullableName 变量。

最后,我们使用了条件表达式??来处理可空类型的变量。如果 message 为空,我们将使用默认消息'Default message'。

16. 你能解释 Dart 中的 Isolate、Event Loop 和 Future 的概念吗? ​

  1. Isolate(隔离区):Isolate 是 Dart 中的并发执行单元。每个 Isolate 都有自己独立的内存堆,并且可以同时执行自己的代码。不同的 Isolate 之间是相互独立的,它们之间不能直接共享内存。Isolate 可以用于执行耗时的计算、并发处理任务和提高应用程序的性能。Dart 提供了 Isolate API,使得创建和通信多个 Isolate 变得简单。

    dart
    import 'dart:isolate';
    
    void isolateFunction(SendPort sendPort) {
      // 执行一些耗时的计算
      // ...
    
      // 向主 Isolate 发送结果
      sendPort.send('Result');
    }
    
    void main() async {
      ReceivePort receivePort = ReceivePort();
      Isolate isolate = await Isolate.spawn(isolateFunction, receivePort.sendPort);
    
      receivePort.listen((message) {
        print('Received message from isolate: $message');
      });
    
      // ...
    }

    在上面的示例中,我们创建了一个新的 Isolate,并在该 Isolate 中执行了一些耗时的计算。然后,将结果通过 sendPort 发送回主 Isolate,并在主 Isolate 中打印接收到的消息。

  2. Event Loop(事件循环):Event Loop 是 Dart 运行时中的一个机制,用于管理和调度异步任务的执行。它是单线程的,负责处理事件和任务的调度。Event Loop 会不断地从事件队列中获取事件,如果队列中有任务,就执行任务。当执行耗时操作时,会将任务转移到 Isolate 中执行,从而避免阻塞主 Event Loop。Event Loop 的工作方式确保了 Dart 代码的非阻塞执行,使得异步编程成为可能。

    dart
    void main() {
      print('Start');
    
      Future.delayed(Duration(seconds: 2), () {
        print('Delayed task completed');
      });
    
      print('End');
    }

    在上面的示例中,我们使用 Future.delayed 创建了一个延迟 2 秒的任务。尽管代码中存在延迟任务,但主 Event Loop 不会被阻塞,它会继续执行后续的代码。因此,我们会先打印 "Start",然后是 "End",最后是 "Delayed task completed"。

  3. Future(未来):Future 是 Dart 中用于表示异步操作结果的对象。它表示一个可能在未来完成的值或错误。Future 可以看作是一个占位符,表示异步操作的结果。当异步操作完成时,Future 可以被解析(resolved)为一个值或一个异常。通过使用 Future,我们可以编写异步代码,可以注册回调函数来处理操作完成的结果,或者使用 async/await 语法来编写更简洁的异步代码。

    dart
    Future<int> fetchNumber() {
      return Future.delayed(Duration(seconds: 2), () => 42);
    }
    
    void main() async {
      print('Fetching number...');
      int number = await fetchNumber();
      print('Fetched number: $number');
    }

    在上面的示例中,我们定义了一个 fetchNumber 函数,它返回一个 Future<int> 对象。该 Future 表示一个在未来可能完成的整数值。我们使用 Future.delayed 来模拟一个异步操作,2 秒后返回整数值 42。

    在 main 函数中,我们使用 await 关键字等待 fetchNumber 函数的结果,并将结果赋值给 number 变量。然后我们打印出获取到的数字。

    通过使用 Future,我们可以编写异步代码,以一种类似于同步代码的方式来处理异步操作的结果。

17. Flutter 无状态和有状态小部件之间有什么区别,以及 setState() 的作用是什么? ​

在 Flutter 中,无状态小部件(stateless widget)和有状态小部件(stateful widget)之间有以下区别:

  1. 无状态小部件(Stateless Widget):

    • 无状态小部件是不可变的,它们在创建后不会发生变化。它们的外观和行为仅取决于输入的属性(parameters)。
    • 无状态小部件通常用于显示静态内容,如文本、图像等,或者用于根据输入属性构建 UI 布局。
    • 由于无状态小部件是不可变的,当它们的属性发生变化时,它们会被完全重建。
  2. 有状态小部件(Stateful Widget):

    • 有状态小部件是可变的,它们在创建后可以发生变化。它们可以包含可变的状态(state)数据。
    • 有状态小部件通常用于处理用户交互、响应事件和动态更新 UI 等情况。
    • 有状态小部件通过继承 StatefulWidget 类来创建,并使用单独的 State 类来管理状态数据。

    setState() 方法用于在有状态小部件中更新状态并触发 UI 的重新构建。当状态发生变化时,调用 setState() 方法会通知 Flutter 框架重新构建小部件的 UI,以反映更新后的状态。

    以下是一个简单的示例,演示了有状态小部件和 setState() 的使用:

    dart
    import 'package:flutter/material.dart';
    
    class CounterWidget extends StatefulWidget {
      @override
      _CounterWidgetState createState() => _CounterWidgetState();
    }
    
    class _CounterWidgetState extends State<CounterWidget> {
      int _counter = 0;
    
      void _incrementCounter() {
        setState(() {
          _counter++;
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            Text('Counter: $_counter'),
            ElevatedButton(
              onPressed: _incrementCounter,
              child: Text('Increment'),
            ),
          ],
        );
      }
    }
    
    void main() {
      runApp(MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: Text('Stateful Widget Example')),
          body: CounterWidget(),
        ),
      ));
    }

    在上面的示例中,我们创建了一个名为 CounterWidget 的有状态小部件。它包含一个 _counter 状态变量和一个 _incrementCounter 方法,用于增加计数器的值。当用户点击按钮时,_incrementCounter 方法会调用 setState(),触发 UI 的重新构建,并更新计数器的显示。当状态发生变化时,只有受影响的部分会被重新构建,以提高性能。

    通过使用有状态小部件和 setState(),我们可以在 Flutter 中实现交互性和动态性的 UI,使得小部件能够根据状态的变化来更新自身的显示。

18. Flutter InheritedWidget 是什么? ​

Flutter 中的 InheritedWidget 是一种特殊的小部件(Widget),它允许在小部件树中共享和传递数据给其子孙小部件,而无需显式地通过构造函数传递数据。

InheritedWidget 的主要特点是它可以将数据在小部件树中向下传递,并且在数据发生变化时,能够自动通知依赖它的小部件进行更新。这样可以避免手动管理数据传递和手动触发小部件的重新构建。

InheritedWidget 的工作原理是基于 Dart 中的继承机制。当一个小部件被标记为 InheritedWidget 并且其数据发生变化时,Flutter 会自动遍历小部件树,找到依赖该 InheritedWidget 的小部件,并通知它们进行更新。

使用 InheritedWidget 可以方便地实现一些全局或跨多个小部件共享的数据,例如应用程序的主题、语言设置、认证状态等。

示例,演示了如何使用 InheritedWidget 共享和传递数据:

dart
import 'package:flutter/material.dart';

class MyData extends InheritedWidget {
  final int counter;

  MyData({required this.counter, required Widget child}) : super(child: child);

  static MyData? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<MyData>();
  }

  @override
  bool updateShouldNotify(MyData oldWidget) {
    return counter != oldWidget.counter;
  }
}

class CounterWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final myData = MyData.of(context);
    final counter = myData?.counter ?? 0;

    return Column(
      children: [
        Text('Counter: $counter'),
        ElevatedButton(
          onPressed: () {
            // 修改数据并触发更新
            MyData.of(context)?.counter++;
          },
          child: Text('Increment'),
        ),
      ],
    );
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyData(
        counter: 0,
        child: Scaffold(
          appBar: AppBar(title: Text('InheritedWidget Example')),
          body: CounterWidget(),
        ),
      ),
    );
  }
}

在上面的示例中,我们首先创建了一个名为 MyData 的 InheritedWidget。它包含一个 counter 属性,并实现了 updateShouldNotify 方法来通知依赖它的小部件进行更新。然后,我们创建了一个名为 CounterWidget 的小部件,在其 build 方法中访问并使用 MyData 中的数据。当用户点击按钮时,我们通过 MyData.of(context) 来获取 MyData 实例,并修改 counter 的值。这将触发依赖于 MyData 的小部件进行更新,包括显示计数器的 CounterWidget。

通过使用 InheritedWidget,我们可以简化数据的共享和传递,实现跨多个小部件的数据更新和同步。它是 Flutter 中一种强大的状态管理工具,尤其适用于共享全局数据或应用程序级别的状态。

19. 你能解释一下在 Flutter 中 keys 键的作用吗? ​

在 Flutter 中,Keys(键)是一种用于标识小部件的机制,它们对于在更新小部件树时进行识别和比较非常重要。Keys 提供了以下作用:

  1. 识别 Widget 小部件:每个小部件都可以关联一个 Key 对象,用于标识自身。通过将 Key 分配给小部件,可以确保在小部件树中唯一地标识该小部件。

  2. 有效地更新 Widget 小部件:当 Flutter 重新构建小部件树时,它会使用新的小部件实例与之前的小部件实例进行比较。通过使用相同的 Key,Flutter 可以确定哪些小部件是相同的(相同类型且具有相同的 Key),从而可以有效地更新这些小部件而不是重新创建它们。

  3. 保留状态:当使用 Key 标识 Widget 小部件时,即使小部件树发生变化,具有相同 Key 的小部件仍将保留其状态。这对于在动态列表或小部件重排时保留用户输入或滚动位置等状态非常有用。

尽管 Keys 提供了一些优势,但在大多数情况下,Flutter 可以自动处理 Widget 小部件树的更新和重建,而无需显式地使用 Keys。只有在特定情况下,如动态列表、Widget 小部件重用或需要保留状态时,才需要使用 Keys。

示例,演示了在 Flutter 中使用 Keys 的情况:

dart
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    final item = items[index];
    return MyItemWidget(
      key: ValueKey(item.id), // 使用 item.id 作为唯一 Key
      item: item,
    );
  },
);

在上面的示例中,我们使用 ListView.builder 构建一个动态列表,其中每个列表项都是一个 MyItemWidget。为了确保在列表项发生变化时正确更新,我们使用 ValueKey(item.id) 将 item.id 作为唯一 Key 分配给每个 MyItemWidget。这样,当列表项的顺序、内容或数量发生变化时,Flutter 可以通过 Key 识别和比较 Widget 小部件,以便进行适当的更新。

需要注意的是,尽量避免频繁地更改或生成新的 Key,因为这可能会导致不必要的小部件重建和性能问题。应该选择稳定且在小部件生命周期内保持一致的键。

20. 在 Flutter 中,Keys(键)有哪几种类型? ​

在 Flutter 中,Key 有两个主要的子类:LocalKey 和 GlobalKey。

  1. LocalKey(局部键):

    • ValueKey:使用特定的值作为标识符,可以是数字、字符串或其他可比较的对象。例如,ValueKey(1) 或 ValueKey('myKey')。
    • ObjectKey:使用对象作为标识符,使用对象的引用进行识别和比较。例如,ObjectKey(myObject)。
    • UniqueKey:生成全局唯一的标识符,用于确保在每次重建时都会创建新的小部件实例。例如,UniqueKey()。

    下面是一个使用这些 LocalKey 的示例:

    dart
    ListView(
      children: [
        ListTile(
          key: ValueKey(1),
          title: Text('Item 1'),
        ),
        ListTile(
          key: ObjectKey(myObject),
          title: Text('Item 2'),
        ),
        ListTile(
          key: UniqueKey(),
          title: Text('Item 3'),
        ),
      ],
    );

    在上面的示例中,我们使用了不同的 LocalKey 类型来标识和比较列表中的 ListTile。ValueKey、ObjectKey 和 UniqueKey 分别用于不同的标识需求。

  2. GlobalKey(全局键): GlobalKey 是 LocalKey 的一个特殊子类,用于在整个应用程序中跨小部件树进行引用和识别。通过 GlobalKey,可以直接访问关联小部件的状态和方法。

    以下是 GlobalKey 的示例:

    dart
    class MyWidget extends StatefulWidget {
      MyWidget({Key key}) : super(key: key);
    
      @override
      _MyWidgetState createState() => _MyWidgetState();
    }
    
    class _MyWidgetState extends State<MyWidget> {
      int _counter = 0;
    
      void _incrementCounter() {
        setState(() {
          _counter++;
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            Text('Counter: $_counter'),
            ElevatedButton(
              onPressed: _incrementCounter,
              child: Text('Increment'),
            ),
          ],
        );
      }
    }
    
    final GlobalKey<_MyWidgetState> myWidgetKey = GlobalKey<_MyWidgetState>();
    
    // 在其他地方使用 GlobalKey 引用 MyWidget 并访问其状态和方法
    // 例如:
    myWidgetKey.currentState._incrementCounter();

    在上面的示例中,我们创建了一个 MyWidget,并使用 GlobalKey<_MyWidgetState> myWidgetKey 进行引用。通过 myWidgetKey.currentState,我们可以访问 MyWidget 的当前状态,并调用其方法。

21. Flutter 中的 Navigator 是什么? ​

在 Flutter 中,Navigator 是一个用于管理路由(页面)导航的类。它提供了一组方法,用于在应用程序中进行页面的推入(push)、弹出(pop)和替换(replace)操作,以及管理页面堆栈。

Navigator 类是由 Flutter 框架提供的,可以在应用程序中使用它来管理应用程序的导航栈。导航栈是一个存储页面路由的堆栈数据结构,可以在其中推入(push)和弹出(pop)页面。

使用 Navigator 类,您可以执行以下操作:

  1. 推入页面(Push):将新页面推入导航栈,并显示在当前页面之上。
  2. 弹出页面(Pop):从导航栈中移除当前页面,并返回到上一个页面。
  3. 替换页面(Replace):替换当前页面为新页面,同时移除导航栈中的上一个页面。
  4. 弹出到指定页面(Pop Until):从导航栈中连续弹出页面,直到指定页面为止。
  5. 获取当前页面的上下文(Context):通过 Navigator 类的方法获取当前页面的上下文,以便执行其他操作。

通过使用 Navigator 类,您可以实现应用程序中的页面导航和页面间的切换,例如在用户进行操作时推入新页面、返回上一个页面或者在特定条件下替换当前页面。

在 Flutter 中,通常将 Navigator 放置在应用程序的根部,以便在整个应用程序中进行页面导航的管理。

使用例子:

dart
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home'),
      ),
      body: Center(
        child: ElevatedButton(
          child: Text('Go to Details'),
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => DetailsPage()),
            );
          },
        ),
      ),
    );
  }
}

class DetailsPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Details'),
      ),
      body: Center(
        child: ElevatedButton(
          child: Text('Go Back'),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ),
    );
  }
}

22. Flutter 中的 Navigator 2.0 是什么? ​

在 Flutter 中,Navigator 2.0 是一种新的导航机制,引入了路由管理器和路由信息的概念,以提供更灵活和可扩展的页面导航功能。Navigator 2.0 是对之前的 Navigator 的改进和升级。

Navigator 2.0 的核心思想是将导航状态(包括当前页面和页面历史记录)存储在应用程序的状态中,而不是直接存储在导航栈中。它通过使用路由管理器(RouteManager)和路由信息(RouteInformation)来管理导航状态。

路由管理器(RouteManager)是一个负责管理路由的对象,它可以监听导航状态的变化,并根据路由信息构建和切换页面。它可以根据应用程序的状态、用户操作或其他条件来处理页面的推入、弹出和替换。

路由信息(RouteInformation)是一个包含导航状态的对象,它描述了当前页面的信息以及页面历史记录。它可以包含路由名称、路径、查询参数等信息,以便在导航过程中进行路由匹配和页面构建。

通过使用 Navigator 2.0,开发者可以更加灵活地管理应用程序的导航状态,并实现更复杂的导航场景,如深链接、动画过渡、持久化导航状态等。

要使用 Navigator 2.0,您需要使用 Flutter 提供的 Router 和 RouterDelegate 类来设置路由管理器,并实现自定义的路由信息。

Navigator 2.0 对于大型应用程序或需要更高级导航功能的应用程序来说是一个强大的工具,它提供了更多的灵活性和可扩展性,以满足不同的应用程序需求。

23. Flutter 中的 Navigator 接收返回值 ​

在 Flutter 中,可以通过 Navigator.push 方法的返回值来接收从目标页面返回的数据。以下是一个示例:

dart
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home'),
      ),
      body: Center(
        child: ElevatedButton(
          child: Text('Go to Details'),
          onPressed: () async {
            final result = await Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => DetailsPage()),
            );

            // 处理从 DetailsPage 返回的数据
            if (result != null) {
              // 根据返回的数据执行相应操作
              print('Received result: $result');
            }
          },
        ),
      ),
    );
  }
}

class DetailsPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Details'),
      ),
      body: Center(
        child: ElevatedButton(
          child: Text('Go Back with Result'),
          onPressed: () {
            // 返回数据到上一个页面
            Navigator.pop(context, 'Hello from DetailsPage');
          },
        ),
      ),
    );
  }
}

在上面的示例中,HomePage 包含一个按钮,当按钮被点击时,它通过 Navigator.push 方法将用户导航到 DetailsPage。注意到 Navigator.push 方法是一个异步方法,并且它返回一个 Future 对象。

在 DetailsPage 中,当按钮被点击时,我们使用 Navigator.pop 方法返回数据到上一个页面。在这个例子中,我们返回了一个字符串 'Hello from DetailsPage'。

在 HomePage 中,我们使用 await 关键字将 Navigator.push 方法的返回值赋给 result 变量。这样,当从 DetailsPage 返回时,我们可以检查 result 的值来获取返回的数据。

在示例中,我们通过检查 result 是否为 null 来确定是否收到了返回的数据。如果 result 不为 null,我们可以根据返回的数据执行相应的操作,例如打印返回的结果。

通过这种方式,您可以在 Flutter 中使用 Navigator 接收从目标页面返回的数据,并对返回的数据进行处理。

24. Flutter 中的 Navigator 嵌套使用 ​

在 Flutter 中,可以通过嵌套使用 Navigator 来实现多级页面导航。每个嵌套的 Navigator 可以管理自己的页面堆栈,使得在不同层级之间进行导航变得更加灵活。

有兴趣可以阅读猫哥的文章和视频:

25. Flutter 中有哪些状态管理,你主要用哪个 ​

在 Flutter 中,有多种用于状态管理的方法,每种方法都有不同的工作原理和适用场景。以下是一些常见的状态管理方法:

  1. 本地状态管理(Local State Management):本地状态管理是指在小规模应用或组件内部管理状态的简单方法。通常使用 StatefulWidget 和 setState 来更新状态。该方法适用于较简单的应用或组件,状态的范围有限且不需要在多个组件之间共享。

  2. InheritedWidget 和 InheritedModel:InheritedWidget 和 InheritedModel 是 Flutter 中的两个基础类,用于在组件树中共享状态。它们通过将状态作为不可变对象传递给子组件来实现状态共享。当共享的状态发生变化时,它们会自动更新子组件。这种方法适用于中等规模的应用,可以在组件树中共享状态,但不适用于大型应用或高度复杂的状态管理。

  3. Provider:Provider 是 Flutter 社区中广泛使用的状态管理库,它构建在 InheritedWidget 之上,提供了一种简化状态共享的方式。它使用了依赖注入的概念,可以在组件树的任何位置共享状态,并自动通知相关的子组件进行更新。Provider 支持多种类型的状态管理,包括基于 ChangeNotifier、Stream、ValueNotifier 等。它适用于中等到大型规模的应用,具有良好的灵活性和性能。

  4. Redux:Redux 是一个基于 Flux 架构的状态管理库,它通过单一的全局状态存储(Store)和纯函数(Reducers)来管理状态。Redux 使用了不可变数据和单向数据流的概念,并通过派发操作(Actions)来触发状态的变化。Redux 适用于大型应用或需要严格的状态管理和可预测性的场景。

  5. Bloc:Bloc 是一种基于 Rx(响应式编程)和单向数据流的状态管理库。它使用 Stream 和 Sink 来处理输入和输出,并使用事件(Events)和状态(States)的流来管理应用的状态。Bloc 适用于复杂的业务逻辑和交互,可以将应用程序的状态和事件分离,并提供了强大的工具和模式来处理异步操作和状态变化。

以上只是一些常见的状态管理方法,还有其他的库和模式可供选择,如 GetX、MobX、Riverpod 等。选择合适的状态管理方法取决于应用的规模、复杂度和团队的偏好。重要的是根据具体需求选择适合的状态管理方案,以提高应用的可维护性和开发效率。

26. Bloc 和 Cubit 之间有什么区别? ​

https://pub-web.flutter-io.cn/packages/bloc

Bloc(Business Logic Component)和 Cubit(Combination of Bloc and Unit)是 Flutter 中常用的状态管理库,它们有一些区别,主要体现在以下几个方面:

  1. 复杂性和灵活性: Bloc 相对于 Cubit 来说更加复杂和灵活。Bloc 是基于 Rx(响应式编程)和单向数据流的状态管理库,它提供了强大的工具和模式来处理复杂的业务逻辑和交互,例如异步操作、副作用管理、事件和状态的转换等。Bloc 提供了更大的灵活性,适用于大型应用或需要处理复杂逻辑的场景。而 Cubit 则是 Bloc 的一个简化版本,它去除了异步操作和副作用管理的部分,更加轻量级和简单,适用于中小型应用或简化的状态管理需求。

  2. 代码量和学习曲线: 由于 Bloc 提供了更多的功能和灵活性,它的代码量和学习曲线相对较高。使用 Bloc 需要熟悉 Rx 编程概念和一些特定的 Bloc 模式和约定。相比之下,Cubit 的代码量更少,学习曲线更平缓,更容易上手和理解。

  3. 状态的管理方式: 在 Bloc 中,状态通常由一个或多个 Stream 来管理,通过派发事件(Events)和监听状态(States)的流来实现状态的变化和更新。而 Cubit 使用一个单一的 State 对象来管理状态,并通过 emit 方法来触发状态的变化。Cubit 更加简洁和直接,适用于简单的状态管理。Bloc 则提供了更多的灵活性,可以处理更复杂的状态变化和异步操作。

  4. 推荐的使用场景: 由于 Cubit 相对于 Bloc 来说更加简单和轻量级,因此它更适合于中小型应用或简化的状态管理需求。如果应用的状态管理相对复杂,需要处理异步操作、副作用等复杂逻辑,那么 Bloc 是更好的选择。Bloc 在大型应用、复杂业务逻辑和团队合作开发中的优势更加明显。

需要注意的是,Bloc 和 Cubit 都是基于相同的概念和模式构建的,它们遵循了单向数据流、不可变状态等原则,并提供了类似的工作方式和 API。选择使用哪个取决于应用的需求和开发团队的偏好,以及对复杂性和灵活性的权衡。

27. Flutter GetX 中 obx 和 getBuild 有什么区别 ​

https://pub-web.flutter-io.cn/packages/get

在 Flutter GetX 库中,obx 和 GetBuilder 是用于在 UI 层观察和更新状态的两个常用组件。它们之间的主要区别如下:

  1. 语法和用法:obx(即 Observer)是一个 Widget,可以将其包裹在需要观察状态变化的部分,通常是一个小部件或一个小部分的 UI。它使用了 GetBuilder 的方式,但提供了更简洁的语法。例如,使用 obx 可以直接在 UI 中访问控制器的属性或方法,而无需显式地使用 Get.find 获取控制器实例。

    GetBuilder 是另一个 Widget,它需要指定一个控制器实例,并通过 builder 回调函数来构建 UI。在 GetBuilder 中,需要手动访问控制器的属性和方法,并在回调函数中根据需要更新 UI。相比之下,obx`提供了一种更简洁和便捷的方式来观察和更新状态。

  2. 重绘粒度:obx 的重绘粒度更细,它会自动追踪使用的控制器的特定属性,并在这些属性发生变化时进行重绘。这意味着只有与变化的属性相关联的部分会被重新构建,而其他不相关的部分将保持不变,提高了性能。

    GetBuilder 的重绘粒度相对较粗,它会在控制器的任何属性发生变化时重新构建整个绑定的 UI 部分。这可能会导致不必要的重绘,特别是在控制器具有多个属性且只有部分属性发生变化时。

综上所述,obx 提供了更简洁、便捷和性能优化的方式来观察和更新状态,特别适用于小规模的状态管理和局部 UI 更新。而 GetBuilder 则更适合于需要手动控制重绘粒度或更复杂的状态管理场景。

obx 例子

dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';

// 创建一个控制器类
class MyController extends GetxController {
  var count = 0.obs; // 使用.obs将count变为可观察的(Observable)

  void increment() {
    count.value++; // 修改可观察变量的值
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  final MyController controller = Get.put(MyController()); // 注入控制器

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('GetX obx Example'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              // 使用obx观察和更新状态
              Obx(() => Text(
                'Count: ${controller.count.value}', // 获取可观察变量的值
                style: TextStyle(fontSize: 24),
              )),
              SizedBox(height: 16),
              ElevatedButton(
                onPressed: () {
                  controller.increment(); // 调用控制器的方法来更新状态
                },
                child: Text('Increment'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

getBuilder 例子

dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';

// 创建一个控制器类
class MyController extends GetxController {
  var count = 0;

  void increment() {
    count++;
    update(); // 手动触发UI更新
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  final MyController controller = Get.put(MyController()); // 注入控制器

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('GetX GetBuilder Example'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              // 使用GetBuilder观察和更新状态
              GetBuilder<MyController>(
                builder: (controller) => Text(
                  'Count: ${controller.count}', // 获取控制器的属性值
                  style: TextStyle(fontSize: 24),
                ),
              ),
              SizedBox(height: 16),
              ElevatedButton(
                onPressed: () {
                  controller.increment(); // 调用控制器的方法来更新状态
                },
                child: Text('Increment'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

如果想系统学习 getx

28. Flutter 中什么事依赖注入 ​

在 Flutter 中,依赖注入(Dependency Injection)是一种设计模式和技术,用于管理和提供应用程序中的依赖关系。依赖注入的目的是解耦组件之间的依赖关系,提高代码的可测试性、可维护性和可扩展性。

在 Flutter 中,依赖注入可以用于以下几个方面:

  1. 控制器(Controller)和服务(Service)的注入:依赖注入可以帮助我们在需要的地方注入控制器和服务的实例,而不需要手动实例化它们。这样可以降低代码的耦合度,并且方便进行单元测试和模块替换。Flutter GetX 库是一个支持依赖注入的流行选择,它提供了一个全局的依赖注入容器,可以通过 Get.put()方法将控制器或服务注册为单例,并在需要时使用 Get.find()获取它们的实例。

  2. 路由(Route)的注入:依赖注入还可以用于在应用程序中注入路由,以便在不同的页面或组件之间进行导航。例如,可以使用依赖注入容器来注册路由配置,并在需要导航到特定页面时,通过注入路由管理器的方式进行导航。这样可以提高代码的可读性和可维护性,并且可以轻松地更改路由配置而不影响其他部分的代码。

  3. 配置和环境变量的注入:依赖注入可以用于注入应用程序的配置和环境变量,例如 API 密钥、服务器地址等。通过将这些配置和环境变量注册为依赖项,我们可以在应用程序的不同部分中轻松地访问它们,并且可以根据需要更改它们,而不需要修改大量的代码。

  4. 其他依赖关系的注入:除了上述示例之外,依赖注入还可以用于注入其他类型的依赖关系,如数据库连接、存储库、第三方库等。通过将这些依赖项注册为单例或根据需要创建新的实例,我们可以更好地管理应用程序中的依赖关系,并提供可扩展和可测试的代码结构。

依赖注入在 Flutter 中用于管理和提供应用程序中的各种依赖关系,包括控制器、服务、路由、配置和其他依赖项。它提供了一种解耦组件之间依赖关系的方式,并提高了代码的可测试性、可维护性和可扩展性。

29. Flutter 组件 Get_it 组件是什么 ​

https://pub-web.flutter-io.cn/packages/get_it

get_it 是一个在 Flutter 中用于依赖注入的第三方库。它提供了一个简单而强大的依赖注入容器,使得在应用程序中管理和访问依赖项变得更加容易。

下面是一些 get_it 库的特点和用法:

  1. 轻量且易于使用:get_it 是一个轻量级的库,没有复杂的配置和依赖关系图。它提供了简单的 API,使得注册和获取依赖项变得非常容易。

  2. 支持单例和懒加载:get_it 支持将依赖项注册为单例,这意味着同一个依赖项只会被实例化一次,并且在应用程序的不同部分共享使用。另外,get_it 也支持懒加载,即只有在第一次访问依赖项时才会进行实例化。

  3. 支持异步和同步依赖项:get_it 提供了对异步和同步依赖项的支持。你可以注册异步工厂函数来创建异步依赖项,也可以注册同步工厂函数来创建同步依赖项。

  4. 支持依赖项解析:get_it 允许你在注册依赖项时指定其解析方式。你可以选择自动解析依赖项(默认情况下),也可以手动解析依赖项。手动解析依赖项可以为你提供更多的灵活性和控制权。

  5. 支持注册别名:get_it 允许你为依赖项注册别名,以便于识别和访问。这对于管理大量依赖项时非常有用。

使用 get_it 库的基本流程如下:

  1. 在应用程序的启动时,注册需要的依赖项。你可以使用 get_it 的 registerSingleton、registerFactory 或 registerLazySingleton 方法进行注册。

  2. 在需要访问依赖项的地方,使用 get_it 的 get 方法获取依赖项的实例。

  3. 可选地,你可以使用 get_it 的 isRegistered 方法检查依赖项是否已经注册。

get_it 提供了一种简单而强大的方式来进行依赖注入,使得管理和访问依赖项变得更加方便和灵活。它是一个流行的依赖注入库,被广泛用于 Flutter 应用程序的开发中。

30. Flutter Sliver 是什么解决了什么问题 ​

在 Flutter 中,Sliver 是用于构建灵活和高性能滚动效果的组件。

Sliver 解决了以下问题:

  1. 可变大小的滚动元素:传统的滚动组件(如 ListView)中的每个子项都具有固定的高度,这限制了滚动元素的灵活性。而 Sliver 允许每个子项具有不同的高度,从而实现了可变大小的滚动元素。这对于需要动态调整高度的元素(如可伸缩的标题、动态列表项等)非常有用。
  2. 交互式滚动效果:Sliver 允许在滚动过程中实现交互式效果和动画。你可以根据滚动位置或其他条件来控制 Sliver 中的内容,例如淡入淡出效果、透明度变化、放大缩小效果等。这为创建吸顶效果、悬浮按钮、展开折叠效果等提供了便利。
  3. 高性能滚动:使用 Sliver 构建的滚动效果可以提供更好的性能。Sliver 通过延迟构建和回收不可见的元素,以及只构建可见元素来减少内存占用和渲染开销。这在处理大数据集或具有复杂布局的滚动视图时特别有用。
  4. 灵活的自定义:Sliver 提供了强大的自定义能力,允许你根据需要定制滚动效果。你可以根据自己的需求创建自定义的 Sliver 组件,实现各种复杂的滚动效果和交互。

代码示例

dart
CustomScrollView(
  slivers: <Widget>[
    SliverAppBar(
      // 在滚动时隐藏/显示标题栏
      title: Text('Sliver Demo'),
      floating: true,
      // 其他SliverAppBar属性...
    ),
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (BuildContext context, int index) {
          // 构建列表项
          return ListTile(
            title: Text('Item $index'),
          );
        },
        childCount: 20,
      ),
    ),
  ],
)