Flutter 面试宝典【Part1】

2026-07-31 Flutter,面试

一、Dart 核心基础

Q1:const 和 final 区别?

final:运行时常量,只能赋值一次,值可以是运行时计算出来的

const:编译期常量,必须在编译时就确定值,全局唯一、不可变

详解

1. 赋值时机

final:

  • 可以在声明时或构造函数初始化列表赋值
  • 值可以是运行时计算出来的
final time = DateTime.now(); // 合法,运行时确定

const:

  • 必须声明时直接赋值
  • 值必须是编译期可确定的字面量
const time = DateTime.now(); // 报错!DateTime.now() 是运行时方法

2. 不可变性

final:变量引用不可变,但对象本身可以可变

final list = [1, 2];
list.add(3); // 允许

const:深度不可变,对象本身也不能修改

const list = [1, 2];
list.add(3); // 报错

3. 内存与实例

final:每次创建都是【新对象】

const:相同内容的 const 对象在【内存中只有一份】(常量 canonicalization)

4. 使用场景(Flutter 重点)

final:
组件里不变的配置(titleid
运行时才能确定的值

const:
写 Flutter 组件时极力推荐:
提升性能,避免重复构建

const Text("Hello");const SizedBox();

极简背诵版

final 运行时常量,const 编译期常量
final 可运行时计算,const 必须编译期确定
final 引用不可变,对象可改;const 深度不可变
const 对象内存复用,final 每次新建
Flutter 里组件尽量用 const 提升性能


Q2:Dart 单线程模型原理?EventLoop 执行顺序?

Dart 为单线程事件循环模型,无多线程竞争;

执行优先级:同步代码 > Microtask微任务队列 > Event事件队列。
Future 默认进入 Event 队列,scheduleMicrotask 进入微任务队列。

面试背诵版(精简)

  1. Dart 是【单线程 + 事件循环】,异步不阻塞,靠 EventLoop(事件循环) 调度。
  2. 真正并发靠 Isolate,内存隔离,通过Port消息通信。
  3. 任务分两级:MicroTask微任务队列 优先级 > Event事件队列
  4. 执行顺序: 同步代码 → 清空微任务 → 执行一个事件 → 再清空微任务 → 循环
  5. Future then 是微任务,Future 构造体、Timer、IO 是事件。

详解

1. Dart 单线程模型原理
Dart 是单线程 + 事件循环(EventLoop)模型,没有多线程抢占式调度。

核心要点

单线程
只有一个主线程(UI Thread)执行所有 Dart 代码。
不存在多线程并发,所以不需要锁、不会出现竞态条件。

异步靠 EventLoop,不靠多线程

IO、网络、计时器、Future/Stream 都不是阻塞等待,而是交给系统底层执行
等结果回来后,把回调丢到事件队列,由 EventLoop 统一调度。

隔离区(Isolate)

Dart 真正的“并发”是 Isolate。
每个 Isolate 有独立的内存、独立的 EventLoop,互不共享内存。
Isolate 之间只能通过 Port 消息通信。

2. 任务队列分类(重中之重)

Dart 事件循环有两级队列

优先级:MicroTask(微任务队列) > Event(事件队列)

3. EventLoop 完整执行顺序

执行 main() 函数,同步代码立即执行。

当同步代码执行完,开始一轮循环:

优先清空所有 MicroTask,直到微任务队列为空。

再从 Event 队列取出一个事件执行。

执行完一个 Event 后,再次检查并清空 MicroTask。

循环往复,直到程序退出。

4. 哪些任务进哪个队列?

MicroTask 微任务

Future.microtask(() {})

scheduleMicrotask()

Future 的 then() / catchError() 内部(默认进微任务)

Event 事件队列

真正的 I/O、网络请求

计时器 Timer

点击、手势等 UI 事件

Future(() {}) 构造函数内部的函数体

Stream 事件

5. 经典面试题执行顺序示例

    main() {
      print('1');

      Future(() => print('2'));
      Future.microtask(() => print('3'));

      print('4');
    }

1 → 4 → 3 → 2

原因:

  1. 同步代码先执行:1、4
  2. 微任务 3
  3. 事件任务 2

Q3:extends、implements、mixin 区别?

  1. extends继承:单继承,继承父类所有属性和方法,可重写;
  2. implements实现:纯接口约定,必须全部实现,不复用代码
  3. mixin/with混入:多继承能力,复用多个类的方法,不构造父子关系

详解

extends(继承)

规则

-只能继承一个类(Dart 是单继承)

-子类自动拥有父类所有非私有属性、方法

-可以重写父类方法

必须调用父类构造方法(显式或隐式)

场景

-典型is-a关系:Dog extends Animal

-Flutter 中:class MyWidgetextendsStatelessWidget

implements(实现接口)

规则

可以同时实现多个类 / 接口

实现类必须重写所有方法和属性(不管父类有没有实现)

不复用任何代码,只是契约约束

场景

只需要接口规范,不需要继承实现

例如:class A ``implements ``Walk, Run

mixin(混入)

关键字:with

规则

可以混入多个 mixin,实现 “多继承”

mixin 类不能有构造函数

直接复用 mixin 中的方法、属性

同名方法:后混入的覆盖先混入的

关键字限制

mixin 定义了一组可复用的“能力”,with 用来“消费”这个能力,而 on 则限制了“谁能消费这个能力”。

当一个 mixin使用了 on 关键字,它就只能被混入(with)到那些继承了 on 后面指定类的类中

场景

// 定义一个基类
class BaseClass {
  void baseMethod() {
    print('Base method');
  }
}

// 定义一个 mixin,并使用 on 关键字
// 这意味着:只有 BaseClass 的子类才能使用这个 mixin
mixin MyMixin on BaseClass {
  void mixinMethod() {
    // 因为 on 的限制,这里可以安全地调用 BaseClass 的方法
    baseMethod(); 
    print('Mixin method');
  }
}

// 正确用法:子类先继承 BaseClass,再混入 MyMixin
class MyClass extends BaseClass with MyMixin {
  // ...
}

// 错误用法:没有继承 BaseClass 的类无法混入 MyMixin
// class AnotherClass with MyMixin {} // 这行代码会导致编译错误

Q4:Dart 空安全机制核心要点?late?/!/required 作用?

Dart 空安全:默认不可空,可空必须加?,编译期检查防 NPE(空指针异常)。

?:声明可空类型。
!:强制非空断言,强制认为非空,相信一定不为 null,为 null 则崩溃。
late:延迟初始化,编译不校验赋值,运行时使用前必须赋值;
required:命名参数必传,不可省略不可空。

详解

一、Dart 空安全核心要点

默认不可空 Dart 中所有类型默认不能为空,变量必须有值,不能为 null。

可空类型必须显式声明 想要变量能为 null,必须加 ?,如 String?int?

编译期检查 空安全是编译时检查,不是运行时,提前避免空指针异常(NPE)。

非空判断机制 访问可空变量前,必须:

判空 if (str != null)

或使用 ! 强制断言

或使用 late 延迟初始化

空安全带来好处

彻底消灭 NullPointerException

代码更健壮、可读性更高

AOT 优化更好,Flutter 运行更快

二、? / ! /late/required 作用

? :声明可空类型

作用:告诉编译器这个变量可以为 null

不判空直接访问会报错。

String? name; // 可空
name = null;   // 合法

! : 非空断言(强制解包)

作用:“我保证它一定不为 null”,让编译器跳过检查

风险:如果实际为 null,会直接崩溃

String? a;print(a!); // 若 a 为 null 则报错

late : 延迟初始化

① late 变量

声明时不赋值,但之后一定会赋值

编译器不再要求立即初始化

    late String name;
    void init() {
      name = "flutter";
    }

② late final

只能赋值一次

常用于 State 里的控制器

    late final TextEditingController _controller;

③ late 懒加载(late + 赋值)

    late String info = fetchInfo(); // 第一次使用时才执行

required : 必传参数

用在命名参数前,表示调用时必须传,不能省略,不能为 null

常和空安全一起使用

    void show({required String title}) {}

    show(); // 报错
    show(title: "hi"); // 正确

Q5:Isolate 和线程的区别?

Dart Isolate 是独立内存隔离单元,不共享内存,无锁竞争;

操作系统线程共享内存、需要锁机制

Isolate 通信只能通过 Port 消息传递,适合耗时计算、大数据解析,避免UI卡顿。

详解:

六个维度对比:

关键机制详解

消息传递是“拷贝”而非“引用”在 Isolate 之间发送数据时,默认进行深度拷贝(通过 SendPort 发送)。这意味着数据在发送端修改,不会影响接收端。

> 优化点:对于大型文件(如 Uint8List 图片数据),Dart 允许通过 TransferableTypedData 进行“转移”(Transfer),此时数据所有权移交,发送端将无法再使用该内存,避免了拷贝开销。

Isolate 内部仍然有“线程”

需要特别注意的是:
Isolate 本身并不是轻量级线程。
实际上,Dart VM 会为每个活动的 Isolate 分配一个独立的原生操作系统线程(或绑定到线程池中)。
因此,如果你开启了 10 个 Isolate,操作系统层面可能就有 10 个线程在运行。
Isolate 解决的是内存安全问题,而不是“超轻量”问题。

在 Flutter 开发中怎么选?

UI 主线程(Main Isolate):处理界面渲染和用户交互。绝对不要在这里执行耗时计算(如解析超大 JSON、图片压缩)。

使用 Isolate(推荐 compute 函数):当遇到CPU 密集型任务时,必须使用 Isolate 来计算,防止 UI 卡顿。

代码示例(在 Flutter 中开启新 Isolate):

    import 'package:flutter/foundation.dart';

    // 耗时的计算任务(必须在顶级函数或静态函数中)
    int heavyComputation(int value) {
      int result = 0;
      for (int i = 0; i < 1000000000; i++) {
        result += i;
      }
      return result + value;
    }

    // 调用方式
    void doWork() async {
      // compute 是 Flutter 提供的便捷方法,内部自动创建 Isolate 并传回结果
      final result = await compute(heavyComputation, 10);
      print(result);
    }

总结一句话

Isolate 是唯一的并发方案,它通过牺牲部分通信性能来彻底根除数据竞争和死锁问题,这是 Dart 设计上为了保障 UI 稳定性做出的核心取舍。

在普通业务开发中,大部分异步需求(网络请求、数据库读写)靠 async/await 事件循环即可解决,只有遇到“计算爆炸”时才需要动用 Isolate


Q6:Future 和 Stream 区别?

Future 单次异步结果(一次成功/失败);Stream 多次异步数据流(持续推送数据),支持单订阅/广播订阅,适合长连接、实时数据场景。

详解:

在 Dart(Flutter)中,FutureStream 都是用于处理异步操作的核心对象。它们最本质的区别在于:Future 只返回一次数据(现在或将来),而 Stream 可以返回多次数据(现在和将来)。

我们可以用下面的例子来形象地理解:

  • Future:像是在饿了么下了一个订单。你等待,最终只会收到一个结果:要么是外卖送达(成功数据),要么是订单取消(错误信息)。整个过程只结束一次。

  • Stream:像是打开了抖音直播。主播(数据源)会源源不断地产生内容(数据包),你作为观众(监听者)会持续收到多个数据,直到主播下播(流关闭)。

核心差异对比表

对比维度 Future Stream
返回次数 单次。只能产生一个数据或一个错误,然后结束。 零到多次。可以持续不断地产生多个数据或错误,直到生命周期结束。
监听方式 通过 then() 回调,或使用 await 关键字同步等待(挂起函数)。 通过 listen() 方法添加监听器(Listener),或有 await for 循环遍历。
执行时机 创建后立即开始执行,并交由事件循环处理。 惰性(Lazy)。创建后不会立即执行,只有当**被监听(listen)**时才会开始产生事件。
常用场景 HTTP 网络请求、读取本地文件、数据库单次查询、延迟定时器。 用户点击事件流(onTap)、WebSocket 长连接、文件流读取、传感器数据(GPS)、UI 动画帧。

代码实战与用法

1. Future:一次性结果

无论是成功还是失败,Future 只会触发一次 thencatchError

    // 模拟网络请求
    Future<String> fetchUserData() async {
      await Future.delayed(Duration(seconds: 2));
      return "用户信息";
    }

    void main() {
      fetchUserData().then((data) {
        print(data); // 只打印一次:用户信息
      });

      // 或者使用 await(必须在 async 函数中)
      // String data = await fetchUserData();
    }

2. Stream:持续事件流

Stream 需要持续订阅,每次有数据推送到流中,listen 里的回调都会被执行。

    // 模拟每隔 1 秒产生一次数字
    Stream<int> getNumberStream() async* {
      for (int i = 1; i <= 5; i++) {
        await Future.delayed(Duration(seconds: 1));
        yield i; // 使用 yield 而不是 return 来产生多个值
      }
    }

    void main() {
      getNumberStream().listen((number) {
        print('收到: $number'); // 会打印 5 次(1, 2, 3, 4, 5)
      }, onDone: () {
        print('流已结束'); // 全部数字发送完后触发
      });
    }

两种不同的生成语法(重要)

在 Dart 中,生成 FutureStream 的关键词不同:

  • 生成 Future:使用 async + return

  • 生成 Stream:使用 async*(异步生成器) + yield(产出单个值)或 yield*(产出另一个流的所有值)。

特殊场景:单订阅 vs 广播流

当你在 Flutter 中处理 Stream 时,需要特别注意它的类型,

否则会遇到“Stream has already been listened to”的报错。

  1. 单订阅流(Single-subscription):默认类型。只能被一个监听器监听,类似于“私聊”。一旦开始监听,数据会从头部开始流式发送。适合读取文件或一次性的 HTTP 响应体。

  2. 广播流(Broadcast):可以被多个监听器同时监听,类似于“群聊”或“电视台”。新加入的监听器只能听到加入之后发生的事件,听不到历史事件。适合处理 UI 点击事件(多个按钮监听同一个点击源)或全局状态变化。

如何转换

    // 将普通流变为广播流,允许多个监听者
    Stream<int> broadcastStream = getNumberStream().asBroadcastStream();

实战中的选择建议(Flutter 开发场景)

  • 当你只需要一个结果时,必须用 Future。例如:调用 RESTful API 获取用户 Token、使用 shared_preferences 读取一次配置。如果用 Stream 去做单次请求,会平白增加代码复杂度和内存泄漏风险。

  • 当你需要持续响应交互或更新时,必须用 Stream。例如:

    • 监听 TextField 的输入变化(onChanged)。

    • 使用 Firebase Realtime Database 或 WebSocket 接收服务端推送。

    • 监听动画每一帧的进度(Ticker)。

总结一句话

Future 是“一次性的承诺”,你等着拿结果就好;
Stream是“持续的管道”,你需要一直守着,来一个处理一个。在日常开发中,90% 的业务网络请求用 Future,只有涉及到实时数据刷新或 UI 事件监听时,才请出 Stream(配合 BlocGetX 等状态管理工具使用效果更佳)。