Flutter 面试题汇总

0 阅读32分钟

1. Flutter 是什么?和 React Native、Weex 等有什么本质区别?

  • Flutter 是 Google 的跨平台 UI 框架,使用 Dart 编写,通过自绘引擎直接绘制 UI(不依赖系统原生控件)。早期引擎以 Skia 为主,新一代 Impeller 已在 iOS/Android 等逐步成为默认。
  • 与 RN/Weex 对比
对比项FlutterReact Native / Weex
渲染自绘(Skia/Impeller 画像素)JS 描述 → 映射为原生 View
一致性各平台 UI 一致不同平台原生控件可能有差异
通信无 JS-Native Bridge依赖 Bridge(RN 新架构为 JSI+Fabric)
性能动画/滚动易 60fps受 Bridge/原生能力影响
包体积自带引擎,包偏大依赖系统,包相对小

2. 为什么 Flutter 选择 Dart 而不是 JavaScript?

  • AOT 编译:可编译为原生 ARM/x64,发布包完全 AOT,启动和运行性能好。
  • JIT + 开发体验:开发时支持 JIT,配合 Hot Reload 快速刷新。
  • 单线程模型清晰:async/await、Isolate 明确,无 JS 多线程历史包袱。
  • 可控:Dart 由 Google 主导,与 Flutter 迭代一致;JS 受 ECMA/引擎生态影响。
  • 类型安全:强类型利于大型项目和工具链(重构、补全、静态分析)。

小结:性能(AOT/JIT)+ 单线程清晰 + 可控 + 类型安全。


3. 说说 Flutter 的架构分层(从上到下)

层级语言主要内容
Framework 层Dart我们写的代码:Widget、渲染、动画、手势、Material/Cupertino 组件
Engine 层C/C++渲染引擎(Skia/Impeller)、Dart 运行时、文本排版、Dart VM
Embedder 层平台原生对接各平台:窗口、输入、线程、平台插件

应用只直接接触 Framework;Engine 和 Embedder 由 Flutter SDK 与各平台工程提供。

架构图(自上而下):

sequenceDiagram
    participant App as 应用 / Widget
    participant F as Framework 层 (Dart)
    participant E as Engine 层 (C/C++)
    participant Em as Embedder 层 (平台)

    App->>F: 调用
    F->>E: 渲染 / 运行时
    E->>Em: 绘制 / 系统 API
    Em->>Em: iOS · Android · Web · Desktop

数据/调用方向:约束与配置从 Framework 往下传;事件从 Embedder 往上 → Engine → Framework → Widget;渲染为 Framework 生成 Layer/Scene → Engine 绘制 → Embedder 交给平台显示。
小结:你的 App → Framework → Engine → Embedder → 操作系统/GPU


4. Impeller 和 Skia 有什么区别?当前 Flutter 渲染引擎的现状?

  • Skia:Flutter 早期使用的 2D 渲染引擎(也是 Chrome、Android 等在用),成熟稳定,但在部分场景下存在运行时着色器编译导致的卡顿(shader jank)。
  • Impeller:Flutter 的新一代渲染引擎,主要改进包括:
    • 预编译着色器(AOT):在引擎构建时编译,避免首帧或新效果时的编译卡顿。
    • 更适配现代图形 API:iOS/macOS 用 Metal,Android 用 Vulkan(无 Vulkan 时回退 OpenGL)。
    • 显式资源管理与并发优化,便于预测性能。
  • 当前现状
    • iOS:Impeller 已是默认,不可切回 Skia。
    • Android:API 29+ 且支持 Vulkan 的设备默认 Impeller。
    • Web:仍以 Skia(CanvasKit/skwasm)为主。
    • macOS/Windows/Linux:Impeller 在推进或实验阶段。
  • 小结:Flutter 正在从 Skia 向 Impeller 迁移,减轻 jank、更好利用各平台 GPU。

5. Flutter 和原生比,你觉得优缺点是什么?

  • 优点:一套代码多端、UI 一致、热重载效率高、自绘性能可控、Dart 类型安全。
  • 缺点:包体积偏大、强依赖自绘引擎(Skia/Impeller)、深度原生能力要写 Channel、生态和成熟度不如原生;复杂混合栈要额外方案。
  • 适用:中高 UI 一致性要求的 App、快速迭代的 To C/内部工具;强依赖最新原生特性的场景要评估。

6. 什么是 Widget?Widget 和 Element、RenderObject 的关系?

Widget 是 Flutter 里对 UI 的不可变配置描述(“我要一个什么样的界面”),是声明式的配置数据,不是最终画在屏幕上的东西。常见例子:Text('hello')Container(color: Colors.blue) 都是 Widget。

三者的角色:

概念是什么主要职责
Widget不可变的配置/描述描述“要什么 UI”(数据)
ElementWidget 在树上的“实例 + 管家”对应一个 Widget,管生命周期子节点更新时对比 Widget 决定复用还是重建
RenderObject真正参与布局和绘制的对象layout(算尺寸位置)、paint(画到屏幕)

一句话:Widget 是配置,Element 是管家,RenderObject 是干活的。关系:Widget → Element →(可能有)RenderObject。

谁有 RenderObject?

  • 通常没有:Container、Padding、Center 等组合型,由子节点来画。
  • :Text、Image、Row、Column 等,对应 RenderParagraph、RenderFlex 等。

树结构关系图(Widget → Element → RenderObject 对应):

sequenceDiagram
    participant W as Widget 树
    participant E as Element 树
    participant R as RenderObject 树

    W->>E: 每个 Widget 对应一个 Element
    E->>E: 管生命周期、子节点、对比复用
    E->>R: 部分 Element 对应 RenderObject(如 Text/Row)
    Note over W,R: Container 等组合型无 RenderObject,由子节点绘制

更新时流程

  1. setState / 父 rebuild → 触发 build,生成新 Widget 树。
  2. Element 对比新旧 Widget(同位置 + Key 一致则复用)。
  3. 复用的 Element 关联的 RenderObject 做 markNeedsLayout / paint。
  4. 下一帧 layout + paint 上屏。

7. StatelessWidget 和 StatefulWidget 的区别?使用场景?

对比项StatelessWidgetStatefulWidget
状态无内部可变状态有 State 保存可变状态
build 依赖仅构造函数参数参数 + State 内数据
更新方式父重建则重建setState() 触发重建
典型场景纯展示、静态内容、无交互有交互、数据变化、表单、动画

选择:能不用状态就不用,优先 StatelessWidget;需要状态再用 StatefulWidget 或状态管理方案。


8. StatefulWidget 的 State 生命周期(常用方法顺序)?

顺序方法调用时机
1createState()StatefulWidget 被创建时,创建对应 State
2initState()State 第一次插入树时,只调一次;做初始化;不能在这里 setState
3didChangeDependencies()initState 之后立即调一次;之后依赖的 InheritedWidget 变化时再调
4build()构建 UI,可多次调用(setState、父 rebuild、依赖更新等)
setState()你主动调用,触发下一帧 build()
5didUpdateWidget(oldWidget)父组件 rebuild 并传入新的 StatefulWidget 时;可对比 oldWidget 做逻辑
6deactivate()State 从树上暂时移除时(如路由替换,可能还会再插入)
7dispose()State 永久移除时,只调一次;必须在这里 dispose Controller、取消订阅

生命周期流程图:

sequenceDiagram
    participant S as State

    S->>S: createState()
    S->>S: initState()
    S->>S: didChangeDependencies()
    S->>S: build()
    Note over S: setState/依赖变化 → 再 didChangeDependencies → build
    Note over S: 父传入新 Widget → didUpdateWidget() → build
    Note over S: 从树移除 → deactivate() → dispose()

只调一次createStateinitStatedispose可能多次didChangeDependenciesbuilddidUpdateWidgetdeactivate

常见考点initState 里不能 setState(此时还未挂载完成);异步回调里 setState 前要 mounted 判断;dispose 里必须释放 Controller、StreamSubscription、Timer、removeObserver,否则易内存泄漏。


9. 什么是 Key?LocalKey 和 GlobalKey 区别?什么时候必须用 Key?

  • Key:在 Widget 树中标识 Widget,用于 Element 复用。同类型同父节点下,Key 相同则复用 Element,否则可能复用错导致状态错乱。
  • LocalKey:在同一父节点下唯一即可(如 ValueKey、ObjectKey、UniqueKey)。
  • GlobalKey:全局唯一,可用来跨组件访问 State 或 Element(如获取 State 调方法、获取 RenderBox 做坐标)。

必须用 Key 的典型场景:列表增删改(尤其是带状态的列表项)、有状态的子 Widget 顺序/数量会变时,给列表项设 ValueKey(item.id)(或 ObjectKey、UniqueKey)等,避免 Flutter 按位置复用错 Element。


10. Flutter 的布局模型是怎样的?约束是怎么向下传递的?

  • 约束驱动:父组件给子组件 BoxConstraints(min/max width/height),子组件在约束内选一个尺寸,再决定子子的约束。
  • 自顶向下传约束自底向上返尺寸;父再根据子的尺寸来摆放(如 Row 根据子宽度排布)。
  • 常见:紧约束(min=max)必须填满;松约束(如 max 为无限)子可自己选尺寸。

约束与尺寸流向:

sequenceDiagram
    participant P as 父 RenderObject
    participant C as 子 RenderObject

    P->>C: 传递 BoxConstraints (min/max 宽高)
    C->>P: 返回 Size (选定的宽高)
    P->>P: 根据子 Size 摆放 → 布局结果

11. 常用布局 Widget 有哪些?Expanded 和 Flexible 区别?

  • 单子:Container、Center、Padding、Align、SizedBox、AspectRatio 等。
  • 多子:Row、Column、Stack(层叠)、ListView、GridView、Wrap(流式)。
  • Expanded:必须是 Row/Column/Flex 的子组件,flex 默认为 1,在主轴方向占满剩余空间(强制填满)。
  • Flexible:可设置 flexFit(tight/loose),Flexible(fit: FlexFit.loose) 只占需要的空间,不强制填满;Expanded 等价于 Flexible(fit: FlexFit.tight)。

12. 如何实现一个 0.5 高度的分割线?为什么不能直接写 height: 0.5?

为什么不能直接 height: 0.5?
布局会做像素取整,0.5 可能被当成 0 或 1;且部分设备上 0.5 物理像素也不一定可见。

常见做法

  • TransformCustomPaint:画一条 0.5 逻辑像素的线。
  • Divider + Transform.scale:在 y 方向缩小。
  • SizedBox(height: 1) + Container + Transform 压成 0.5。
  • CustomPaint:画 1px 线,用颜色透明度或线宽控制视觉细线。

考点:理解约束与取整 + 会用 Transform / CustomPaint 做细线。


13. 除了 setState,你还用过哪些状态管理?类型与对比?

一、状态管理工具的类型

  • 作用范围:本地/组件级(setState) vs 全局/应用级(Provider、Riverpod、Bloc、GetX、Redux)。
  • 更新方式:命令式(主动调用更新) vs 响应式(数据变化自动重建)。
  • 架构风格:InheritedWidget 系(Provider、Riverpod)、事件/单向流(Bloc、Redux)、一体化(GetX)。

二、不同状态管理工具对比图

(多维对比表,便于横向看差异)

方案作用范围更新方式典型 API / 用法适用场景
setState组件内命令式setState(() => _x = v)单组件、逻辑简单
Provider子树/全局响应式Provider + context.watch/read + notifyListeners()中小项目、入门
Riverpod全局响应式Provider 定义 + ref.watch/read,不依赖 context中大型、可测试
Bloc/Cubit子树/全局命令式(emit)BlocProvider + BlocBuilder + emit(state)复杂状态机、易测试
GetX全局响应式(Obx)/命令式(GetBuilder)GetxController.obsObxGet.put小项目、快速开发
Redux全局命令式(dispatch)Store + dispatch(action) + reducer超大型、多端一致

三、选型建议

入门用 Provider(第 13、40 题);中大型用 Riverpod(第 22 题);复杂状态机用 Bloc(第 15 题);小项目用 GetX(第 54~58 题);多端一致用 Redux。按团队规模、可测试性、是否要强约束选。


14. Provider 大致原理?如何把数据“提供给”子树?

基于 InheritedWidget:树上方用 Provider<T> 包一层,子节点 context.watch<T>() / context.read<T>() 取 T。watch 会随 notifyListeners() 重建,read 只取一次不重建。ChangeNotifier 提供 notifyListeners;MultiProvider 组合多 Provider;Consumer/Selector 缩小监听范围。底层原理见第 90 题


15. 说说 Bloc 的 Bloc/Cubit + Event + State 模型

一、三个核心概念

  • State(状态):当前页/模块的数据与界面状态(如列表数据、加载中/成功/失败、选中的 tab)。State 是不可变的,每次变化都是新对象,Bloc/Cubit 通过 emit(newState) 发出新状态。
  • Event(事件):用户操作或系统触发的**“发生了什么事”(如点击刷新、下拉、登录、错误重试)。只有 Bloc 有 Event 层;Cubit 没有 Event,外部直接调 Cubit 的方法**。
  • Bloc / Cubit业务逻辑所在,根据“输入”(Event 或方法参数)和当前 State 算出下一 Stateemit,UI 只监听 State 建界面,不写业务逻辑。

二、Bloc 和 Cubit 的区别

对比项CubitBloc
输入直接调方法(如 cubit.loadData()Event(如 bloc.add(LoadDataEvent())
内部方法里根据参数 + 当前 state 算新 state,emit(state)on<Event>mapEventToState 里根据 event + 当前 state 算新 state,emit(state)
事件来源不区分“谁触发的”,只看方法参数每个 Event 是独立类型,可追溯“点了什么、发了什么事件”
适用状态机简单、触发方式少事件多、需要区分来源、方便日志/调试/测试

三、数据流(单向)

Cubit:  UI/业务 调用方法  →  Cubit 内计算  →  emit(newState)  →  BlocBuilder 拿到 state 重建 UI
Bloc:   UI/业务  add(Event)  →  Bloc 的 on<Event> 里计算  →  emit(newState)  →  BlocBuilder 拿到 state 重建 UI
  • 原则:UI 只负责发 Event / 调方法根据 State 渲染在 UI 里写“加载中改哪个变量、失败弹什么”等逻辑,都放在 Bloc/Cubit 里,通过 State 表达出来。
  • 易测试:给定「当前 State + Event(或方法参数)」可以断言「下一 State」是什么,业务逻辑单测简单。

四、Cubit 用法小结

  • 继承 Cubit<State>,构造函数里 super(initialState)
  • 方法里做异步/同步逻辑,需要更新界面时 emit(newState);不能重复 emit 同一实例(State 要不可变、用新对象)。
  • UI 里 BlocProvider 提供 Cubit,BlocBuilder<Cubit, State> 根据 state 建 Widget;需要“只监听、不建 UI”时用 BlocListener(如 state 变失败时弹 Toast)。

五、Bloc 用法小结

  • 继承 Bloc<Event, State>,构造函数里 super(initialState),用 on<XxxEvent>((event, emit) => ...) 注册处理函数。
  • on<Event> 里根据 eventstate 算新 state,调 emit(newState);可 await 异步操作再 emit。
  • UI 里 BlocProvider 提供 Bloc,BlocBuilder 根据 state 建 UI,BlocListener 只听 state 做副作用;需要“先听再建”可用 BlocConsumer
  • BlocProvider 可包在路由或页面根;RepositoryProvider 常和 Bloc 一起用,在 Bloc 里通过 context.read<Repo>() 拿仓库做请求。

六、UI 组件对照

组件作用
BlocProvider把 Bloc/Cubit 提供给子树,子节点用 context.read<Bloc>()
BlocBuilder监听 State,state 变化时重建 builder 返回的 Widget;可设 buildWhen 控制何时重建
BlocListener只监听 State,不建 UI,在 listener 里做副作用(Snackbar、导航、弹窗)
BlocConsumerBlocBuilder + BlocListener 合一,先 listenerbuilder
RepositoryProvider提供 Repository 等依赖,Bloc 里 context.read<Repo>() 取用

16. context.watch 和 context.read 区别?

  • watch:监听变化,当 Provider 通知更新时,当前 Widget 会重建;适合在 build 里拿数据并展示。
  • read:只取一次当前值,不建立监听、不因该数据变化而重建;适合在事件回调里取 store 调方法,避免在 build 里用 read 导致更新不生效。
  • 总结:展示用 watch,事件里用 read。

17. GetX 详细介绍?Obx 和 GetBuilder 区别?

一、GetX 是什么(一句话)

GetX 是一套轻量一体化库:把状态管理、路由、依赖注入(以及 snackbar/dialog、国际化等)放在一起,API 简单、少写样板代码,适合快速开发;代价是对 BuildContext 依赖少、易“全局化”,大项目需要团队规范以防难维护、难测试。

二、核心能力与常用 API

能力常用 API / 概念
状态管理GetxController、.obs 响应式变量、ObxGetBuilderupdate()
路由GetMaterialAppGet.to() / Get.off() / Get.offAll()Get.back()Get.arguments
依赖注入Get.put()Get.lazyPut()Get.find(),不依赖 BuildContext
其他Get.snackbarGet.dialogGet.bottomSheetGet.updateLocale

三、状态管理:从「注册」到「使用」的流程

  1. 写 GetxController:子类里放状态和业务方法;状态可以是普通变量 + GetBuilder + update(),或 .obs 响应式变量 + Obx
  2. 注册:在合适位置 Get.put(MyController())(立即创建)或 Get.lazyPut(() => MyController())(首次 find 时创建),详见第 19 题。
  3. 在页面里用Get.find<MyController>() 取实例,或用 GetView<MyController> 的页面自动带 controller;用 Obx(() => Widget) 包住依赖 .obs 的 UI,或用 GetBuilder 包住依赖 update() 的 UI。
  • GetxController:在其中维护状态和业务;onInit / onReady / onClose 对应初始化和销毁(对应关系见第 18 题)。
  • .obs:把变量变成响应式(如 0.obs → RxInt),改 .value 或扩展方法(如 count++)后,用到它的 Obx 会重建。
  • Obx:只重建其回调里「读到的 .obs」对应的那部分 UI,依赖 GetX 内部依赖收集。
  • GetBuilder:不依赖 .obs,在 Controller 里调 update()update(id) 时,对应 GetBuilder(同 id)才重建;适合不想用 .obs、或要精确控制刷新范围时。

Obx 与 GetBuilder 对比

对比项ObxGetBuilder
依赖.obs 响应式变量GetxController 的 update()
更新方式自动(.obs 变化即重建)手动调用 update()
刷新范围依赖追踪到的变量所在 Obx可指定 id,只更新同 id 的 GetBuilder
适用响应式、写法简洁按需刷新、精确控制、不想用 .obs

四、路由与依赖注入

  • 路由GetMaterialApp + Get.to/off/offAll/back,传参 Get.to(Page(), arguments: data),目标页 Get.arguments。对照 Navigator 见第 21 题
  • 依赖注入Get.put 立即创建,Get.lazyPut 首次 find 时创建,Get.find<T>() 取实例;pop 时未设 permanent 的会回收。细节见第 19 题

五、优缺点与注意点

  • 优点:API 简单、状态/路由/依赖注入一体、不依赖 context 也能路由和取 Controller,适合小项目或原型。
  • 缺点:全局风格重、易滥用,测试和重构成本高;和 Flutter 惯用的「树 + context」不一致,大团队需规范。
  • 注意:Controller 适时 Get.delete 或依赖路由回收,避免泄漏;Obx 里只做「读 .obs + 返回 Widget」,不要写重逻辑或异步。

18. GetxController 生命周期?与 State 的 initState/dispose 对应?

一、生命周期顺序(时间线)

Controller 从被 Get.put 创建到被回收,顺序是:

  1. 创建Get.put(MyController()) 或首次 Get.find(若用 lazyPut)时实例化。
  2. onInit():紧接着调用,只执行一次;适合初始化数据、订阅 Stream、发请求等,此时 Widget 尚未 build
  3. onReady():在 onInit 之后、下一帧回调;此时当前页已 build 完,适合依赖「已经挂上树的 UI」的逻辑(如弹窗、SnackBar、定位到某个 Key)。
  4. 使用期:页面通过 Get.find / GetView 使用,Obx / GetBuilder 随状态更新。
  5. onClose():Controller 被 Get.delete 或随路由 pop 自动回收之前调用;必须在这里取消订阅、关闭 Stream、dispose 子 Controller 等,避免内存泄漏。

二、与 StatefulWidget 的 State 对应

GetxControllerStatefulWidget 的 State说明
onInit()initState()创建后立即调用,只一次
onReady()首帧后(如 addPostFrameCallback)界面已 build 完
onClose()dispose()回收前做清理

三、注意点

  • Get.put 默认在当前路由 pop 且该 Controller 只被该页引用时,会自动 Get.delete,从而触发 onClose
  • Get.put(Controller(), permanent: true) 不会随路由回收,需在合适时机手动 Get.delete,否则易泄漏。

19. Get.put 和 Get.lazyPut 区别?什么时候用哪个?

一、区别概览

对比项Get.putGet.lazyPut
创建时机调用时立即实例化并放入容器只注册工厂首次 Get.find<T>() 时才创建实例
回收默认随当前路由 pop 自动 Get.deletepermanent: true 则不会)同样可在 pop 时回收,取决于绑定关系
典型场景页面一进来就要用的 Controller、希望随路由同生共死不一定用得到、想减轻启动/依赖、避免循环依赖

二、Get.put

  • 调用 Get.put(MyController()) 时,马上执行构造函数并放入 GetX 容器,之后 Get.find<MyController>() 直接取到同一实例。
  • 未设 permanent: true 时,当前路由 pop 且该 Controller 只被该页引用,会自动 delete 并触发 onClose
  • 适用:该页一定用这个 Controller(如详情页的 DetailController),且希望随页面销毁一起回收。

三、Get.lazyPut

  • Get.lazyPut(() => MyController()) 只保存一个「创建函数」,第一次某处 Get.find<MyController>() 时才执行该函数并放入容器,之后 find 拿到同一实例。
  • 若从未有人 find,Controller 不会创建,也不会执行 onInit/onClose
  • 适用:不是每个入口都会用到的 Controller(如设置页才用 SettingsController);或依赖多、希望延迟初始化、避免循环依赖。

小结必用且随页销毁Get.put按需用、减启动、解依赖Get.lazyPut。put 的要注意 permanent 和适时 Get.delete,避免泄漏。


20. GetX 的 .obs 响应式原理?Obx 如何监听变化?

一、.obs 是什么

  • 0.obs''.obsfalse.obs 等会把值包成 GetX 的 Rx 类型(如 RxIntRxStringRxBool),内部是一个「可监听」的包装,value 改变时会通知所有订阅方。
  • 修改方式:必须通过 .value 或 GetX 提供的扩展方法(如 count++name.value = 'x')修改,这样 GetX 才能拦截并触发通知;直接改普通变量不会触发 UI 更新。

二、Obx 如何监听(数据流)

  1. build 时Obx(() => Text('${count.value}')) 执行回调,GetX 在回调执行过程中记录该 Obx 访问了哪些 .obs(如 count),建立「这个 Obx 依赖 count」的关系。
  2. .obs 变化时:当 count.value 被修改,GetX 通知「依赖 count 的 Obx」需要重建。
  3. 下一帧:只重建这些 Obx 包裹的子树,其它 Widget 不受影响。

本质:依赖收集(build 时谁读了谁)+ 订阅 .obs 的 value 变化 + 按需重建,和「谁用到了谁,谁变就更新谁」一致。

三、注意点

  • Obx 回调里只应读 .obs 并返回 Widget,不要写复杂计算或异步;否则依赖收集和重建范围会难以预期。
  • 只读到的 .obs 才会被记为依赖:Obx 内没用到某个 .obs,改那个 .obs 不会触发该 Obx 重建;在 Obx 外改 .obs、Obx 内用到了,会正常触发重建。

21. GetX 路由(Get.to / Get.off)和原生 Navigator 的区别?

一、API 对照

GetX原生 Navigator说明
Get.to(Page())Navigator.push压栈,可返回
Get.off(Page())Navigator.pushReplacement当前页被替换,不可返回当前页
Get.offAll(Page())pushAndRemoveUntil 清栈再 push清空栈再进新页
Get.back()Navigator.pop出栈
Get.to(Page(), arguments: x) / Get.argumentsModalRoute.settings.arguments传参 / 目标页取参

二、典型用法

  • 根用 GetMaterialApp(内部仍是 Navigator),用 Get.to(NextPage()) 跳转,Get.back(result) 返回;传参用 Get.to(Page(), arguments: data),在目标页 Get.arguments 取。
  • Get.off 替换当前页(如登录成功后进首页且不能回到登录页);Get.offAll 清空栈再进新页(如登出后回到登录)。

三、和原生 Navigator 的主要区别

  • 不依赖 BuildContextGet.to / Get.off / Get.back 可在 Controller、Service、任意地方调用,不需要 context;原生要用 Navigator.of(context).push/pop,必须能拿到 context。
  • 根组件:GetX 用 GetMaterialApp,底层仍是 Navigator,Get 封装了路由 API 和 Get.arguments 等。
  • 与 Controller 回收Get.put 的 Controller 未设 permanent: true 时,常在该路由 pop 时随 Get 的绑定被 Get.delete 并触发 onClose;原生 Navigator 无这层绑定,需在 State.dispose 里自己释放。

小结:需要在无 context 处做路由(如全局逻辑、弹窗回调)时,Get 路由较方便;若项目统一用 GoRouter / Navigator 2.0,可以只用 GetX 的状态 + 依赖注入,不用 Get 路由。


22. Riverpod 的 Provider 类型有哪些?ref.watch 和 ref.read?

一、Provider 类型概览

Riverpod 用不同的 Provider 类型表示「数据形态」和「是否可变」,在 Widget 或 Notifier 里通过 ref.watch / ref.read 使用。

类型含义典型用法何时用
Provider不可变值,创建后一般不改配置、单例服务、常量只读、全局一份的数据
StateProvider简单可变状态(如 int、bool)ref.watch(stateProvider) 读,ref.read(stateProvider.notifier).state++计数器、开关等简单状态
FutureProvider异步单次(Future)ref.watch(futureProvider) 得 AsyncValue,处理 loading/data/error单次请求、初始化加载
StreamProvider流(Stream)ref.watch(streamProvider) 得 AsyncValue实时推送、WebSocket、订阅
StateNotifierProvider复杂状态 + 业务逻辑(StateNotifier)ref.watch(provider) 取 state,ref.read(provider.notifier) 调方法改状态多字段、多方法的业务状态
NotifierProvider(2.x)同上,Riverpod 2.x 推荐写法Notifier 子类 + ref,替代 StateNotifierProvider新项目推荐用 NotifierProvider
  • Providerfinal configProvider = Provider((ref) => AppConfig());,只读。
  • StateProviderfinal countProvider = StateProvider<int>((ref) => 0);,通过 .notifier 取 Notifier 再改 .state
  • FutureProviderfinal userProvider = FutureProvider<User>((ref) => api.getUser());,watch 得到 AsyncValue<User>(data/loading/error)。
  • StreamProviderfinal messagesProvider = StreamProvider<List<Message>>((ref) => stream);,同样得到 AsyncValue
  • StateNotifierProvider:状态由 StateNotifier 管理,ref.read(xxx.notifier) 调方法,内部 state = newState 触发依赖该 Provider 的 Widget 重建。

二、ref.watch 和 ref.read 的区别

对比项ref.watch(provider)ref.read(provider)
使用位置主要在 build 里(或依赖 ref 的 Notifier/Provider 体内)事件回调里(如 onPressed、initState 里不推荐,可用 ref.read)
是否建立监听会;当前 Widget/Provider 依赖该 provider,其值一变就重建/重新计算不会;只读当前值一次,不订阅变化
典型场景用数据驱动 UI(显示 state、根据 AsyncValue 显示 loading/data/error)用户操作时取一次值或调 notifier 方法(如提交表单、翻页加载)
  • ref.watch:在 build 里用,表示「依赖这个 provider,它的值变了我就重建」。例如 ref.watch(counterProvider) 显示计数;不要在事件回调里用 watch,否则容易重复监听或不符合「只读一次」的语义。
  • ref.read:在 onPressed、onTap 等回调里用,只取当前值或 ref.read(provider.notifier) 调方法,不建立监听,不会因为该 provider 变化而重建当前 Widget。

四、ref.listen(顺带)

  • ref.listen(provider, (prev, next) { ... }):监听 provider 变化并执行副作用(如弹 SnackBar、导航),不会让当前 Widget 为显示该值而重建;适合「值变了我要做一件事,但 UI 不直接依赖这个值」的场景。

小结:Provider 类型按「不可变 / 简单可变 / 异步 / 流 / 复杂状态」选;ref.watch 在 build 里用、建立依赖且会重建;ref.read 在事件里用、只读或调 notifier,不监听。


23. 如何做 Flutter 的依赖注入?

  • 目的:不在类内部 new 依赖,由外部/容器注入,解耦、单测可替换、统一管理生命周期。Flutter 里常需 Repository/ApiClient/ViewModel 等全局或子树共享。
  • 常见做法
方式如何「注入」如何「取」特点 / 适用
Provider / Riverpod在树顶或某层用 Provider / ProviderScope 包一层,create 里建实例子节点 context.read<T>() / ref.read(provider)依赖挂在树上,和 Widget 树绑定;Riverpod 不依赖 context,可测试性好
get_itmaininitGetIt.I.registerSingleton / registerFactory任意处 GetIt.I<T>()全局单例/工厂,不依赖 BuildContext,适合纯逻辑层、测试时 registerSingleton 换成 Mock
BlocRepositoryProviderBlocProvider 在树顶或某层提供 Repository / Bloc子节点 context.read<Repository>()BlocProvider.of和 Bloc 架构配套,Repository 给 Bloc 用
GetXGet.put(Controller()) / Get.lazyPut(() => Controller())Get.find<Controller>()自带容器,不依赖 context;详见第 54、56 题
  • 选型:树上绑定用 Provider/Riverpod/Bloc;全局与树无关用 get_it;一体化用 GetX。常见:UI 层 Provider/Riverpod/GetX,逻辑层 get_it 取 Repository;单测时 get_it 换 Mock。

24. ValueListenableBuilder、ListenableBuilder 和 setState、Obx 的区别?适用场景?

一、各是什么、重建范围

方式监听/依赖重建范围
setState无,手动 setState()整个 State 的 build,整页重建
ObxGetX 的 .obs 变量该 Obx 包裹的子树,依赖收集到的 .obs 变才重建
ValueListenableBuilderValueListenable<T>(如 ValueNotifier<T>仅 builder 返回的子树,value 变才重建
ListenableBuilderListenable(如 AnimationControllerChangeNotifier仅 builder 返回的子树,listen 触发时重建

二、适用场景

  • setState:简单页、状态少,整页重建成本可接受。
  • Obx:项目用 GetX,希望自动依赖、局部重建
  • ValueListenableBuilder单值或简单模型(如 ValueNotifier<int>),要局部刷新、不用 GetX 时用。
  • ListenableBuilder多个字段Listenable(如 ChangeNotifierAnimation),要局部刷新;动画场景常用 AnimatedBuilder(本质也是听 Listenable)。

三、小结

  • setState 整页重建;Obx 依赖 .obs 局部重建;ValueListenableBuilder / ListenableBuilder 依赖 ValueNotifier / Listenable 只重建 builder 子树。要局部刷新且不用 GetX 时,用 ValueListenableBuilderListenableBuilder

25. Flutter 路由有哪几种方式?Navigator 1.0 和 2.0 区别?

对比项Navigator 1.0Navigator 2.0
方式命令式(push/pop)声明式(路由由应用状态驱动)
APINavigator.push / popRouter + RouteInformationParser + RouterDelegate
栈管理直接操作栈通过配置/状态生成路由栈
适用简单页面栈Web 深链接、多 tab、复杂栈、URL 同步
封装常用 GoRouter(配置化、redirect、深链接)

26. 如何做页面间传参?如何接收返回结果?

传参

  • 构造函数传参:push 时 new Page(args)
  • ModalRoute.of(context).settings.arguments 在目标页取。
  • 命名路由在 routes 里配置,通过 arguments 传参,目标页用 ModalRoute.settings.arguments 取。

返回结果
await Navigator.push(...) 得到目标页 Navigator.pop(context, result) 的 result;或通过状态管理/EventBus 传回。


27. Hero 动画是什么?使用注意?

一、Hero 是什么

  • Hero 是 Flutter 里的共享元素转场:在两个页面上各放一个 Herotag 相同,路由切换时 Flutter 会把「源页的 Hero 子节点」在视觉上飞过去接到「目标页的 Hero 子节点」位置,中间自动做位置、尺寸的过渡动画,常用于列表头像点进详情大图、卡片展开等。

二、怎么用

  • 源页(如列表):用 Hero(tag: 'xxx', child: Widget) 包住要「飞」的那块(如小头像)。
  • 目标页(如详情):用同一个 tag 再包一层 Hero(tag: 'xxx', child: Widget),这里是「落地」后的样子(如大图)。
  • 路由用 Navigator.push 正常跳转即可,Flutter 会根据 tag 匹配两个 Hero,自动播过渡动画;返回时动画反向。

三、使用注意

注意点说明
tag 唯一同一路由栈内,同一时刻不能有两个相同 tag 的 Hero 同时存在;列表里每个 item 的 Hero 要用不同 tag(如带 id:'avatar-${item.id}'),否则匹配错乱或报错。
两边 child 尽量一致两边的 child 类型、宽高比不要差太多,否则过渡时会有明显「形变」或跳动;例如一边是圆形头像、一边是大方图,可以接受,但一边是文字一边是图就容易怪。
包一层 Material若 Hero 的 child 是图片、透明背景等,在部分平台可能被裁剪或出现黑边,用 MaterialMaterial(type: MaterialType.transparency) 包一层 child 可减轻问题。
flightShuttleBuilder想自定义「飞行过程中」显示的 Widget,可设 Hero.flightShuttleBuilder,默认是源 child 在飞。

四、小结:相同 tag 在两页各包一块 UI 即共享元素转场;列表每项用不同 tag(如 'avatar-${id}'),两边 child 尽量一致,必要时 Material 包一层防裁剪。


28. 如何做路由鉴权 / 登录拦截?(如 GoRouter redirect)

一、思路简述

  • 鉴权/登录拦截:在用户访问需要登录的页面时,若未登录则先跳到登录页,登录成功后再跳回原目标页(或首页)。

  • 常见做法:GoRouterredirect 里根据当前路径 + 登录态决定重定向到哪;Navigator 1.0 可在 push 前或 onGenerateRoute 里判断。

  • GoRouterredirect同步读登录态(Provider/Riverpod、get_it),未登录访问鉴权路径时 return '/login?from=...',登录成功 context.go(from ?? '/');登录态不能用 async,用 refreshListenable 触发重新 redirect。

  • Navigator 1.0:push 前判断登录态,未登录则 push 登录页,登录页回调里 pop 再 push 目标页;或在 onGenerateRoute 里按 settings.name + 登录态返回 MaterialPage。


29. PopScope 和 WillPopScope 区别?如何拦截返回键?

  • 拦截返回:用户点返回键/左滑/AppBar 返回时默认 Navigator.pop;拦截 = pop 前先判断(如表单未保存弹确认、Web 再按一次退出、混合栈交给原生),确认后再 Navigator.pop
  • PopScope(推荐):canPop: false 不自动 pop,onPopInvokedWithResult 里做逻辑(弹窗、通知原生等),需 pop 时自己调 Navigator.popWillPopScope(onWillPop 返回 true/false)已废弃。
  • 对比
对比项WillPopScopePopScope
状态已废弃(Flutter 3.12+)推荐替代
拦截方式onWillPop 返回 true/false,false 即不 popcanPop: false 表示不自动 pop;onPopInvokedWithResult 在用户点返回时被调用
“拒绝”返回回调里 return false 即可,当前页不 popcanPop: false,在 onPopInvokedWithResult 里弹 Dialog 等,用户确认后再 Navigator.pop(context)
拿到“用户点了返回”onWillPop 被调时就是用户点了返回onPopInvokedWithResult 被调时就是用户点了返回,可在这里做二次确认
  • 场景:表单未保存 → canPop: false + 弹确认;Web 壳子 → 再按一次退出(回调里记时间);混合栈 → 回调里通知原生由原生决定。

30. Flutter 与原生混合开发时,页面路由如何管理?(大厂高频)

一、目标:混合 = 原生页 + Flutter 页互相跳转。要栈统一(返回关谁)、返回键/手势一致、传参清晰。

二、常见做法原生维护主栈,Flutter 作为一屏或多屏嵌入;FlutterBoostadd-to-app 等:原生打开/关闭 Flutter 容器时带 arguments,Flutter 通过 Channel 或方案 API 通知原生 finish/pop、回传结果。

三、要点小结(表格)

要点说明
栈统一原生维护主栈,Flutter 作为一屏或多屏嵌入;返回键由原生或方案统一处理,并通知 Flutter 是否 pop
传参 原生→Flutter启动 Flutter 容器时带 arguments(如 Intent.extra、Route 的 initialRoute/arguments)
传参 Flutter→原生Platform Channel 或方案提供的 API(如 FlutterBoost 的 close 带 result)
单引擎/多引擎一般单引擎复用,多 Flutter 页共用一个引擎,减少内存;多引擎适合隔离业务
生命周期Flutter 页的可见性、前后台要与原生一致,便于暂停动画、释放资源

31. Dart 是单线程的,如何做“多线程”?Isolate 和 Future 区别?

一、Isolate 是什么

  • Isolate 是 Dart 里的独立“线程”:每个 Isolate 有自己的内存堆独立的事件循环不共享内存,因此没有传统多线程的锁、竞态问题。
  • 主 Isolate:跑 UI、build/layout/paint、你写的绝大部分 Dart 代码;子 Isolate 通过 Isolate.spawn 或 Flutter 的 compute() 创建,在后台跑耗时计算。
  • 通信方式:Isolate 之间只能通过 SendPort / ReceivePort 传消息(序列化数据),不能直接访问对方变量;传参和返回值必须可序列化(基本类型、List/Map、简单对象等)。

二、API 要点

  • ReceivePort:当前 Isolate 收消息,sendPort 给对方用于回传。
  • Isolate.spawn(entryPoint, message):entryPoint 为顶层或静态函数,message 可带 SendPort;子 Isolate 只能通过 port 收发数据,不能访问主 Isolate 变量或 Flutter 插件/UI。
  • Isolate.exit(result):子 Isolate 退出时可带返回值,主 Isolate 在 ReceivePort.listen 里收到。

三、Isolate 与 Future 对比

对比项Future / async-awaitIsolate
执行位置同一 isolate(主线程)独立 isolate,独立内存与事件循环
数据共享共享内存,直接访问变量不共享内存,通过 SendPort/ReceivePort 传消息
适用IO、轻量异步、不阻塞 UICPU 密集(加解密、大 JSON、图像处理)
典型用法async/await、Future.thenIsolate.spawn、compute()
  • 计算密集应放到 Isolatecompute(),避免卡 UI;IO、网络等用 async/await 即可。更细的 Isolate 用法与 compute 区别见第 34 题。

32. async/await 和 Future 的关系?FutureBuilder 使用注意?

  • async/await 是 Future 的语法糖:async 函数返回 Future,await 等待 Future 完成并取结果,不阻塞 UI 线程。
  • FutureBuilder:根据 Future 的 connectionState 和 data/error 构建不同 UI。注意:同一 Future 不要重复传给 FutureBuilder 导致多次触发;Future 创建时就会执行,别在 build 里每次 new 会重复请求的 Future。

33. Stream 和 Future 区别?StreamBuilder 使用场景?

对比项FutureStream
数据次数一次,一个值(或异常)多次,连续数据流
典型场景单次请求、一次性异步WebSocket、蓝牙、搜索建议、传感器
UI 组件FutureBuilderStreamBuilder
取消无内置取消(可封装)listen 返回 Subscription,需 cancel
  • StreamBuilder:根据 Stream 的 connectionState 和 data/snapshot 构建 UI;适合实时推送、聊天消息。注意:订阅要在 dispose 里 cancel,否则易内存泄漏。Stream 单订阅与广播区别见第 36 题

34. compute() 和 Isolate 的关系?什么时候用 compute?

一、Isolate 简要回顾

  • Isolate 概念、API(spawn、SendPort/ReceivePort)、与 Future 对比见第 31 题。此处仅需知:每个 Isolate 独立堆与事件循环,之间靠消息传递,传参与返回值需可序列化。

二、compute() 和 Isolate 的关系(对比要清晰)

  • compute() 是 Flutter 提供的封装:内部就是用 Isolate.spawn 起一个新 Isolate,把传入的顶层/静态函数一个参数发过去执行,结果通过 SendPort 回传,compute 转成 Future<T> 返回;执行完后该 Isolate 自动结束并回收
  • 关系一句话:compute = 对「单次、一进一出」的 Isolate 用法的封装,你不用自己写 ReceivePort、spawn、listen、Isolate.kill,只写一个静态/顶层函数compute(fn, arg) 即可。

compute() 与 手写 Isolate 对比

对比项compute()手写 Isolate.spawn
谁起 Isolatecompute 内部起一个你自己 Isolate.spawn
通信方式一个入参、一个返回值,函数执行完一次就结束自己 ReceivePort + SendPort,可多次 send/listen,长期驻留
生命周期函数执行完 Isolate 自动回收需自己 Isolate.kill 或让 isolate 自己 exit
API 形式compute(fn, message)Future<result>,async/await 即可要写 port.listen、spawn、传 sendPort 等
适用单次耗时计算(解析、编解码、复杂运算)多次收发、长驻后台、复杂流水线

三、什么时候用 compute、什么时候用裸 Isolate

场景用谁说明
单次 CPU 密集(大 JSON 解析、加解密、图片编解码、复杂计算)compute(fn, data)一进一出,写起来简单,Isolate 自动回收
需要长期驻留、多次收发(如后台持续处理、复杂流水线、长连接)Isolate.spawn + 自己维护 ReceivePortcompute 只跑一次就结束,无法「常驻 + 多次通信」
只做 IO、轻量异步(网络请求、读文件)async/await 在主 IsolateIO 不会占满 CPU,不需要新 Isolate

四、使用注意

  • compute 的 fn 必须是顶层或静态函数(或静态方法),不能传匿名函数或带闭包,否则无法在子 Isolate 里反序列化执行。
  • 子 Isolate 里不能Flutter 插件、dart:ui、BuildContext 等依赖主 isolate 的 API,只能做纯 Dart 计算或 dart:io(如 File、HttpClient)。
  • 传参和返回值不宜过大,否则序列化/反序列化也耗时;大二进制可考虑 TransferableTypedData

五、小结compute = 单次 Isolate 封装,一进一出、自动回收;单次 CPU 密集用 compute,多次通信/长驻用手写 Isolate,纯 IO 用 async/await。对比见上表。


35. Dart 事件循环里微任务和事件队列与 Flutter 帧的关系?

一、两个队列

  • 微任务队列:scheduleMicrotask、Future.then 等,先被清空
  • 事件队列:Timer、I/O、手势、渲染帧任务等,每轮只取一个执行。

二、每轮顺序:① 清空微任务 → ② 取一个事件执行 → 下一轮。若微任务多或当前事件耗时长,后面的任务(含渲染帧)会迟迟轮不到。

三、Flutter 的「帧」在事件队列里

  • 引擎在每帧 VSync 前(或按帧率)向事件队列里投递一个 「渲染帧」任务
  • 只有当事件循环取到这个渲染帧任务并执行时,才会跑 build → layout → paint → 提交 GPU
  • 因此:渲染帧和 Timer、I/O、手势一样,都是事件队列里的一个任务;它不会「插队」,必须等前面的微任务和前面的事件都处理完(或让出)才会轮到。

四、和 jank 的关系

情况结果
微任务太多 / 执行太久本帧要执行的「渲染帧」任务还在事件队列里排队,等微任务清完才能被取到 → 渲染延迟 → 本帧超时 → jank
前面的事件(如某个 Timer、手势回调)执行太久同上,渲染帧任务迟迟轮不到 → jank
在 build 或某回调里同步写大量逻辑相当于把「当前事件」拉长,当前事件可能就是渲染帧本身或前面的任务,会拖慢本帧或下一帧。

结论:要把耗时逻辑放到 Isolate 或至少延后到下一帧(如 scheduleFrameCallback),保证「渲染帧任务」被取到时能尽快执行完 build/layout/paint。

五、流程图

sequenceDiagram
    participant Micro as 微任务队列
    participant Event as 事件队列
    participant Run as 执行

    Run->>Micro: ① 清空微任务 (scheduleMicrotask / Future.then)
    Micro->>Event: ② 取一个任务
    Event->>Run: 渲染帧 / Timer / I/O / 手势
    Note over Run: 若是渲染帧 → build→layout→paint→提交 GPU
    Note over Run: 否则 → 执行对应回调,再下一轮循环

小结:单线程下先清空微任务,再处理一个事件渲染帧是事件队列里的一种任务,被取到才执行 build/layout/paint。微任务或前面事件耗时长 → 渲染帧迟迟轮不到 → jank


36. Stream 单订阅(single)和广播(broadcast)区别?使用场景?

  • 单订阅(single):一个 Stream 只能被 listen 一次,多次 listen 会报错;适合“单消费者、按序消费”的场景,如单次网络响应流、单次文件读取流。StreamController() 默认创建单订阅 Stream。
  • 广播(broadcast):可被多个 listener 同时监听;适合“多消费者、事件广播”的场景,如 WebSocket 消息、全局事件总线、多个 UI 同时监听同一数据流。用 StreamController.broadcast()stream.asBroadcastStream()
  • 区别:单订阅可支持 pause/resume、背压;广播不保证每个 listener 都收到“历史”数据,且不支持 pause。若需要多 listener 且要共享同一数据源,用 broadcast;若只有一个消费者且要严格按序,用 single。Stream 与 Future 区别、StreamBuilder 使用见第 33 题。面试可答:单订阅 = 单消费者、可暂停;广播 = 多消费者、不保证历史、不能 pause

37. 在 Flutter 里发网络请求后更新 UI,要注意什么?

  • async/awaitFuture.then 在请求完成后 setState 或通知状态管理更新。
  • 一定要在 setState 前判断 mounted:异步回来时 Widget 可能已 dispose,否则会报错。
  • 处理 加载中、错误、空数据 等状态,避免未处理异常导致白屏或崩溃。
  • 可配合 FutureBuilder 或状态管理统一管理请求状态(loading/success/error)。

38. 说说 Flutter 的 build 时机和如何减少不必要的 build

  • build 在:initState 之后、didUpdateWidget、父 build、InheritedWidget 依赖更新、setState 等时机被调用。
  • 减少 build
    • const 构造函数:相同配置复用同一实例,减少重建。
    • 把状态下沉:只把会变的 state 放在子树顶层,用 StatefulWidget 包最小范围
    • RepaintBoundary:隔离重绘区域,避免整棵子树重绘。
    • 状态管理里用 select/watch 精细依赖,只监听用到的字段(如 Riverpod 的 select)。

39. ListView 大量数据怎么优化?ListView.builder 和 ListView 区别?

  • ListView:一次性构建所有子 Widget,只适合少量数据。
  • ListView.builder懒加载,只构建可见(加缓存)的 item,适合长列表。
  • 进一步优化:itemExtent 固定高度减少布局计算;cacheExtent 控制预加载范围;避免在 item 里做重操作;图片用 cached_network_image 等缓存。

40. 什么是 Flutter 的 jank?如何排查和优化?

  • Jank:掉帧、卡顿,通常是单帧耗时超过约 16ms(60fps),用户感觉卡。成因(UI 线程慢 vs GPU 线程慢)见第 45 题
  • 排查Performance Overlay(右上角显示 GPU/UI 帧耗时)、DevTools Timeline 看哪一帧超时、profile 模式 跑真机。
  • 优化:减少 build 范围、避免在 build 里做重计算、大列表用 builder、耗时计算放 Isolate、图片压缩与缓存、减少 saveLayer 等昂贵操作。

41. 为什么 build 里不能做耗时操作?应该放哪?

为什么不能?
build 在每帧或每次重建时都会调用。在 build 里做重计算、同步 IO、大 JSON 解析等会阻塞 UI 线程,导致掉帧(jank)、卡顿。

应该放哪?

场景放哪里
一次性初始化initState
异步请求Future / async,结果回来后再 setState
CPU 密集compute()Isolate,算完再通知 UI
定时 / 轮询TimerStream,在回调里更新状态

原则:build 只做轻量的「根据状态返回 Widget 树」,不做事务逻辑、不阻塞。


42. saveLayer 为什么贵?如何避免或减少?

为什么贵?
saveLayer 会为子树分配离屏缓冲,先画到缓冲再合成,涉及多遍绘制、GPU 内存与带宽,是 Flutter 里较贵的操作之一。

常见触发:Opacity(非 0/1)、ColorFilter、ShaderMask、BackdropFilter(模糊)、部分 Transform 等。

如何避免或减少?

  • Opacity:能不用就不用,子节点直接设透明色。
  • BackdropFilter:用 ClipRect 包小区域,限制范围。
  • RepaintBoundary:包住会触发 saveLayer 的子树,避免影响整屏。
  • 设计:减少全局半透明、模糊层数。

43. vsync 是什么?为什么 Ticker 需要它?刷新流程是怎样的?

  • vsync:显示器每帧扫描完成后发出的同步信号(如 60Hz ≈ 16.67ms 一次),表示可以准备下一帧。Flutter 按此节拍做 build/layout/paint 并提交,与屏幕刷新对齐。
  • Ticker 为什么需要 vsync:Ticker 在每帧回调里 tick 一次,动画与渲染同频;页面不可见时不再注册回调,动画自动暂停、省电。用 Timer 需自己管生命周期且易与帧不同步。

刷新流程(一帧):VSync 触发 → UI 线程(Ticker.tick、build → layout → paint)→ 产出 Layer Tree → 提交给 GPU 线程 → 合成并写帧缓冲 → 下次 vsync 上屏。GPU 线程的 Compositor 可通过 Throttle 回压 UI 线程,避免掉帧。

1daa0472d1524886b4704d6735d2c45a~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.png


44. Flutter 渲染管线大致几步?Layout、Paint、Layer、Compositing 是什么?

一、渲染管线大致几步(顺序)

一帧内,从「需要刷新」到「上屏」可以简化为:

  1. Build:根据 Widget 树 更新 Element 树,挂载/更新 RenderObject(对应「要画什么」)。
  2. LayoutRenderObject 做布局,算出每个节点的尺寸和位置
  3. PaintRenderObject 做绘制,产出 Layer,多个 Layer 组成 Layer 树
  4. Compositing:把 Layer 树 转成 GPU 能执行的纹理与绘制指令,确定合成顺序。
  5. 提交 GPU → 上屏:GPU 执行绘制、写帧缓冲,等 VSync 后显示到屏幕。

二、Layout、Paint、Layer、Compositing 各是什么

概念是什么谁在做产出/作用
Layout(布局)根据约束算出每个 RenderObject大小和位置RenderObject.performLayout()自顶向下传约束,自底向上回传尺寸,得到布局好的 RenderObject 树
Paint(绘制)把当前 RenderObject 画出来RenderObject.paint(Canvas)Canvas 画,产出 Layer(如 PictureLayer、TransformLayer、OpacityLayer)
Layer 树描述「谁画什么、顺序、变换」的一棵树RenderObject.paint 时挂到 Layer 上供合成与裁剪用;Compositing 的输入
Compositing(合成)Layer 树 变成 GPU 可执行的指令引擎(Skia/Impeller)在 GPU 线程确定 z-order、透明度混合等,生成纹理与绘制命令,写入帧缓冲

三、管线流程图

sequenceDiagram
    participant Build as Build (Widget→Element)
    participant Layout as Layout (performLayout)
    participant Paint as Paint (Layer 树)
    participant Comp as Compositing (GPU 指令)
    participant VSync as VSync 上屏

    Build->>Layout: 布局
    Layout->>Paint: 绘制
    Paint->>Comp: 合成
    Comp->>VSync: 提交

四、小结:Build → Layout → Paint → Layer 树 → Compositing → 上屏。各步含义见上表。从 VSync 触发到上屏的流程图见第 43 题UI/GPU 线程与 jank第 45 题


45. UI 线程和 GPU 线程如何协作?为什么会导致 jank?

一、两线程分工(管线步骤见第 44 题

线程主要工作
UI 线程(Dart)build → layout → paint,生成 Layer 树并提交给 GPU
GPU 线程(C++)接收 Layer 树,转 GPU 指令、合成、写帧缓冲VSync 后上屏

二、一帧内协作

VSync 触发本帧 → UI 线程完成 build/layout/paint 并提交 Layer 树 → GPU 线程做 Compositing、写帧缓冲 → 下一 VSync 上屏。要在 60fps 下不丢帧,UI 和 GPU 的活都要在约 16.67ms 内完成。

三、为什么会导致 jank(掉帧)

  • jank = 某一帧没能在预期时间(如下一个 VSync)前准备好并上屏,用户会感到卡顿、掉帧。
原因说明
UI 线程太慢build / layout / paint 任一步耗时过长(如 build 里逻辑重、layout 过于复杂、paint 里大量绘制),或主 isolate 在做耗时计算(大 JSON、复杂运算),导致本帧内迟迟交不出 Layer 树,GPU 等不到数据或收到时已错过本帧上屏时机 → jank
GPU 线程太慢Layer 树过大、过复杂(如 saveLayer 多、半透明层多、过度绘制),GPU 转指令和合成的时间超过一帧预算 → 即使 UI 按时提交,GPU 也来不及在本帧内画完jank

四、协作流程图

sequenceDiagram
    participant VSync
    participant UI as UI 线程 (Dart)
    participant GPU as GPU 线程 (C++)

    VSync->>UI: 触发新一帧
    UI->>UI: build → layout → paint
    UI->>GPU: 提交 Layer 树
    GPU->>GPU: Skia/Impeller 转 GPU 指令
    GPU->>GPU: 合成、写帧缓冲
    Note over UI,GPU: 若任一步 >16.67ms → jank
    VSync->>VSync: 下一帧显示
  • 优化:UI 线程用 const、缩小 build 范围、耗时放 Isolate;GPU 用 RepaintBoundary、少 saveLayer、减半透明。
  • 小结:UI 线程 build→layout→paint 并提交 Layer 树,GPU 线程合成、上屏;任一线程超时即 jank。UI 慢多为 build/主线程耗时,GPU 慢多为 saveLayer/过度绘制。

46. 为什么说 Layer 树?和 RenderObject 树的关系?

  • Layer 树:合成与上屏按「」组织(位图/变换/透明度),Paint 产出 → Compositing 消费,便于光栅化、缓存(RepaintBoundary)、saveLayer 等。
  • 与 RenderObject 树:RenderObject 树负责 layout + paint(谁在算、谁在画);paint 时产出 Layer 组成 Layer 树(画的结果如何分层交给 GPU)。一个 RenderObject 可对应 0/1/多个 Layer(RepaintBoundary 成层、saveLayer 多一层)。

47. 什么是 RepaintBoundary?什么时候用?

  • 是什么:在 Widget 树上画一条边界,边界内子树变成单独一层(Layer);边界内变只重画边界内,边界外变可复用边界内缓存,从而缩小重绘范围。用多了多占层和内存,不能滥用。
  • 什么时候用
场景用法
列表里每个 item 很复杂给每个 item 包一层 RepaintBoundary,滚动时只重画可见/变化的那几个 item,其它 item 不跟着整列表重画。
某一块有小动画(如一个图标在转)只给这一块包 RepaintBoundary,动画每帧只重画这一小块,不会带动整页重绘。
DevTools 里发现某块重绘特别频繁用性能视图找到对应 Widget,在它外面包一层 RepaintBoundary,把重绘范围锁在这一块。
  • 别乱用:不要给每个小 Widget 都包;只在列表 item 很重、局部动画、或 DevTools 确认某块重绘过重时加。

48. 如何用 DevTools 做性能分析?Timeline、Memory 怎么用?

Timeline

  • 看每帧耗时,build / layout / paint 各占多少。
  • 找出哪一帧超过 16ms、是 build 慢还是 paint 慢。
  • 配合 Profile 模式flutter run --profile)和真机。

Memory

  • 堆快照(heap snapshot),看对象数量、Retained 大小。
  • 对比操作前后快照,找未释放的 Controller、Subscription、大对象。
  • 看是否有持续增长的曲线(泄漏)。

其他

  • CPU 火焰图:看哪段代码占 CPU 高。
  • Widget inspector:看树结构和 rebuild 范围。

常用流程:先 Timeline 定位是 build 还是 paint 卡 → 再针对性优化,或看 Memory 查泄漏。


49. 启动速度和包体积有哪些常见优化手段?

启动优化

  • 减少 main() 到首帧的同步工作(少在 main 里重计算、少同步 IO)。
  • 延迟初始化非首屏必需的库或服务。
  • 首屏用简单 Widget,复杂内容异步加载。
  • Release 模式用 AOT,比 Debug 快很多。
  • Native 侧:Splash/占位、引擎预加载。

包体积优化

  • 使用 --split-debug-info--obfuscate
  • 去掉未用资源(图片、字体),按需引入第三方库。
  • Android:打 App Bundle(aab),让商店按架构分包。
  • 分析flutter build apk --analyze-size 或 DevTools 的 size 工具看大块占用。

小结:启动看「首帧时间」和 main 到首绘路径;包体积看资源与 native 库、是否混淆与 split。从 main() 到首帧的详细流程见第 50 题


50. Flutter 应用从 main() 到首帧显示的启动流程?(大厂/原理)

一、流程分阶段

阶段做什么
1. main()执行 runApp(MyApp()) 前会绑定 WidgetsBindingSchedulerBinding 等;runApp 把根 Widget 挂到引擎,触发首次构建。
2. 构建 Element / RenderObject 树从根向下 mount(创建 Element、RenderObject),再 layout(算尺寸位置),再 paint(产出 Layer 树)。
3. 合成与上屏Engine 把 Layer 树 compositing 成 GPU 指令,Embedder 与平台窗口、VSync 对接,把首帧画到屏幕。
4. 之后进入 事件循环:输入、动画、Timer、微任务等驱动后续帧与交互。

二、一句话串起来

  • main()runApp 挂根 Widget → mount / layout / paint 得到 Layer 树 → Compositing 转 GPU 指令 → Embedder + VSync 首帧上屏 → 进入事件循环。

三、可优化点

  • 减少 main() 到首帧的同步工作(少在 main 里重计算、少同步 IO)、延迟初始化非首屏库、首屏用简单 Widget;详见第 49 题启动优化。

51. AnimationController 和 Tween 分别做什么?

  • AnimationController:控制动画的时间线(start/stop/repeat/reverse),产出 0.0~1.0 的线性进度,需要 vsync(TickerProvider)防止不可见时继续跑。
  • Tween:定义值域映射,如 Tween&lt;double&gt;(begin: 0, end: 1)ColorTween,通过 animate(animation) 得到 Animation<T>,把 0~1 映射到具体类型。
  • 组合:Controller 驱动 → Animation 0~1 → Tween.animate 得到 Animation<T> → 在 addListener 或 AnimatedBuilder 里用 value 更新 UI。

52. 隐式动画和显式动画区别?举例

对比项隐式动画显式动画
用法改目标值,框架自动插值自己持有一个 AnimationController + Tween
典型组件AnimatedContainer、AnimatedOpacity、AnimatedPositionedAnimationController + Tween + addListener / AnimatedBuilder
控制力简单,适合单属性过渡强,可做组合、循环、复杂曲线
举例按钮点击后容器变宽 → AnimatedContainer页面转场、复杂曲线 → Controller + Tween

53. AnimatedBuilder 和 setState 做动画的区别?

  • setState:在 Animation.addListener 里调 setState,整棵 State 的 build 都会重建,范围大,易造成多余 rebuild。
  • AnimatedBuilder:只重建 builder 返回的那棵子树,重建范围小,性能更好;且不需要自己 addListener + setState,代码更清晰。
  • 做动画时优先用 AnimatedBuilder(或 ListenableBuilder)把重建限制在最小子树。

54. 如何和原生(Android/iOS)通信?Platform Channel 是什么?三种 Channel 分别适用什么场景?

Platform Channel 是什么
Flutter(Dart)与原生(Java/Kotlin、Swift/ObjC)通过信道交换消息。Flutter 侧 MethodChannel(name).invokeMethod('xxx', args);原生侧在对应 Channel 的 handler 里处理并 reply。

三种 Channel 对比(字节/阿里常考):

Channel适用场景说明
MethodChannel方法调用,Dart 调原生并异步拿一次结果最常用;调原生 API、获取设备信息、一次请求一次响应
EventChannel原生持续向 Dart 推数据(传感器、蓝牙、广播)Dart 侧 listen;适合“原生主动推送、多次数据”
BasicMessageChannel二进制/字符串消息,双向收发,无固定“方法名+参数”自定义协议、简单键值或二进制流;使用频率低于 MethodChannel
  • 选型:调方法拿结果用 MethodChannel原生持续推送用 EventChannel自定义消息格式用 BasicMessageChannel开发 Flutter 插件及 FFI 与 Platform Channel 选型见第 56 题

55. 混合栈(Flutter 与原生页面互相跳转)要注意什么?

  • 需统一管理原生容器(Activity/ViewController)与 Flutter Engine,避免多引擎或页面栈错乱。
  • 常见方案:FlutterBoost官方 add-to-app,由原生主导路由,Flutter 作为单页或多页嵌入。
  • 注意:引擎复用、生命周期与内存、返回键/手势与原生一致、传参方式统一。混合栈下的页面路由如何管理见第 30 题。

56. 如何开发一个 Flutter 插件(Plugin)?FFI 和 Platform Channel 怎么选?

一、插件是什么:Dart 不能直接调 Java/Swift 或系统 API;插件 = 加原生代码(android/、ios/),通过 Platform ChannelFFI 让 Dart 与原生互通。

二、如何开发一个插件(用 Platform Channel 时)

  1. 创建插件工程flutter create --template=plugin --org com.xxx 插件名,会生成带 android/ios/lib/ 的工程;pubspec.yaml 里会声明 pluginplatforms(android、ios 等)。
  2. 原生端:在 android/ 里用 MethodChannel 注册一个「通道名」,在 setMethodCallHandler 里根据 call.method(方法名)和 call.arguments(参数)做逻辑,结果通过 result.success(...)result.error(...) 回给 Dart。ios/ 里同样用 FlutterMethodChannelsetMethodCallHandler
  3. Dart 端:在插件库里 MethodChannel('通道名').invokeMethod('方法名', 参数),返回 Future,拿到原生端 result.success 传回来的值。
  4. 发布:写清 platforms、README、示例;发到 pub.dev 或私有仓库,别人 dependencies 里引用即可。

三、Platform Channel 和 FFI 各是什么

方式是什么通俗理解
Platform ChannelFlutter 的「通道」:Dart 发消息(方法名 + 参数)给原生,原生执行完再回传结果;参数与返回值需序列化三种 Channel(Method/Event/BasicMessage)适用场景见第 54 题适合调 Java/Kotlin、Swift/ObjC、系统 API、原生 UI。
FFI(dart:ffi)Dart 通过 dart:ffi 直接调 C/C++ 动态库(.so、.dylib、.dll),不经过通道、不序列化。适合高性能、已有 C 库、底层 C API;不适合调 Java/Swift 界面或服务。

四、怎么选(FFI 和 Platform Channel)

场景更合适的方式
Android/iOS 系统服务(相机、蓝牙、定位、通知、存储)Platform Channel,原生端用 Java/Kotlin、Swift/ObjC 调系统 API。
现成原生 SDK(如某厂商的 Java/Swift SDK)Platform Channel,在原生端封装 SDK,Dart 只调 Channel。
需要原生 UI(如系统选择器、地图原生控件)Platform Channel,原生端建 View/ViewController,通过 Channel 和 Flutter 通信。
高性能、算密集(图像处理、编解码、大量运算)且已有 C/C++ 库FFI,Dart 直接调 C 库,无序列化、延迟低。
系统底层 C API、或要复用 C 库FFI

五、小结:插件 = 原生 + Channel/FFI 互通;开发用 flutter create --template=plugin,Dart invokeMethod、原生 setMethodCallHandler。选型:系统/原生 SDK/UI → Platform Channel;高性能/C 库 → FFI。三种 Channel 见第 54 题。


57. 打包 Android / iOS 的流程与注意点?

Android

  • 命令:flutter build apk(通用)或 flutter build appbundle(上架推荐)。
  • 配置:android/app/build.gradle 里 versionCode、versionName、signingConfigs
  • 注意:minSdkVersion、targetSdkVersion、混淆(ProGuard/R8)。

iOS

  • 命令:flutter build ios;Xcode 打开 ios/Runner.xcworkspace。
  • 配置:签名、Bundle ID、证书与描述文件。
  • 注意:Info.plist 权限说明、iOS 最低版本。

通用
flutter clean 后重打;Release 用 --release;渠道/环境用 --dart-define 或 flavor。各平台构建产物路径与用途见第 58 题


58. Flutter 构建会生成哪些产物?各平台输出路径与用途?

平台命令产物路径产物/用途
Androidflutter build apk --releasebuild/app/outputs/flutter-apk/app-release.apk通用 APK,可直接安装;测试、内部分发
Androidflutter build apk --split-per-abi同目录下多 APK(arm64-v8a、armeabi-v7a、x86_64 等)按架构分包,单包体积更小
Androidflutter build appbundle --releasebuild/app/outputs/bundle/release/app-release.aabAAB,上传 Google Play;商店按设备生成优化 APK
iOSflutter build ios(需 Xcode 打包)在 Xcode 中 Archive 后导出 .ipaIPA,上传 App Store / TestFlight
Webflutter build webbuild/web/(含 main.dart.js、assets、index.html 等)静态资源,部署到 Web 服务器;可选 --web-renderer canvaskit/html--wasm
Windowsflutter build windowsbuild/windows/runner/Release/(exe + 依赖)Windows 桌面可执行包
macOSflutter build macosbuild/macos/Build/Products/Release/(.app)macOS 应用包
Linuxflutter build linuxbuild/linux/x64/release/bundle/Linux 可执行与库

注意:Android/iOS 需配置签名;Web 的 build/web 整目录部署;桌面端产物含引擎与 Dart 运行时,需整体分发。打包命令、签名与注意点见第 57 题


59. Flutter Web 和 桌面端开发要注意什么?

  • Web:渲染选 CanvasKit(一致性好、包大)或 HTML(包小、SEO 友好);SEO 需路由与 URL 同步(如 GoRouter);注意 CORS、Cookie/Storage,敏感存储见第 82 题。
  • 桌面:窗口/快捷键用 window_manager 或平台通道;文件用 file_pickerpath_providerflutter build windows/macos/linux 产物含引擎与运行时,需整体分发。

60. Hot Reload 和 Hot Restart 区别?Hot Reload 不能生效的情况?

  • Hot Reload:保留应用状态,只重新执行 build,改动 UI 和部分逻辑很快可见。
  • Hot Restart:重启整个应用,状态清空,main() 重新跑。
  • Hot Reload 可能不生效:改了 initState、全局变量、枚举、静态字段、main()、某些 mixin/扩展等,需要 Hot Restart 或完整重启。Hot Reload 大致原理见第 62 题

61. Flutter 的 build 模式:Debug、Profile、Release 区别?

模式编译特点用途
DebugJIT断言、Service Extension、调试信息;包大、慢日常开发、Hot Reload
ProfileAOT接近 Release 性能,保留 profiling性能分析(Timeline、真机 profile)
ReleaseAOT关闭调试、优化体积与速度上架、正式环境

命令:flutter run 默认 Debug;flutter run --profile / flutter build apk --release


62. Hot Reload 的大致原理是什么?

  • 是什么:不重启应用,把改动代码注入 Dart VM,触发 rebuild,保留 State(表单、滚动等)。
  • 流程:IDE 发增量 kernel → VM 加载/替换代码 → Flutter 标记脏 Element → 下一帧 rebuild 受影响子树,State 尽量保留。
  • 限制:initState、main()、全局/静态/枚举等改动常需 Hot Restart 或完整重启。

63. Flutter 有哪几种测试?unit / widget / integration 区别?

类型测什么工具/方式特点
单元测试(unit)Dart 逻辑、函数、状态转换test 包,flutter test快、不依赖 Flutter,适合业务与状态管理
组件测试(widget)单个 Widget 渲染与交互flutter_test:testWidgets、pumpWidget、find、tap可测 UI 与用户操作,中等速度
集成测试(integration)完整 App 流程integration_test,真机/模拟器慢、适合冒烟与关键路径

比例建议:单元 > 组件 > 集成,金字塔结构。


64. 如何测试有状态组件的用户操作?

核心思路:在 testWidgets 里挂载带状态的 Widget → 用 find 定位控件 → 用 tester 模拟点击/输入 → pump 推进一帧让 setState 生效 → 用 expect 断言 UI 或状态。

一、基本步骤

步骤做法说明
1. 挂载tester.pumpWidget(YourWidget())有状态组件用 pumpWidget 挂到测试环境,State 会创建并执行 initState/build
2. 定位find.byType(ElevatedButton)find.text('确定')find.byKey(Key('ok'))按类型、文字、Key 等找到要操作的 Widget
3. 操作tester.tap(find.byType(ElevatedButton))tester.enterText(find.byType(TextField), 'hello')模拟点击、输入、长按等,只触发回调,不会立刻重绘
4. 推进帧tester.pump()tester.pumpAndSettle()有状态组件在 setState 后要下一帧才重绘;pump() 推一帧,pumpAndSettle() 推到动画/定时器稳定
5. 断言expect(find.text('结果'), findsOneWidget)expect(find.byType(Text), findsNWidgets(2))断言界面是否出现预期 Widget 或数量

二、为什么要 pump?

  • 用户操作会触发 setState,但 setState 只是标记 dirty,真正 rebuild 发生在下一帧。测试里没有真实的 60fps 循环,必须手动 pump 一次(或多次)才会执行 build。
  • pump():推进 1 帧(默认 0ms),适合「点一下按钮,立刻看文字变化」。
  • pumpAndSettle():反复 pump 直到没有动画/定时器再请求帧(有超时),适合「点完有动画、Dialog 弹出」等。

三、依赖 Provider / InheritedWidget 时:被测用到了 context.watchProvider.of 时,挂载时外层包 Provider<MyState>.value(value: myFakeState, child: MyPage())MultiProvider

四、常见注意点

  • 异步:操作后有 Future 时用 pump() 推帧或 runAsync()pumpAndSettle(),必要时 mock。Key:多同类型时用 find.byKey 精确命中。mounted:异步回调时注意 pump 顺序,避免 setState after dispose。

小结:pumpWidget → find → tap/enterText → pump/pumpAndSettle → expect;依赖 InheritedWidget 时外层包 Provider。


65. pub 包管理:版本约束、plugin 与 package 区别?

一、版本约束

pubspec.yamldependencies 里写依赖名和版本规则,pub get 时会按规则解析出可用的版本。

写法含义示例
^1.2.3(caret)兼容 1.2.3 ≤ 版本 < 2.0.0,即同主版本内兼容常用,允许自动升级到 1.x 最新
1.2.3(无 ^)仅允许 1.2.3 这一版锁定版本,不自动升级
>=1.2.3 <2.0.0自定义区间需要精确控制时用
path: ../xxx依赖本地路径的包,不从 pub 拉本地开发、Monorepo
git: url ref: branch/tag依赖 Git 仓库的某分支/标签未发 pub 或用私有仓
dependency_overrides强制所有依赖里对某包使用的版本解决冲突时慎用,可能破坏兼容

二、plugin 与 package 对比

  • Package纯 Dart 的库,没有 Android/iOS 等平台原生代码,只提供 Dart API,可在 Dart/Flutter 项目里用,不涉及 MethodChannel、原生编译。
  • Plugin:在 Package 的基础上,额外带平台代码(如 android/ios/ 目录),通过 Platform Channel(MethodChannel/EventChannel 等)与原生通信,需要跑 flutter pub get 后参与各平台构建;pub.dev 上会标成 Plugin
对比项PackagePlugin
是否含平台代码否,仅 Dart是,含 Android/iOS 等原生实现
是否与原生通信是,通过 MethodChannel 等
pub.dev 标签PackagePlugin(并可能同时是 Package)
典型用途工具函数、状态管理、纯 Dart 逻辑、UI 组件(仅 Dart/Flutter)调系统 API、相机、蓝牙、原生 SDK、平台能力
创建方式flutter create --template=packageflutter create --template=pluginpubspec.yaml 里需声明 pluginplatforms

三、小结

  • 版本约束^1.2.3 表示 1.x 兼容;path/git 依赖本地或仓库;dependency_overrides 慎用。
  • Plugin 与 PackagePackage = 纯 Dart 库;Plugin = 带平台代码、能与原生通信的包;选型看是否需要调原生,需要则用 Plugin,否则用 Package。

66. Flutter 在 CI/CD 里一般怎么做?(flutter test、build、缓存)

一、常用流水线步骤

步骤命令/做法
依赖flutter pub get;可缓存 .dart_toolpubspec.lock 加速下次。
静态/分析flutter analyze;可选 dart format --set-exit-if-changed 检查格式。
测试flutter test 跑单元与 widget 测试;集成测试用 integration_testflutter_driver 在真机/模拟器跑(CI 需装 SDK、模拟器或真机)。
构建Androidflutter build apkflutter build appbundleiOS:需 macOS,flutter build ios 后 Xcode 归档;Webflutter build web
缓存缓存 Flutter SDKpub cache(如 ~/.pub-cache)、Gradle/Maven(Android)、CocoaPods(iOS),可明显缩短流水线时间。

二、注意

  • iOS 构建需要 Apple 证书与描述文件,CI 上常用 fastlane 或 CI 提供的签名;渠道/环境用 --dart-defineflavor 区分。

67. SafeArea 是做什么的?

  • SafeArea:给子 Widget 加安全区域内边距,避开刘海、状态栏、底部指示条、圆角等,避免内容被遮挡或裁切。
  • 常用于页面根布局,包一层 SafeArea 再放主体内容;也可用 SafeArea 的 left/top/right/bottom 单独控制哪边加 padding。

68. MediaQuery 常用属性?适配不同屏幕怎么做?

一、MediaQuery 常用属性

通过 MediaQuery.of(context) 取当前媒体信息,常用属性如下:

属性含义典型用法
size当前逻辑像素下的可用区域宽高(Size,宽×高)按比例布局、判断横竖屏、大屏/平板分栏
padding被系统 UI 占用的内边距(刘海、状态栏、导航条、安全区等)与 SafeArea 类似,自己做 padding 时用
viewPadding系统 UI 的“稳定”内边距(不随键盘等变化)需要不受键盘影响的 safe 区域时用
viewInsets被遮挡的 insets(如键盘弹出时底部被挡)键盘弹起时上推输入框、避免被挡
devicePixelRatio逻辑像素 : 物理像素 比例(如 2.0、3.0)需要物理尺寸、1:1 绘制时换算
textScaleFactor系统字体缩放倍数(无障碍大字号)文字布局要尊重用户字号设置
orientationportrait / landscape横竖屏布局切换、不同布局
platformBrightnessBrightness.light / dark跟随系统深色模式、状态栏图标颜色

注意:padding 在键盘弹起时可能被压缩;viewPadding 不变,viewInsets 反映键盘等遮挡,做“键盘避让”时常用 viewInsets.bottom

二、适配不同屏幕的常见做法

  • 避免写死尺寸:少用固定 width/height(px),多用 Flex + Expanded/Flexible比例(如 size.width * 0.8)、LayoutBuilder 拿约束再算子组件宽高,这样不同屏幕下可伸缩。
  • 安全区与刘海:用 SafeAreaMediaQuery.padding 给内容加边距,避开刘海、状态栏、底部指示条。
  • 尊重系统字号:布局和断行要考虑 textScaleFactor,避免“大字号”下文字溢出或重叠;必要时用 MediaQuery.textScalerOf(context)FittedBox
  • 横竖屏 / 大屏:用 MediaQuery.orientationsizeLayoutBuilder 判断横竖屏或宽度区间,做不同布局(如竖屏单栏、横屏/平板双栏);也可用 Breakpoint(如 responsive_framework)或自定义宽度阈值做断点。
  • 逻辑像素 vs 物理像素:UI 布局用 逻辑像素(size、padding 等);需要“真实物理大小”或与原生对齐时,用 devicePixelRatio 换算(物理 = 逻辑 × devicePixelRatio)。

小结:MediaQuery 提供 size、padding/viewPadding/viewInsets、devicePixelRatio、textScaleFactor、orientation、platformBrightness;适配用弹性布局 + 比例 + LayoutBuilder/断点,安全区用 SafeArea 或 padding,横竖屏用 orientation/size 分支。


69. ThemeData 如何做亮色/暗色主题切换?

一、ThemeData 与 MaterialApp 的关系

  • ThemeData:描述一套主题(颜色、字体、组件样式等)。Flutter 不强制「亮/暗」两套,但通常配合 MaterialApptheme(亮色)和 darkTheme(暗色)使用。
  • MaterialApp 三个关键参数:
    • theme:亮色主题,类型 ThemeData
    • darkTheme:暗色主题,类型 ThemeData
    • themeMode:当前用哪一套,类型 ThemeMode

二、themeMode 的三种取值

themeMode含义
ThemeMode.system跟随系统(深色模式开关),系统亮则用 theme,系统暗则用 darkTheme
ThemeMode.light强制亮色,始终用 theme
ThemeMode.dark强制暗色,始终用 darkTheme

不设 themeMode 时默认是 ThemeMode.system

三、如何做「运行时」亮/暗切换

思路:用状态保存当前是 light 还是 dark(或 system),把该状态传给 MaterialApp(themeMode: ...);用户点切换时更新状态并触发 rebuild。

  • StatefulWidgetThemeMode _mode = ThemeMode.system,切换时 setState(() => _mode = ThemeMode.dark),根用 MaterialApp(themeMode: _mode, theme: ..., darkTheme: ...)
  • Provider(或其它状态管理):存 ThemeMode,根 MaterialApp(themeMode: context.watch<ThemeNotifier>().mode, ...),切换时在 Notifier 里改 mode 并 notifyListeners()

这样无需重启 App,整棵 Widget 树会按新的 theme/darkTheme 重建,Theme.of(context) 会拿到对应那套 ThemeData。

四、ThemeData 里常配什么?子组件怎么取?

  • colorScheme:主色、背景色、错误色等,推荐优先用 ColorScheme.fromSeedcolorScheme: ThemeData.light().colorScheme 再改。
  • textTheme:标题、正文、按钮等文字样式。
  • appBarTheme:AppBar 背景、标题样式等。
  • useMaterial3:是否用 Material 3 风格(true/false)。

子组件里用 Theme.of(context) 取当前生效的 ThemeData,用 Theme.of(context).colorSchemeTheme.of(context).textTheme 等,这样在切换 themeMode 后会自动拿到亮色或暗色那套。

小结:MaterialApp 的 theme/darkTheme 两套,themeMode(system/light/dark)决定用哪套。运行时切换 = 状态存 themeMode 传给 MaterialApp,切换时 setState/notifyListeners;子组件 Theme.of(context) 取当前主题。


70. 图片加载与缓存注意点?

网络图
cached_network_image 等带缓存库,避免重复请求;设 placeholdererrorWidget;大图考虑 fit、缩略图。

本地图
Image.asset 会打包进应用;大图可 decode 后缩放到合适尺寸再显示,避免内存过大。

内存
列表大量图片时用 ListView.builder + 缓存库的缓存策略,避免 OOM;长列表优化见第 39 题


71. Flutter 常用本地存储方案?

方案类型适用场景
shared_preferences键值对简单配置、开关等(敏感数据如 token/密码见第 82 题
sqfliteSQLite 关系型复杂查询、大量结构化数据
hiveNoSQL、纯 Dart中等数据、对象存储、高性能
path_provider + File文件缓存、下载、大文件
MMKV高性能键值频繁读写、低延迟

选型:轻量配置用 shared_preferences;表结构复杂用 sqflite;要高性能、少依赖用 hive。


72. 如何做下拉刷新、上拉加载?

  • 下拉刷新RefreshIndicatorListView.builderonRefresh 返回 Future,里头发第一页请求(或 dispatch 刷新事件),请求完成后列表恢复。
  • 上拉加载ScrollController.addListener 里当 position.pixels >= position.maxScrollExtent - 阈值 时加载下一页;或 itemCount = list.length + 1,最后一项根据状态显示 loading / 没有更多。要防重复(loading 标志)、维护分页参数与「没有更多」状态,disposescrollController.dispose()
  • 完整实现(防重复、Footer 状态、代码示例)见第 98 题ScrollController 用法见第 78 题

73. WidgetsBindingObserver 是什么?常用场景?

  • WidgetsBindingObserver:监听应用生命周期(前后台、生命周期事件)的接口,在 didChangeAppLifecycleState 里拿到 AppLifecycleState(resumed / inactive / paused / detached 等)。
  • 常用场景:切到后台时暂停动画/音视频、暂停轮询;回到前台时刷新数据、恢复;在 State 的 initState 里 WidgetsBinding.instance.addObserver(this)、dispose 里 removeObserver

74. 说说 Overlay 和 OverlayEntry?

  • Overlay = 浮在 Navigator 页面上方的浮层栈,不占路由;OverlayEntry = 一条浮层,builder 建内容,insert 显示、remove 移除。MaterialApp 已配 Overlay,通常不用自建。适合 Toast、Loading、弹窗、悬浮按钮等。
  • API
操作写法
拿到当前 OverlayOverlay.of(context)Navigator.of(context).overlay
插入一条浮层Overlay.of(context).insert(entry);可 insert(entry, above: xxx) 控制叠放顺序
移除一条浮层entry.remove()
只刷新这条浮层entry.markNeedsBuild(),只重建该 entry 的 builder,不整棵子树重建

75. GestureDetector 和 Listener 区别?

对比项GestureDetectorListener
层级手势层(语义化)原始指针事件层
输入点击、长按、拖拽、缩放等手势onPointerDown / Move / Up 等,带 pointer 编号
冲突内置竞技场裁决手势冲突无,需自己处理
适用常见交互(按钮、滑动、缩放)画板、自定义滑动、精细控制、GestureDetector 无法表达的行为

需要“原始事件 + 手势”时,可 ListenerGestureDetector 或反过来,注意命中与传递顺序。


76. 如何实现自定义 Sliver?

  • Sliver 是什么:CustomScrollView 里的 Viewport 由多个 Sliver 组成,每一节根据 SliverConstraints(scrollOffset、视口剩余等)返回 SliverGeometry(scrollExtent、paintExtent 等),在 performLayout/paint 里完成布局与绘制。自定义 Sliver = 按这套协议实现一节可滚动区域。

常见做法

方式场景说明
SliverToBoxAdapter一截固定内容(头部、Banner)包一个 child,高度由 child 决定,不懒加载
SliverList / SliverGrid一节列表/网格用 SliverChildBuilderDelegate 按需 build,懒加载
SliverPersistentHeader 等吸顶、占满剩余等直接用系统 Sliver 或看源码仿写
自定义 RenderSliver视差、自定义吸顶、不规则高度实现 RenderSliver 子类,自己算 SliverGeometry

不写 RenderObject 时SliverToBoxAdapter(child: ...) 加一截固定内容;SliverList(delegate: SliverChildBuilderDelegate(...)) 加一节列表。自定义一节列表可写自己的 SliverChildDelegate 或包一层 SliverList。

完全自定义(RenderSliver):子类实现 performLayout(根据 SliverConstraints 算 geometry,对子节点 layout)、paint(用 SliverPaintContext 画子节点);Widget 层用 SliverWithSingleChildRenderObjectWidget 包一层,createRenderObject 返回该 RenderSliver。SliverGeometry 常用字段:scrollExtent(本节总长度)、paintExtent(可见需绘制长度)、layoutExtent、maxScrollObstruction(吸顶时压住前一段的长度)。


77. Form 与 TextFormField 如何做校验?FormState 怎么用?

  • Form:包一组可校验的 TextFormField,设 key: GlobalKey<FormState>,用 key.currentState 拿到 FormState
  • FormStatevalidate() 依次调所有子项 validator,全部通过返回 true,否则显示错误并返回 false;save() 依次调各子项 onSaved 取到值;reset() 清空校验状态。
  • TextFormFieldvalidator(value) 返回 String?,null 通过、非 null 为错误提示;onSaved(value)FormState.save() 时被调,可赋给变量;autovalidateMode 控制何时校验(disabled / onUserInteraction / always)。
  • 提交流程:先 formKey.currentState?.validate(),为 true 再 formKey.currentState!.save(),在 onSaved 里拿到值后发请求。

78. ScrollController 和 ScrollPhysics 分别做什么?监听滚动怎么实现?

一、ScrollController 是什么、做什么

  • ScrollController 用来绑定一个可滚动组件(ListView、GridView、CustomScrollView、SingleChildScrollView 等),通过 controller 参数传入。
  • 主要作用有两类:
    1. 控制滚动位置jumpTo(offset) 瞬间跳到某位置,animateTo(offset) 动画滚到某位置;可配合 initialScrollOffset 设置初始滚动位置。
    2. 监听滚动:通过 controller.offsetcontroller.positions 拿到当前滚动偏移和 ScrollPosition,用 addListener 在滚动过程中做逻辑(如吸顶栏显隐、上拉加载更多)。

二、ScrollController 常用 API

API作用
ScrollController(initialScrollOffset: 0)创建时可设初始滚动偏移
controller.offset当前滚动偏移(像素),只读
controller.positions当前关联的 ScrollPosition 集合(通常一个)
controller.jumpTo(offset)无动画跳到 offset
controller.animateTo(offset, duration: ..., curve: ...)动画滚到 offset
controller.addListener(callback)滚动时回调(offset 变化会触发)
controller.dispose()用完后在 State.dispose 里调用,避免泄漏

三、ScrollPhysics 是什么、做什么

  • ScrollPhysics 决定滚动手感:到顶/到底时是否回弹、是否有惯性、是否可 overscroll 等。传给可滚动组件的 physics 参数。
  • 常见几种:
类型效果
BouncingScrollPhysics到边界有回弹(类似 iOS),可 overscroll
ClampingScrollPhysics到边界硬停,无回弹(类似 Android 默认)
NeverScrollableScrollPhysics禁止滚动,常用于嵌套时禁止内层滚动
AlwaysScrollableScrollPhysics即使内容不足一屏也可拖拽(配合 shrinkWrap 等)

不传 physics 时,MaterialApp 下一般会根据平台选默认(如 iOS 用 Bouncing,Android 用 Clamping)。

  • 监听滚动:① ScrollController.addListener:绑到 ListView 等,回调里用 offsetposition.pixelsmaxScrollExtent 做「超过某值显示顶栏」「接近底部加载更多」等;dispose 里 removeListenerdispose()。② NotificationListener<ScrollNotification>:包住可滚动组件,onNotification 里用 metrics.pixels 等,不依赖 controller,适合父组件只关心子是否滚动的场景。

小结:ScrollController 负责绑定、jumpTo/animateTo、监听;ScrollPhysics 管手感(Bouncing/Clamping 等);用毕 dispose 里 removeListenerdispose()


79. dio 常用能力?拦截器、取消请求、超时怎么用?

一、dio 是什么

  • dio 是 Flutter 里常用的 HTTP 客户端,支持请求/响应拦截、取消、超时、FormData 上传/下载等,比 http 包功能更全,适合业务项目。

二、常用能力概览

能力用法
请求方法dio.get(url)dio.post(url, data: map)dio.putdio.deletequeryParameters 传 query,data 传 body
全局配置BaseOptionsbaseUrlconnectTimeoutreceiveTimeout(毫秒)、headerscontentType 等;Dio(BaseOptions(...)) 创建实例后,请求可写相对路径
上传FormData.fromMap({'file': MultipartFile.fromFile(path)})dio.post(url, data: formData)
下载/进度dio.download(url, savePath)onSendProgressonReceiveProgress 回调 (sent, total) 可做进度条

三、拦截器怎么用

  • InterceptorsWrapper 定义 onRequestonResponseonError,通过 dio.interceptors.add(InterceptorsWrapper(...)) 添加;执行顺序:请求前 onRequest → 发请求 → 收到响应后 onResponse,出错则走 onError
  • onRequest(options, handler):可修改 options(如 options.headers['Authorization'] = 'Bearer $token'),然后 handler.next(options) 继续,或 handler.reject 终止。
  • onResponse(response, handler):可对 response.data 统一解包(如取 data.data)、改格式,再 handler.next(response)
  • onError(err, handler):可统一弹窗、打日志、按状态码重试,再 handler.next(err)handler.resolve 改成成功。

四、取消请求怎么用

  • 创建 CancelToken,请求时传入:dio.get(url, cancelToken: token);需要取消时调 token.cancel('reason'),该 token 关联的未完成请求会被取消(会抛 DioExceptiontype == DioExceptionType.cancel)。
  • 典型用法:页面里持有一个 CancelToken,在 disposetoken.cancel(),避免页面销毁后回调里还在 setState;或搜索框防抖时取消上一次请求。

五、超时怎么用

  • 连接超时connectTimeout(从发请求到建立连接的最长时间,毫秒)。
  • 接收超时receiveTimeout(从建立连接到收完响应的最长时间,毫秒)。
  • BaseOptions 里统一设:BaseOptions(connectTimeout: Duration(seconds: 5), receiveTimeout: Duration(seconds: 10)),也可单次请求时传 Options(connectTimeout: ..., receiveTimeout: ...)。超时后会抛 DioExceptiontype == DioExceptionType.connectionTimeoutreceiveTimeout

小结:dio 常用 get/post、BaseOptions、FormData、进度;拦截器 InterceptorsWrapper(onRequest/onResponse/onError)做 token/解包/错误处理;取消 CancelToken + cancel();超时 connectTimeoutreceiveTimeout


80. Semantics 是什么?如何做无障碍(TalkBack/VoiceOver)?

  • 背景:无障碍让视障等用户通过读屏使用 App;TalkBack(Android)/ VoiceOver(iOS)会朗读焦点处的语义。Semantics 给 UI 贴语义(标签、角色、hint),形成 Semantics 树供读屏;自定义/纯图标需自包 Semantics。
  • 常用写法
需求做法
给无文字的图标/区域加说明Semantics 包住,设 label(主要读的内容)、hint(操作提示,如「双击提交」)
标明可点击Semantics(button: true, ...)label 里带「按钮」等,读屏会读成可点击
多个子节点合并成一句MergeSemantics 包住,读屏会把子节点语义合并成一条读出来,避免一句句拆开读
装饰性内容不想被读ExcludeSemantics 包住,这块就不会出现在语义树里,读屏会跳过
自定义控件在根 Widget 外包 Semantics,设 labelvalue(如进度)、hintbutton/link
  • 其他:对比度、可点区域 ≥44px、textScaleFactor 放大;大厂/政府常要求 WCAG。

81. Flutter 里如何做状态恢复(State Restoration)?场景是什么?

一、是什么

  • 切后台后进程可能被回收或页面被移除,再打开时若不做处理,滚动位置、表单输入等会丢失。状态恢复 = 回收/移除前把状态存起来,再次显示时读回并恢复 UI。

二、典型场景

场景说明
后台被系统杀进程Android 省电、内存紧张时杀后台进程;用户再点图标进来时 App 重新启动,若没做恢复,所有页面状态都是初始值。
页面被暂时移除如 Navigator 里某页被 pop 掉又 push 回来、或系统为省内存把不可见页回收,再显示时需要恢复该页的滚动、输入等。
长表单、多步骤流程用户填到一半切走,回来希望接着填,而不是从头开始。

三、Flutter 里怎么做(思路与 API)

  1. 建一个「恢复作用域」:在根用 MaterialApp(restorationScopeId: 'app') 或包一层 RestorationScope,表示「整棵子树参与状态恢复」,系统会在适当时机把存起来的数据按 restorationScopeId 和子节点的 restorationId 灌回来。
  2. 在需要恢复的 State 里:混入 RestorationMixin,用 RestorableProperty(如 RestorableIntRestorableStringRestorableBool)存要恢复的字段;在 restoreState 里把这类属性注册RestorationBucket,这样系统会负责「保存时序列化、恢复时反序列化并写回」。
  3. 恢复时机:系统在「应用从后台恢复」或「页面重新被插入」等时机,把之前保存的 RestorationBucket 数据灌回对应 State,RestorableProperty 会更新值并触发 UI 更新(如 notifyListeners)。
  4. 滚动位置ListViewScrollView 等若设置了 PageStorageKey,滚动位置会参与 PageStorage 的保存与恢复,可与 State Restoration 配合使用。

四、常用 API 小结

API作用
MaterialApp.restorationScopeId根恢复作用域 ID,整 App 的恢复数据会按这个 ID 存/取
RestorationMixin混入 State,提供 restoreStateRestorationBucket 等,用于注册要恢复的数据
RestorableInt / RestorableString / RestorableBool 等可序列化、可被恢复的「属性」,值变化时会被记录,恢复时写回
restoreState在 State 里重写,把 RestorableProperty 注册到 RestorationBucket
PageStorageKey给 ScrollView 等设 key,滚动位置会通过 PageStorage 参与保存/恢复
  • 注意:数据要可序列化;混合栈/复杂路由需与原生或路由方案配合。根设 restorationScopeId,State 用 RestorationMixin + RestorableProperty 在 restoreState 里注册;滚动用 PageStorageKey。

82. 敏感数据(token、密码)在 Flutter 里怎么存?和 SharedPreferences 的区别?

一、SharedPreferences 为什么不能存敏感数据

  • SharedPreferences 数据写在本地文件里,一般是明文或简单编码root备份可读,不适合存 token、密码、密钥等敏感信息。

二、敏感数据怎么存

  • flutter_secure_storage(或平台通道调原生安全存储):
    • Android:背后用 Keychain / Keystore,密钥由系统保护。
    • iOS:背后用 Keychain,数据由系统加密、与设备绑定。
  • 跨平台flutter_secure_storage 统一 API,适合存 token、密码、密钥。

三、和 SharedPreferences 对比

对比项SharedPreferencesflutter_secure_storage
存储方式本地文件,明文或简单编码系统安全存储(Keychain/Keystore),加密
适用普通配置、非敏感缓存(如主题、语言)token、密码、密钥等敏感数据
安全性易被 root/备份读取由系统保护,相对安全

四、小结

  • 敏感数据(token、密码)用 flutter_secure_storage 或平台原生安全存储;非敏感配置、缓存用 SharedPreferences 即可。

83. Dart 空安全是什么??!late 怎么用?

  • 空安全:类型默认非空,只有显式声明 ? 才可存 null,减少运行时空指针。
  • ?:可空类型,如 String?;调用前需判空或使用 ?.??
  • !:断言非空,对可空类型使用 x! 表示“我保证这里不是 null”,否则抛异常,慎用。
  • late:延迟初始化,首次访问前必须赋值;用于依赖注入、在 initState 里赋值的成员等。

84. Mixin 是什么?和继承的区别?

  • Mixin:用 mixin 定义、with 混入,把一批方法/属性“混”进类里,可多混入,解决 Dart 单继承限制。
  • 和继承区别:继承是“是一个”,单继承;Mixin 是“带有某种能力”,可多个。Flutter 里常见 SingleTickerProviderStateMixinAutomaticKeepAliveClientMixin 等。与 extends、implements 的完整区别及声明顺序见第 85 题。

85. mixin、extends、implements 在 Dart 里的区别与使用顺序?(大厂基础)

一、三者是什么

关键字含义
extends单继承:子类继承一个父类,获得其实现(方法、字段),可 override;Dart 只能 extends 一个类。
implements实现接口:类承诺实现某类型声明的所有成员(抽象方法、getter/setter),不继承任何实现;可 implements 多个接口。
mixinwith混入:把 mixin 的方法/字段实现「混进」当前类,不占继承位,用来在单继承下复用多份能力;可 with 多个 mixin。

二、使用顺序(语法规定)

  • extends 只能一个,且写在最前;接着 with 多个 mixin;最后 implements 多个接口。
  • 正确写法:class A extends B with M1, M2 implements I1, I2 { }

三、注意点

  • mixin 不能有有参构造函数(无参可省略);mixin 可用 on X, Y 限制「只能被 with 在 X、Y 的子类上」。
  • implements 只「承诺实现」,不带来实现;with 带来实现;extends 带来实现且占唯一的继承位。

86. const 构造函数有什么作用?什么时候用?

  • 作用:const 构造出来的实例是编译时常量,相同参数在同一程序里是同一实例,可减少对象创建、利于 Flutter 在 diff 时复用,减少 rebuild 成本。
  • 什么时候用:Widget 配置不变时尽量加 const(如 const Text('xx')const SizedBox());构造函数里所有字段都 final 且为编译时常量时才能声明 const 构造函数。

87. final 和 const 区别?

  • final:运行时常量,只能赋一次值,可在运行时确定(如 final x = getValue())。
  • const:编译时常量,值必须在编译期确定;const 变量隐式 final。
  • const 构造函数:创建的是编译时常量对象。

88. 级联操作符 .. 是做什么的?

  • ..:对同一对象连续调用方法或赋值,返回的是对象本身而不是方法返回值。
  • 例:controller..addListener(...)..forward() 等价于先 controller.addListener(...)controller.forward(),整体表达式的值是 controller。

89. BuildContext 是什么?能做什么、不能做什么?

是什么
Widget 在树中的位置上下文,用来查找祖先 InheritedWidget、获取路由/导航、Overlay、尺寸与位置等。

能做的
context.watch&lt;T&gt;()context.read&lt;T&gt;()Theme.of(context)MediaQuery.of(context)Navigator.of(context).push() 等。

不能 / 慎用

  • initState 里用 context 查 InheritedWidget(Theme、Provider 等),可能还不是最终依赖;详见第 95 题
  • context 在 dispose 后不可用,异步回调里用前要 mounted 判断。

90. InheritedWidget 原理?Provider 如何基于它实现?

InheritedWidget

  • 在树上层存数据;子节点通过 context.dependOnInheritedWidgetOfExactType<T>() 获取并注册依赖
  • 当该 InheritedWidget 更新时,依赖它的子节点会 didChangeDependencies 并重建。

Provider

  • 本质:InheritedWidget + ChangeNotifier
  • Provider.of / context.watch 内部调 dependOnInheritedWidgetOfExactType。
  • notifyListeners() 时 Provider 重建,依赖的子树就 rebuild。

91. Dart 里值传递和引用传递?库私有(_)是什么?

  • 传递方式:Dart 里传参是引用(reference)的拷贝;没有 C++ 那种“引用传递”,修改形参指向不会改变实参指向;修改形参指向的对象内容会反映到实参(同一对象)。表象上:
表现说明
简单类型(int、bool 等)不可变,看起来像“值拷贝”,函数内改不影响外部
对象类型传引用拷贝,函数内改对象内容会影响外部;重新赋值(换对象)不影响外部
  • 库私有:以下划线 _ 开头的顶级成员(变量、函数、类、mixin)是库私有,只在当前 .dart 文件(库) 内可见。面试可答:传参是“引用拷贝”;_ 开头即库内私有

92. Dart 的 extension 扩展方法是什么?有什么用?

一、是什么

  • extension 可以给已有类(包括 Dart SDK、第三方库)无侵入地增加方法getter不需要继承、也不用改原类代码。
  • 同文件或 import 后,该类型的实例就能像调用普通方法一样调用扩展方法。

二、有什么用

用途说明
语义化 API如对 int 扩展 duration getter,写 5.duration 代替 Duration(seconds: 5)
工具方法StringList 等扩展 format、parse、校验方法,不污染原类。
Flutter 里GetX 的 .obs.value 等通过 extension 挂在类型上;部分包对 BuildContext 扩展 themesize 等 getter。

三、注意

  • 扩展是静态解析的,不能多态;不同 extension 里同名方法用扩展名区分:MyExt(value).myMethod()

93. 错误处理:FlutterError.onError 和 Zone?

方式捕获范围典型用法
FlutterError.onErrorFlutter 框架层错误(build 里抛异常、布局错误等)设置后在该回调里上报、打日志、弹窗;可配合 FlutterError.presentError 决定是否再抛给系统
runZonedGuarded(Zone)异步未捕获异常(Future、async 里未 try-catch 的异常)runZonedGuarded(() => runApp(MyApp()), (e, s) { ... }),在回调里上报;与 FlutterError.onError 同时使用可覆盖同步 + 异步,统一上报
PlatformDispatcher.instance.onError异步错误与 Zone 二选一或配合使用

小结:同步 Flutter 错误用 FlutterError.onError;异步未捕获用 runZonedGuardedPlatformDispatcher.instance.onError;生产环境常两者都设,统一上报。


94. 如何做 Flutter 的国际化(多语言)?

  • 做什么:按当前 Locale 显示不同文案、数字/日期格式(如中文「确定」、英文「OK」)。
  • 官方方案
    1. 依赖:pubspec 里加 flutter_localizationsintl
    2. MaterialApp 三件套localizationsDelegates(系统 + 自己 App 的 delegate,如 AppLocalizations.delegate)、supportedLocales(如 [Locale('zh'), Locale('en')])、locale(当前语言,不设则跟系统)。
    3. 文案:写 Arb 文件(app_zh.arbapp_en.arb 等),用 flutter gen-l10n 生成 AppLocalizations,代码里 AppLocalizations.of(context)!.xxx
  • 运行时切换:用状态存当前 Locale 并传给 MaterialApp 的 locale,切换时更新状态触发重建即可;GetX 可用 Get.updateLocale(Locale('en'))
  • 注意:RTL 语言由 MaterialApp 根据 locale 处理;数字、日期用 intl 的 NumberFormatDateFormat 按 locale 格式化。

95. initState 里拿不到 context 依赖(如 Theme)怎么办?(字节/阿里)

一、原因

  • initState 执行时,State 刚挂上树context 还未完成对 InheritedWidget 的依赖注册,此时 Theme.of(context)MediaQuery.of(context)context.read<T>() 等可能拿不到拿不准(如拿到的是上层某处的旧值),且不会在 Theme 等变化时自动 rebuild。
  • 所以:initState 里不要用 context 去依赖 InheritedWidget(Theme、MediaQuery、Provider 等)。

二、做法

需求做法
用到 Theme/MediaQuery 等做逻辑放到 didChangeDependencies() 里:首次在 initState 之后调,之后 Theme 等变化也会调;或放到 build() 里直接 Theme.of(context)
根据主题设「初始值」并 setStatedidChangeDependenciesif (依赖已就绪) 取一次 Theme 等,setState 赋给成员变量,避免在 initState 里取。
必须在「首帧渲染之后」再执行WidgetsBinding.instance.addPostFrameCallback((_) { ... }),在首帧 paint 完成后的回调里再 Theme.of(context)context.read

三、小结

  • initState 里不用 context 依赖 Theme/MediaQuery/Provider;需要时放到 didChangeDependenciesbuild,或首帧后用 WidgetsBinding.instance.addPostFrameCallback

96. Flutter 里常见内存泄漏怎么避免?如何排查?

一、什么是内存泄漏(简要)

  • 页面或对象已经不用了(如页面 pop 掉),但仍有引用指向它,导致 GC 无法回收,占用的内存一直不释放,反复进出页面或操作后可能 OOM 或卡顿。

二、常见原因与对应避免方式

类型为什么容易漏怎么避免
Controller 未 disposeScrollController、AnimationController、TextEditingController 内部会注册监听、持有 context/Element,不 dispose 会一直挂着State.dispose() 里统一 controller.dispose();Controller 在 State 里创建、在 dispose 里释放
StreamSubscription 未 cancel对 Stream 调 listen 会返回 StreamSubscription,不 cancel 则回调会一直存在,若回调里用到了已销毁的 State 就会泄漏或报错subscription 存起来,在 disposesubscription.cancel()
Timer 未 cancelTimer.periodicTimercancel 会一直跑,回调里若引用 Widget/State 会泄漏disposetimer.cancel();或使用 TickerAnimationController 等与生命周期绑定的方式
Listener / Observer 未移除WidgetsBinding.instance.addObserver(this)ScrollController.addListener 等,不 remove 会一直收到回调disposeremoveObserver(this)controller.removeListener(...)
GlobalKey 长期持有 StateGlobalKey 会持有对应 State 的引用,若 Key 是全局/静态的,State 所在 Widget 树拆掉后 State 仍被 Key 引用,无法回收尽量不用 GlobalKey 存 State;若用,在不用时置空或确保 Key 作用域与页面一致
GetX / Bloc 等未释放Get.put 的 Controller 未 Get.delete、Bloc 未 close,会一直活在内存里随页面销毁时 Get.deletebloc.close(),或使用 Get.lazyPut + 路由绑定自动回收
  • 习惯:State 里创建的 Controller/Subscription/Timer 都在 dispose() 里释放(先 removeListener/cancel 再 dispose);异步回调里 setStateif (!mounted) return;Stream 的 listen 放在 initState、dispose 里 cancel。
  • 排查DevTools → Memory 拍快照,多次「进页→返回」后对比实例数量;Profile 模式跑;快照里搜 Controller/Subscription/State 看 Retaining path 找持有者。

97. CustomPainter 适用场景?和 CustomWidget 区别?

一、CustomPainter 是什么

  • CustomPainter 是一个抽象类,实现 paint(Canvas canvas, Size size) 和可选的 shouldRepaint。把它传给 CustomPaintpainter 参数,Flutter 在布局完成后会调用 paint,你在里面用 Canvas 直接画线、形、文字、图片、路径等,不通过子 Widget 组合,完全自绘。
  • CustomPaint 需要尺寸:要么设 size,要么包在 SizedBox / 有约束的父组件里(否则会尽可能大),foregroundPainter 可再叠一层绘制。

二、适用场景

场景说明
不规则图形、路径折线、曲线、多边形、贝塞尔等,用 Path + Canvas.drawPath 比用多个 Widget 拼更简单、性能更好。
图表柱状图、折线图、饼图等,大量线段/形状,用 Canvas 一次画完,避免成百上千个 Widget。
签名板、画板用户触摸轨迹用 Path 记录,在 paintdrawPath,配合 GestureDetector 收集点。
动效背景、粒子、渐变每帧重画,shouldRepaint 返回 true,在 paint 里根据时间或状态画;用 RepaintBoundary 包住可减少重绘范围。
大量重复简单图形如网格、点阵、纹理,用 Canvas 批量画比建大量 Widget 更轻量。

三、和「CustomWidget」(用 Widget 组合)的区别

  • 这里的 CustomWidget 泛指:用 StatelessWidget / StatefulWidget 组合已有 Widget(Container、Text、Row、CustomPaint 等)做成的自定义组件,布局和渲染都交给子 Widget 树。
  • 对比如下:
对比项CustomPainter(自绘)CustomWidget(组合子 Widget)
实现方式实现 paint(Canvas, Size),用 Canvasbuild 里 return 一堆子 Widget
层级更底层,直接操作 Canvas依赖现有 Widget,树更深
适用不规则图形、图表、签名、大量图形、每帧动效常规 UI:按钮、列表、表单、卡片组合
性能一个 CustomPaint 一层绘制,适合「一笔画完」的复杂图子 Widget 多时会有更多 layout/paint 节点;简单组合没问题
布局自己做布局,尺寸由 CustomPaintsize 或父约束决定由子 Widget 的 layout 决定
手势CustomPainter 不处理手势,需在外层包 GestureDetector子 Widget 可自带或包 GestureDetector

四、小结

  • CustomPainter 用在 CustomPaint 里,用 Canvas 自绘,适合不规则图形、图表、签名板、动效、大量图形;和用 Widget 组合的 CustomWidget 相比,更底层、适合「画」出来的 UI,组合 Widget 适合常规排版和交互。需要手势时在 CustomPaint 外包 GestureDetector

98. 你做过的最复杂的 Flutter 功能是什么?遇到过什么坑?

答:(示例思路,需结合自己项目)

复杂功能举例
长列表 + 多类型 + 下拉刷新/上拉加载 + 本地缓存;或复杂表单 + 多步骤 + 校验 + 草稿。

常见坑

  • 列表滑动与下拉刷新冲突。
  • 键盘弹起导致布局错乱。
  • iOS/Android 表现不一致。
  • 混合栈返回键、内存泄漏(Controller 未关闭)、多 isolate 通信等。

解决思路

  • 选对组件:RefreshIndicator + ListView.builder;MediaQuery 与 SafeArea;Platform 适配。
  • 混合栈:用 FlutterBoost 等规范路由。
  • 内存:dispose 里取消订阅;用 compute / isolate 规范通信。

99. 手写/口述:实现下拉刷新 + 上拉加载更多的列表,要考虑哪些点?(腾讯/美团场景题)

  • 下拉刷新RefreshIndicatorListView.builderonRefresh 返回 Future,里头发第一页请求(page=1、清空列表再请求),await 完再结束否则动画会提前收。
  • 上拉加载ScrollController.addListener 里判断 position.pixels >= maxScrollExtent - 阈值(如 200)!_loading && !_noMore 时调 _loadMore();维护 page_noMore,返回条数 < pageSize 时设 _noMore 并底部显示「没有更多了」。
  • 防重复bool _loading,请求前 if (_loading) return 并置 true,try/finally 里置 false;disposescrollController.removeListenerscrollController.dispose()
  • 列表与 FooteritemCount: list.length + 1,最后一项按状态显示加载中 / 失败重试 / 没有更多;失败可 retryCount 或 Footer 点「重试」再调 _loadMore。
  • 优化RepaintBoundary 包 item、itemExtent 固定高度、图片缓存;大厂会问 Footer 状态机(idle / loading / error / noMore)。

100. 手写/口述:登录页(手机号格式化、60 秒验证码倒计时、表单校验)核心思路?(大厂场景题)

  • 手机号inputFormattersFilteringTextInputFormatter.digitsOnly + LengthLimitingTextInputFormatter(11);需 3-4-4 空格或脱敏时,onChanged 里处理或自定义 TextInputFormatter,脱敏用 substring 拼前 3 + "****" + 后 4 展示。
  • 60 秒倒计时Timer.periodic(Duration(seconds: 1)) 每秒 setState 减一,到 0 时 cancel;按钮 onPressed 在 countdown > 0 时置 null,dispose_timer?.cancel() 防泄漏。
  • 表单校验Form + GlobalKey<FormState> 包各 TextFormFieldvalidator 里手机号 1 开头 11 位、验证码 6 位、密码长度等,返回 null 通过。提交前 formKey.currentState?.validate() 通过再发请求。详见第 76 题。
  • 防重复提交bool _isSubmitting,提交里先判断再置 true,await 请求后 finally 置 false;按钮 onPressed 在 _isSubmitting 时传 null。