本文是关于 Flutter 面试问题的第二篇,第一篇点这里 https://ducafecat.com/blog/flutter-interview-questions-with-answers-01 。 如果你想系统学习请关注猫哥课程 https://ducafecat.com 。

Flutter 面试题整理 02

视频

https://youtu.be/YlwY1j4RExA

前言

原文 https://ducafecat.com/blog/flutter-interview-questions-with-answers-02

本文是关于 Flutter 面试问题的第二篇,第一篇点这里 https://ducafecat.com/blog/flutter-interview-questions-with-answers-01

如果你想系统学习请关注猫哥课程 https://ducafecat.com

正文

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):高层模块不应该依赖于低层模块,它们都应该依赖于抽象。抽象应该依赖于细节,而不是细节依赖于抽象。这通过依赖注入、控制反转等技术来实现,以提高系统的灵活性和可测试性。

img

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

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变量在编译时进行类型检查,如果尝试对其执行不兼容的操作,编译器会发出类型错误。

示例:

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

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

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对象上设置nameage属性,而无需重复引用person对象。

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

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

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的示例:

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混入类中的方法。

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

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

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):使用非空断言操作符!可以告诉编译器某个变量在某个点不为空,类似于后置感叹号操作符,但不同的是,非空断言不会自动将可空类型转换为非空类型,而是在运行时强制断言。

举例:

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);
}

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

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

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

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

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

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

  1. Event Loop(事件循环):Event Loop 是 Dart 运行时中的一个机制,用于管理和调度异步任务的执行。它是单线程的,负责处理事件和任务的调度。Event Loop 会不断地从事件队列中获取事件,如果队列中有任务,就执行任务。当执行耗时操作时,会将任务转移到 Isolate 中执行,从而避免阻塞主 Event Loop。Event Loop 的工作方式确保了 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"。

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

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 共享和传递数据:

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 的情况:

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 的示例:

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 分别用于不同的标识需求。

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

以下是 GlobalKey 的示例:

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 的当前状态,并调用其方法。

小结

本节讲了第 11 ~ 20 个 Flutter 面试中出现的问题,如果你在面试中遇到奇怪问题可以联系我。

下次再见。

感谢阅读本文

如果有什么建议,请在评论中让我知道。我很乐意改进。


flutter 学习路径


© 猫哥 ducafecat.com

end