文章

Flutter 面试知识点整理

Flutter 面试知识点整理

本文整理 Flutter 高频面试知识点,按「Dart 语言 → Widget 与三棵树 → 状态管理 → 渲染与布局 → 事件机制 → 线程与异步 → 混合开发 → 性能优化 → 引擎架构」的顺序组织,方便系统性复习。

一、Dart 语言基础

Dart 为什么适合 Flutter?

  • JIT + AOT 双编译:开发期用 JIT(Just-In-Time)编译,支持 Hot Reload(热重载);发布期用 AOT(Ahead-Of-Time)编译成原生机器码,运行性能好、启动快。
  • 单线程事件循环模型:UI 逻辑跑在单一 isolate 上,天然避免多线程访问 UI 的并发问题(对齐主流客户端 UI 框架)。
  • AOT 下无需 JVM/桥接,直接调用 Skia 绘制,避免了 RN 那种 JS Bridge 的序列化开销。
  • 强类型 + 空安全(Null Safety),编译期就能规避大量空指针问题。

finalconst 的区别?

  • final运行时常量,只能赋值一次,值可以在运行期才确定。
  • const编译期常量,值必须在编译时就能确定,且是深度不可变的。
  • Flutter 里大量使用 const 构造 Widget,因为 const Widget 会被规范化(canonicalize)复用同一个实例,重建时可直接跳过,是重要的性能优化点。
1
2
3
final now = DateTime.now();   // ✅ 运行时才知道值
const pi = 3.14;              // ✅ 编译期常量
// const now = DateTime.now(); // ❌ 编译报错

late 关键字的作用?

延迟初始化非空变量:声明时不赋值,但保证使用前一定会赋值;也可用于「懒加载」——被 late 修饰的变量在第一次访问时才执行初始化表达式。

???.??=! 各是什么?

  • ?.:空感知调用,左侧为 null 时整体返回 null。
  • ??:为 null 时取默认值,a ?? b
  • ??=:为 null 时才赋值,a ??= b
  • !:非空断言,告诉编译器”我确定它不为 null”(错了会运行时抛异常)。

mixinextendsimplementswith 的区别?

  • extends:单继承,复用父类实现。
  • implements:实现接口,必须重写所有成员(Dart 中任何类都可当接口)。
  • with(mixin):横向复用代码,多个 mixin 线性化混入,解决单继承的复用局限。
  • 典型场景:class A extends B with M1, M2 implements I1 {},线性化顺序影响 super 调用链。

二、Widget、Element、RenderObject(三棵树)

Flutter 三棵树分别是什么?

作用是否可变创建频率
Widget 树UI 的配置/描述,不可变(immutable)不可变频繁重建、开销小
Element 树Widget 的实例化载体,维护生命周期与关联关系可变、复用尽量复用
RenderObject 树真正负责 布局、绘制、命中测试可变、复用尽量复用
  • Widget 是”图纸”,Element 是”施工队/中间人”,RenderObject 是”真正的房子”。
  • Widget 频繁重建,但 Element / RenderObject 尽可能复用,这是 Flutter 高性能的核心。

为什么需要 Element 这一层?

Widget 不可变且频繁重建,如果每次都重建 RenderObject 会非常昂贵。Element 作为中间层:

  • 持有对 Widget 和 RenderObject 的引用;
  • 负责在 Widget 变化时判断能否复用已有的 Element / RenderObject(通过 canUpdate);
  • 维护上下文(BuildContext 本质就是 Element)。

Widget.canUpdate 的判断逻辑?

1
2
3
4
static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
}

runtimeType 相同 且 key 相同 → 复用旧 Element(只更新配置);否则销毁旧 Element、创建新的。这也是 Key 存在意义的底层依据。

BuildContext 到底是什么?

BuildContext 就是 Element 的抽象接口。build(BuildContext context) 里的 context 指向当前 Widget 对应的 Element,可用来向上查找祖先(如 Theme.of(context)context.findAncestorStateOfType)、获取自身在树中的位置信息(size、渲染对象等)。

三、StatelessWidget / StatefulWidget 与 State

两者区别?

  • StatelessWidget:无内部可变状态,UI 只由外部传入的配置决定。
  • StatefulWidget:持有可变状态,状态放在独立的 State 对象里。为什么状态要单独放在 State 里?因为 Widget 本身不可变、会被频繁重建丢弃,而 State 对象由 Element 持有、跨重建保留

State 的生命周期?

1
2
3
4
5
6
7
8
createState()
  → initState()          // 只调用一次,做初始化、订阅
  → didChangeDependencies() // 依赖的 InheritedWidget 变化时也会调用
  → build()              // 构建 UI(可多次)
  → didUpdateWidget()    // 父 Widget 重建导致配置更新时
  → setState()           // 触发重建
  → deactivate()         // 从树中移除(可能被重新插入)
  → dispose()            // 彻底销毁,释放资源、取消订阅
  • initState:初始化,此时还不能用 context 访问 InheritedWidget(应放到 didChangeDependencies)。
  • dispose必须在这里释放 controller、动画、流订阅、定时器等,否则内存泄漏。

setState 的原理?

setState 做两件事:

  1. 执行传入的回调,修改状态字段;
  2. 调用 element.markNeedsBuild(),把当前 Element 标记为 dirty,加入 BuildOwner 的 dirty 列表。

下一帧 WidgetsBindingdrawFrame 会重建所有 dirty 的 Element(rebuild),触发 build

注意

  • setState 只标记当前 Element 子树需要重建,不会重建整棵树;
  • 回调里应只做同步的状态修改,异步操作要在 mounted 为 true 时才调用 setState
  • 频繁/大范围 setState 是常见性能问题,应尽量下沉状态、缩小重建范围。

四、Key 的作用

什么时候需要 Key?

同类型 Widget 在列表中顺序发生变化、增删时,仅靠 runtimeType 无法正确匹配新旧 Element,会导致状态错乱(比如带勾选状态的列表项被复用到错误的位置)。此时用 Key 帮助 Element 精确匹配。

Key 的分类?

  • LocalKey(局部,用于同一父节点下的兄弟节点区分):
    • ValueKey:用一个值区分(如 id)。
    • ObjectKey:用对象本身区分。
    • UniqueKey:每次都唯一(强制不复用)。
  • GlobalKey(全局唯一):
    • 可跨树访问 State / Element / RenderObject(globalKey.currentStatecurrentContext);
    • 用于跨位置保留 State、拿到 Form/子组件的状态;
    • 开销较大,滥用会影响性能,非必要不用。

五、状态管理

为什么需要状态管理框架?

setState 只能管理组件自身局部状态。当状态需要跨组件、跨层级共享,靠层层传参(prop drilling)会非常繁琐且难维护,因此引入状态管理方案。

常见方案对比

方案核心思想特点
InheritedWidget官方底层,自上而下高效共享数据是众多方案的基础,直接用较繁琐
Provider对 InheritedWidget 的封装 + ChangeNotifier官方推荐、轻量、易上手
RiverpodProvider 作者重写,编译期安全、不依赖 BuildContext解决 Provider 的诸多痛点
Bloc / Cubit基于事件流(Stream),单向数据流规范、可测试性强、模板代码多
GetX状态 + 路由 + 依赖注入一体上手快、侵入性强、生态争议

InheritedWidget 的原理?

  • 它把数据”注入”到 Element 树中,子孙节点通过 context.dependOnInheritedWidgetOfExactType<T>() 获取数据,同时把自己登记为依赖者
  • 当 InheritedWidget 的数据变化(updateShouldNotify 返回 true)时,会精准通知所有依赖它的 Element 重建,而不是重建整棵子树。
  • Theme.of(context)MediaQuery.of(context) 都是基于它实现的。

六、渲染与布局机制

Flutter 的渲染流水线(一帧做了什么)?

Vsync 信号到来后,SchedulerBinding 触发 drawFrame,依次执行:

  1. Build:重建 dirty 的 Widget → 更新 Element / RenderObject。
  2. Layout(布局):约束自上而下传递,尺寸自下而上返回。
  3. Paint(绘制):生成 Layer 树,记录绘制指令。
  4. Composite(合成):将 Layer 树合成,交给 Engine。
  5. Rasterize(光栅化):Engine 用 Skia/Impeller 光栅化,最终上屏。

Flutter 的布局约束原则?

一句话口诀:Constraints go down, Sizes go up, Parent sets position.

  • 约束向下:父节点把 BoxConstraints(min/max width/height)传给子节点;
  • 尺寸向上:子节点在约束范围内确定自己的尺寸,返回给父节点;
  • 父定位置:父节点根据自己的布局逻辑决定子节点的位置(offset)。

理解这条原则能解释大量布局问题,比如「为什么 Container 在无约束下会尽可能大/小」「为什么要用 Expanded / Flexible」。

为什么会出现”无边界约束(unbounded constraints)”报错?

当一个可滚动(如 ListView)或弹性布局在主轴方向约束为无限大时,子组件无法确定尺寸就会报错。常见于 Column 里直接放 ListView(Column 主轴高度无限),解决方式是用 Expanded / Flexible 给出有界约束,或给 ListView 指定 shrinkWrap: true

RepaintBoundary 有什么用?

将子树的绘制隔离到独立的 Layer,使其重绘不影响其它区域、其它区域重绘也不影响它。对于频繁重绘的局部(如动画)能显著减少不必要的 repaint。滥用会增加 Layer 数量和内存,需权衡。

七、事件处理机制

Flutter 的事件分发流程?

  1. Engine 收到触摸事件(PointerEvent)传给 Framework。
  2. 命中测试(Hit Test):从 RenderView 开始自上而下遍历 RenderObject 树,收集命中路径上的所有 RenderObject 到 HitTestResult
  3. 事件派发:沿命中结果从内到外(子到父)派发事件(handleEvent)。

GestureArena(手势竞技场)是什么?

当多个手势识别器同时命中(如同时有点击和滑动),它们会加入”竞技场”竞争:

  • 每个识别器根据后续事件”声明胜出(accept)”或”放弃(reject)”;
  • 最终由一个胜出者独占该次手势,其余被拒绝;
  • 这套机制解决了手势冲突(如 ListView 内的按钮点击 vs 列表滑动)。

八、线程与异步(Isolate / Event Loop)

Dart 是单线程的,怎么处理异步?

Dart 单线程通过事件循环(Event Loop) 实现异步非阻塞。每个 isolate 有两个队列:

  • Microtask Queue(微任务队列):优先级高,如 scheduleMicrotaskFuture.then 回调。
  • Event Queue(事件队列):I/O、定时器、手势、Future 等。

执行顺序:先清空当前微任务队列,再取一个事件队列任务;每处理完一个事件又会先清空微任务队列。微任务永远优先于事件任务

Future 与 async/await?

  • Future 表示一个异步操作的最终结果(未来完成/失败)。
  • async 标记异步函数,返回 Futureawait 挂起当前函数、等待 Future 完成,但不阻塞线程(让出控制权给事件循环)。
1
2
3
4
Future<String> fetch() async {
  final data = await http.get(url);  // 不阻塞,让出主线程
  return data.body;
}

Future 与 Stream 的区别?

  • Future单个异步结果(一次性)。
  • Stream多个异步结果组成的序列(可持续产出),有单订阅流和广播流;配合 StreamBuilderasync*/yield 使用。

Isolate 是什么?和线程有什么区别?

  • Isolate 是 Dart 的并发单元,各自独立的内存堆、独立的事件循环,不共享内存
  • 与传统线程最大区别:Isolate 之间不共享内存,只能通过 SendPort/ReceivePort 消息传递通信,因此没有锁和竞态问题。
  • 使用场景:CPU 密集型任务(大 JSON 解析、图片处理、加解密),避免阻塞 UI isolate 造成掉帧。可用 compute() 快捷创建。
1
2
// compute 会在新 isolate 中执行 heavyParse,完成后把结果传回
final result = await compute(heavyParse, jsonString);

九、混合开发与原生通信

Flutter 如何与原生(Android/iOS)通信?

通过 Platform Channel,消息在 Flutter 与原生之间以二进制编解码传递,异步

Channel用途
MethodChannel方法调用(最常用),Flutter 调原生方法并拿返回值,反之亦可
EventChannel事件流,原生向 Flutter 持续发送数据(如传感器、电量监听)
BasicMessageChannel传递字符串或半结构化消息,双向
  • Channel 通过 name(字符串标识) 区分,Flutter 端和原生端 name 必须一致。
  • 通信默认在平台主线程处理,注意不要在原生侧做耗时操作阻塞。
1
2
const channel = MethodChannel('com.example/battery');
final level = await channel.invokeMethod<int>('getBatteryLevel');

Flutter 三种混合栈接入方式?

  • 纯 Flutter 应用;
  • 原生为主、局部嵌入 Flutter(add-to-app,FlutterEngine / FlutterFragment);
  • 混合栈路由管理(如闲鱼 FlutterBoost),解决原生页面与 Flutter 页面互相跳转、栈管理与内存问题。

十、性能优化

常见性能优化手段?

  1. 多用 const 构造:命中 const 复用,重建时直接跳过。
  2. 缩小 setState 范围:把状态下沉到最小的 StatefulWidget,避免大范围重建。
  3. 列表用 ListView.builder / SliverList:按需懒加载,而非一次性构建所有子项。
  4. RepaintBoundary 隔离频繁重绘区域(如动画)。
  5. 避免在 build 里做耗时操作 / 创建大对象 / 新建闭包,build 会被频繁调用。
  6. 图片优化cacheWidth/cacheHeight 控制解码尺寸,及时释放,使用缓存库。
  7. CPU 密集任务放到 Isolatecompute),避免阻塞 UI。
  8. 减少不必要的透明度 / 裁剪 / 阴影saveLayer 开销大)。
  9. DevTools 的 Performance / Timeline 定位 jank,观察 Raster 与 UI 线程耗时。

如何定位卡顿(Jank)?

  • Flutter DevTools → Performance,查看每帧是否超过 16.6ms(60fps)
  • 区分是 UI 线程(build/layout/paint 慢)还是 Raster 线程(光栅化慢,通常是绘制过重);
  • Timeline 找出超时帧对应的耗时方法,针对性优化。

十一、引擎架构与其它高频问题

Flutter 的整体架构分层?

  • Framework 层(Dart):Material/Cupertino、Widgets、Rendering、Animation、Painting 等,我们平时写的代码。
  • Engine 层(C/C++):核心是 Skia / Impeller(图形)Dart Runtime、文本排版、Platform Channel 等。
  • Embedder 层(平台相关):把引擎嵌入到各平台(Android/iOS/Web/桌面),负责线程管理、事件循环、Surface。

Skia 和 Impeller 的关系?

  • Skia:Flutter 长期使用的 2D 图形库,跨平台绘制。
  • Impeller:Flutter 新一代渲染引擎,预编译 shader,解决 Skia 首次运行时 shader 编译造成的卡顿(shader jank)。iOS 已默认,Android 逐步默认。

Hot Reload 与 Hot Restart 的区别?

  • Hot Reload(热重载):把改动的 Dart 代码增量注入正在运行的 Dart VM,保留应用当前状态,重建 Widget 树;不会重跑 main() / initState。适合改 UI。
  • Hot Restart(热重启):重启 Dart VM,丢失状态,重跑 main();比重新编译安装快。
  • 依赖 JIT 编译,Release(AOT)模式下不支持热重载

Widget 重建 ≠ RenderObject 重建

面试常考的关键结论:build 被调用、Widget 重新创建,不代表 Element 和 RenderObject 被重建。只要 canUpdate 通过,Element 复用,RenderObject 只是被更新属性(updateRenderObject),这正是 Flutter 频繁 rebuild 却依然高性能的原因。

Flutter 的优缺点?

  • 优点:一套代码多端一致(自绘 UI,不依赖平台控件)、性能接近原生、Hot Reload 开发效率高、UI 表现力强。
  • 缺点:包体积偏大、部分平台特性仍需原生桥接、动态化能力弱(AOT 限制)、大型混合栈接入有成本。

十二、Dart 进阶

普通构造、命名构造、factory 构造的区别?

  • 普通/命名构造:必然创建一个新实例(Point(1,2)Point.origin())。
  • factory 工厂构造不强制创建新对象,可以返回缓存实例、子类实例或已有实例,因此常用于单例、缓存池、根据参数返回不同子类
1
2
3
4
5
6
7
class Logger {
  static final Map<String, Logger> _cache = {};
  final String name;
  Logger._internal(this.name);            // 私有命名构造
  factory Logger(String name) =>          // 工厂构造:命中缓存则复用
      _cache.putIfAbsent(name, () => Logger._internal(name));
}

Dart 中如何实现单例?

1
2
3
4
5
class Singleton {
  Singleton._();                          // 私有构造,外部无法 new
  static final Singleton instance = Singleton._();
  factory Singleton() => instance;        // 让 Singleton() 也返回同一实例
}

static final 保证懒汉/饿汉且线程安全(Dart 单 isolate 无并发问题)。

==hashCode 为什么要一起重写?

判断两个对象「值相等」时需重写 ==;而 Set/Map 依赖 hashCode 分桶,只重写 == 不重写 hashCode 会导致 Set 去重、Map 查找失效。推荐用 Object.hash(a, b, ...) 生成,或直接用 freezed/equatable 自动生成。

extension(扩展方法)?

在不修改/继承原类的前提下给已有类型「加方法」,编译期静态解析:

1
2
3
4
extension StringExt on String {
  bool get isEmail => contains('@');
}
'a@b.com'.isEmail; // true

级联操作符 .. 的作用?

对同一对象连续调用多个方法/赋值并返回对象本身,避免重复写变量名:

1
2
3
4
final p = Paint()
  ..color = Colors.red
  ..strokeWidth = 2
  ..style = PaintingStyle.stroke;

深拷贝与浅拷贝?

Dart 没有内置深拷贝List.from / Map.from / ...spread 都是浅拷贝(元素仍是引用)。深拷贝需手动逐层复制,或 jsonDecode(jsonEncode(obj))(仅适用可序列化对象),或用 freezedcopyWith

sync* / async*yield

  • sync* + yield → 惰性生成 Iterable(同步、按需产出)。
  • async* + yield → 生成 Stream(异步、可持续产出),yield* 委托给另一个序列。

十三、动画机制

隐式动画 vs 显式动画?

  • 隐式动画(Implicit)AnimatedContainerAnimatedOpacityAnimatedPadding 等,只要改变属性并给 duration,框架自动补间,无需管理控制器,简单场景首选。
  • 显式动画(Explicit):手动用 AnimationController 控制,可暂停/反向/重复/监听进度,复杂动画用它。

显式动画的核心三件套?

  1. AnimationController:控制动画的开始/停止/时长/方向,值域 0~1;需要 vsyncTickerProvider)。
  2. Tween:定义补间范围(Tween<double>(begin: 0, end: 100)),tween.animate(controller) 生成 Animation
  3. AnimatedBuilder / Transition:监听 Animation 重建局部 UI(用 AnimatedBuilderchild 参数缓存不变子树,减少重建)。
1
2
3
4
5
6
class _S extends State<Demo> with SingleTickerProviderStateMixin {
  late final _c = AnimationController(vsync: this, duration: 1.seconds);
  late final _a = Tween(begin: 0.0, end: 1.0).animate(_c);
  @override void initState() { super.initState(); _c.repeat(reverse: true); }
  @override void dispose() { _c.dispose(); super.dispose(); } // 必须 dispose
}

Ticker 与 vsync 是什么?为什么要 SingleTickerProviderStateMixin

  • Ticker 在每一帧回调(对齐 Vsync 信号),驱动 AnimationController 更新值。
  • vsync 传入一个 TickerProvider,让 Ticker 与页面绑定:页面不可见时 Ticker 暂停,避免离屏动画白白消耗资源。
  • 单个控制器用 SingleTickerProviderStateMixin,多个用 TickerProviderStateMixin

十四、路由与导航

基于栈,命令式增删页面:

  • Navigator.push(context, MaterialPageRoute(builder: ...)) 入栈;
  • Navigator.pop(context, result) 出栈并可回传结果;
  • 命名路由 Navigator.pushNamed(context, '/detail', arguments: ...),在 routes / onGenerateRoute 注册。

Router + Pages 声明式描述整个路由栈(栈由状态驱动),解决 Web 深链、浏览器前进后退、恢复路由状态 等 1.0 难以处理的场景。核心组件:

  • RouterDelegate:根据 App 状态构建 Navigatorpages 列表;
  • RouteInformationParser:URL ↔ 应用状态互转。

因为原生 2.0 模板代码多,实际项目多用官方封装 go_router(声明式路由表 + 深链 + 重定向 + 嵌套路由)。

页面间传参与回传结果?

  • 传参:构造函数直接传、命名路由 arguments
  • 回传:await Navigator.push(...) 拿到 pop(result) 的返回值。

十五、网络请求与数据序列化

http 与 dio 的选择?

  • http:官方轻量包,够用但功能少。
  • dio:功能全,支持拦截器(统一加 token/日志/错误处理)、超时、取消(CancelToken)、文件上传下载、FormData、全局配置,中大型项目主流选择。

JSON 序列化的三种方式?

方式特点
手写 fromJson/toJson简单直接,字段多时易错、维护累
json_serializable注解 + build_runner 代码生成,减少样板
freezed生成不可变数据类 + copyWith + ==/hashCode + union(sealed),配合状态管理很香

Dart 没有运行时反射(release 下),所以主流做法是编译期代码生成而非反射解析。

十六、本地存储

方案适用
SharedPreferences轻量 KV(配置、开关、token),底层是 plist/xml
sqflite关系型 SQLite,结构化、复杂查询
Hive / Isar纯 Dart NoSQL,读写快、无需原生依赖
driftSQLite 之上的类型安全 ORM,编译期检查 SQL
flutter_secure_storage加密存储敏感数据(Keychain/Keystore)
path_provider获取文档/缓存/临时目录路径,配合文件读写

十七、App 与页面生命周期

如何监听 App 前后台切换?

实现 WidgetsBindingObserver,注册后重写 didChangeAppLifecycleState

1
enum AppLifecycleState { resumed, inactive, paused, hidden, detached }
  • resumed:可见可交互(前台);
  • inactive:非活跃(如来电、下拉通知栏);
  • paused:退到后台、不可见;
  • detached:还在运行但已脱离视图;hidden:所有视图隐藏(较新增加)。

如何感知某个页面被覆盖/回到前台?

RouteObserver<PageRoute> + RouteAware,重写 didPush / didPopNext(当前页重新可见)/ didPushNext(被新页覆盖)。

十八、Sliver 与滚动

为什么需要 Sliver?

当页面里多个可滚动区域(AppBar 折叠 + 列表 + 网格)需要共享同一个滚动时,用 CustomScrollView 承载多个 Sliver,统一滚动、按需懒加载。

常见 Sliver:SliverAppBar(折叠头)、SliverList / SliverGrid.builder 懒加载)、SliverPersistentHeader(吸顶)、SliverToBoxAdapter(把普通 Widget 塞进 Sliver)、SliverFillRemaining

ListView.builder 的复用原理?

ListView.builder 只构建视口内 + 缓存区(cacheExtent)的子项;滑出屏幕的 Element / RenderObject 会被回收复用给即将进入的项(类似原生 RecyclerView 的复用),因此长列表内存平稳、性能好。列表项顺序会变时记得加 Key

十九、异步进阶

Completer 有什么用?

手动创建并控制一个 Future 的完成时机:把「基于回调的 API」桥接成 Future

1
2
3
4
5
Future<T> toFuture(void Function(Function(T)) reg) {
  final c = Completer<T>();
  reg((v) => c.complete(v)); // 回调里完成
  return c.future;
}

并发等待多个异步?

  • Future.wait([f1, f2])全部完成才返回(结果列表),任一失败即抛错;
  • Future.any([...])最先完成的那个;
  • 需要各自容错时对每个 Future 单独 catchError

Zone 是什么?

Zone 是异步执行的「上下文/沙箱」,可用来捕获整个异步调用链中未处理的异常runZonedGuarded),或提供区域局部变量。常见于 App 入口统一兜底、埋点错误上报。

微任务与事件任务的顺序(经典输出题)

1
2
3
4
5
6
7
print('A');
Future(() => print('B'));           // 事件队列
Future.microtask(() => print('C')); // 微任务队列
Future(() => print('D')).then((_) => print('E'));
print('F');
// 输出:A F C B D E
// 同步先跑(A F)→清空微任务(C)→依次取事件任务(B, 然后 D 及其 .then E)

二十、测试

  • 单元测试test(),纯 Dart 逻辑;依赖用 mockito / mocktail 打桩。
  • Widget 测试testWidgets() + WidgetTesterpumpWidget 挂载、pump/pumpAndSettle 推进帧,find + expect 断言。
  • 集成测试integration_test,真机/模拟器上跑完整流程。
1
2
3
4
5
6
testWidgets('计数器 +1', (tester) async {
  await tester.pumpWidget(const MyApp());
  await tester.tap(find.byIcon(Icons.add));
  await tester.pump();
  expect(find.text('1'), findsOneWidget);
});

二十一、构建、打包与包体积优化

Debug / Profile / Release 三种模式?

  • Debug:JIT,支持热重载与断言,含调试信息,体积大、性能低。
  • Profile:AOT,保留部分性能分析工具,用于性能测试(不能装模拟器上测)。
  • Release:AOT,去断言/调试信息,最优性能与体积,上线用。

减小包体积的手段?

  • 按 ABI 拆分:flutter build apk --split-per-abi,或发 appbundle 让商店按机型下发;
  • 代码混淆并剥离符号:--obfuscate --split-debug-info=<dir>
  • 图标 tree-shaking(默认对 IconData 生效)、压缩图片、移除无用资源与字体子集化;
  • 谨慎引入大依赖,检查 flutter build --analyze-size

flavor / scheme?

多环境/多渠道(dev/staging/prod、不同包名/图标/配置)用 Android 的 productFlavors 与 iOS 的 scheme/configuration,Flutter 侧用 --flavor + --dart-define 注入环境变量。

二十二、屏幕适配

  • MediaQuery:拿屏幕尺寸、像素密度、安全区(padding/viewInsets,如键盘、刘海)。
  • LayoutBuilder:拿到父约束再决定布局,做响应式。
  • OrientationBuilder:横竖屏切换。
  • 相对布局:Expanded/Flexible/FractionallySizedBox/AspectRatio
  • 三方:flutter_screenutil(按设计稿基准尺寸等比缩放 .w/.h/.sp)。

二十三、手撕代码 / 原理深挖

为什么 build 方法里不能有副作用?

build 会被频繁、多次调用(每次 rebuild),且顺序、次数不可预期。如果在里面发网络请求、订阅、启动定时器,会造成重复请求、泄漏、状态混乱。副作用应放到 initState / didChangeDependencies / 事件回调里。

手写防抖(Debounce)与节流(Throttle)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 防抖:停止触发 delay 后才执行(搜索框输入)
class Debouncer {
  final Duration delay;
  Timer? _t;
  Debouncer(this.delay);
  void call(void Function() action) {
    _t?.cancel();
    _t = Timer(delay, action);
  }
  void dispose() => _t?.cancel();
}

// 节流:一段时间内最多执行一次(滑动/按钮防连点)
class Throttler {
  final Duration interval;
  Timer? _t;
  Throttler(this.interval);
  void call(void Function() action) {
    if (_t?.isActive ?? false) return;
    action();
    _t = Timer(interval, () {});
  }
}

手写一个简易 EventBus(基于广播流)

1
2
3
4
5
6
class EventBus {
  final _controller = StreamController.broadcast();
  Stream<T> on<T>() => _controller.stream.where((e) => e is T).cast<T>();
  void emit(Object event) => _controller.add(event);
  void dispose() => _controller.close();
}

setState 之后为什么不是「立即」刷新?

setState 只是把 Element 标记为 dirty,真正的重建发生在下一帧的 drawFrame。所以连续多次 setState 会被合并到同一帧处理,不会触发多次 build——这也是为什么在异步回调里 setState 要先判断 mounted

const 构造函数为什么能优化性能?

const Widget 在编译期就完成实例化并被规范化(同值只存一份),父级 rebuild 时,const 子 Widget 引用不变,Widget.canUpdate 判定可复用,直接跳过该子树的重建。这是「无脑加 const」能提性能的原理。


以上为核心高频点的速记版。建议对每个点都能「结合源码或原理讲清楚 why」,而不仅是背结论——面试官通常会顺着三棵树、渲染流水线、事件分发、Isolate 这几条主线深挖。

本文由作者按照 CC BY 4.0 进行授权