Skip to content

IM · 移动端 / Flutter 面试题 ​

B Flutter 域已有 44 篇系统文章 + 3 份速查(Riverpod / GoRouter / Freezed), 本篇绝大部分题目直接链过去,只补充域内没有的(Dart 手写、跨端原理对比、Isolate 通信、热更新)。

本项目技术栈:Riverpod 2.6 + GoRouter 14 + Freezed 2.5 + Retrofit 4 + Dio 5。

📌 章序:Dart → 渲染 → 布局 → 状态 → 路由 → 数据持久化 → 性能 → 平台能力 → 测试 → 监控 → 架构 → 工程化 → 跨端对比。


高频 Top 10 ​

#题目去哪补
1Flutter 三棵树各自职责B03
2setState 之后发生了什么B03
3Widget 为什么不可变、Key 有什么用B02
4"约束向下、尺寸向上" 怎么理解B04
5RenderFlex overflowed 怎么解B04 / SB01
6状态管理怎么选(Provider/Bloc/GetX/Riverpod)B27
7Riverpod 的 watch/read/listen 区别速查 06
8列表卡顿怎么优化B22
9为什么要用 const 构造函数B22
10Platform Channel 有哪几种B21

一、Dart 语言 ​

Q:mixin 和 extension 的区别?(内联)

A:mixin 是可复用的类成员注入(with 混入,可 on 限定宿主类型,解决单继承的限制); extension 是给已有类型加语法糖方法(不改原类,编译期静态解析,不能被 override)。 典型:状态复用用 mixin(如 AutomaticKeepAliveClientMixin),工具方法用 extension。

Q:Future 和 Isolate 的区别?什么时候必须用 Isolate?

A:Future 是单线程内的异步(靠事件循环,不并发); Isolate 是真正的并行(独立内存堆,靠 ReceivePort/SendPort 消息通信,不共享内存)。 CPU 密集任务(大 JSON 解析、图片处理、加密)必须用 Isolate,否则会卡掉 UI 线程。

→ 详见 B06 · Dart 异步并发

Q:Isolate 之间怎么通信?(内联)

dart
final receivePort = ReceivePort();
await Isolate.spawn(worker, receivePort.sendPort);   // 把主 isolate 的 sendPort 传进去
receivePort.listen((msg) => print('收到 $msg'));

void worker(SendPort sendPort) {
  final result = heavyCompute();
  sendPort.send(result);      // 只能传**可序列化**的值(基础类型/List/Map)
}

💡 Flutter 提供了更简单的 compute(fn, message)(内部即 Isolate),适合一次性重任务。

Q:空安全里 late、?、! 怎么用?

A:Type? 可空;! 强制解包(不确定时别用,会抛异常); late 延迟初始化(声明时不赋值,首次访问才初始化,适合依赖注入/异步赋值的非空字段)。

→ 详见 B01 · Dart 语言基础

Q:手写 Dart 单例(内联)

dart
class ApiClient {
  ApiClient._();
  static final ApiClient instance = ApiClient._();   // ✅ 静态字段天然懒加载 + 线程安全
  factory ApiClient() => instance;
}

二、渲染原理 ​

Q:Flutter 的三棵树分别是什么?

A:Widget 树(配置/蓝图,不可变、频繁重建)→ Element 树(中间层,负责 diff 与复用) → RenderObject 树(真正布局与绘制,创建成本高、尽量复用)。 Widget 廉价重建,Element/RenderObject 尽量复用,这是 Flutter 性能模型的核心。

→ 详见 B03

Q:setState 之后发生了什么?

A:标记对应 Element 为 dirty → 下一帧重建 Widget → Element 树 diff(能复用就复用) → RenderObject 重新布局/绘制 → 合成上屏。 注意:setState 只重建该 State 对应的子树(不是整棵树)。

→ 详见 B03

Q:Key 有什么用?什么时候必须加?

A:Key 让框架在 Widget 重建时正确匹配旧的 Element/State。 列表中有状态组件且顺序会变时必须加(否则状态会跟错对象)——与前端 key 同理。 ValueKey(按值)/ ObjectKey(按引用)/ GlobalKey(跨树访问 State)。

→ 详见 B02

Q:Flutter 的生命周期有哪些?

A:initState → didChangeDependencies → build → didUpdateWidget → deactivate → dispose; 另有 didUpdateWidget、reassemble(热重载)。dispose 里必须清理:控制器、监听、定时器、订阅。

→ 详见 B03


三、布局 ​

Q:怎么理解"约束向下、尺寸向上"?

A:父级给子级约束(min/max 宽高)→ 子级在约束内决定自己的尺寸 → 父级根据子级尺寸决定位置。 所以子级不能自己想要多大就多大;想突破约束要用 UnconstrainedBox/OverflowBox。

→ 详见 B04

Q:RenderFlex overflowed 怎么排查?

A:本质是子组件想要的尺寸超出父约束。 常见解法:给 Row/Column 里的长文本包 Expanded/Flexible;列表改 ListView; Row 里的 TextField 要包 Expanded;开启 DevTools 的 "Show Guidelines" 看约束。

→ 详见 B04 / SB01 报错速查

Q:Sliver 是什么?什么时候用?

A:Sliver 是可被滚动容器按需懒加载的一块区域(SliverList/SliverGrid/SliverAppBar)。 需要"多个滚动区域协同、折叠头部、自定义滚动效果"时用 CustomScrollView + Slivers。

→ 详见 B04 / B28 自定义渲染


四、状态管理 ​

Q:状态管理方案怎么选?

A:setState(局部、简单)→ Riverpod(本项目选择,编译安全、无 Context 依赖、易测试) → Bloc(强约束、事件驱动、大团队)→ GetX(轻量但易滥用、不推荐大型项目)。 判断维度:团队规模、是否需要强可追溯、测试要求、学习成本。

→ 详见 B27 · 状态管理选型与横向对比

Q:Riverpod 的 watch / read / listen 区别?

A:watch 值变了重建(用在 build);read 只取一次、不订阅(用在回调); listen 变化时不重建、只触发副作用(弹窗、跳转)。 回调里误用 watch 是高频错误。

→ 详见 速查 06 · Riverpod / B08

Q:AsyncValue 怎么处理异步三态?

A:when(data/loading/error) 全量处理;maybeWhen 只处理部分并用 orElse 兜底。 想"刷新时保留旧数据":先存旧值再置 AsyncLoading,或用 AsyncValue.guard。

→ 详见 速查 06

Q:Riverpod 里怎么减少无谓重建?

A:ref.watch(p.select((s) => s.field)) 只订阅用到的字段; 把消费者下沉到最小子树(用 Consumer 包局部而不是整个页面); 列表项抽成独立 Widget。

→ 详见 速查 06


五、路由 ​

Q:go 和 push 的区别?

A:go 替换整个路由栈(适合底部 Tab 切换、登录后进主页); push 压栈可返回(适合列表进详情)。另有 replace(替换当前)、pop(出栈,可带返回值)。 出栈前建议判 context.canPop()。

→ 详见 速查 07 · GoRouter

Q:登录守卫怎么写?为什么登录后不自动跳转?

A:在 GoRouter.redirect 里判断登录态,未登录返回 /login。 必须配 refreshListenable(把登录状态传进去),否则登录成功后 redirect 不会重新触发。 同时要判断"当前已在登录页"以防无限重定向。

→ 详见 速查 07 / B13


六、数据层与持久化 ​

Q:本地存储怎么选?

方案适用特点
SharedPreferences少量 KV(配置、token、开关)简单;不适合大量数据,注意 iOS 上是异步
文件(path_provider)大块数据、图片/日志缓存灵活,自己管格式与清理
SQLite(drift/sqflite)结构化、需要查询/关联/事务强查询能力,drift 有编译期 SQL 校验
Hive / Isar高频读写的对象存储快、纯 Dart、无需 SQL;Isar 功能更强
安全存储(flutter_secure_storage)Token、密钥等敏感数据走 Keychain / Keystore

选型判断:配置类 → SharedPreferences;结构化 + 复杂查询 → SQLite/drift; 对象缓存 + 高频读写 → Isar;任何密钥类一律走安全存储,别图省事放 SharedPreferences。

→ 详见 B18 · 本地存储与持久化

Q:数据层怎么分层?Repository 该做什么?

A:典型三层——DataSource(远程 API / 本地 DB)→ Repository(统一出口:缓存策略、合并、错误转换)→ 领域/状态层。 Repository 的职责是屏蔽数据来源:上层不知道数据来自网络还是缓存。 常见策略:先返回缓存再刷新(stale-while-revalidate)、失败回落缓存、写操作先更新本地再同步。

→ 详见 B09 · 数据层链路

Q:离线与缓存怎么设计?(内联)

A:四件事:① 缓存优先 + 过期策略(TTL 或版本号);② 写操作进待同步队列,恢复网络后重放; ③ 冲突处理(时间戳 / 版本号 / 服务端裁决);④ UI 要能表达"这是本地未同步的数据"。


七、性能 ​

Q:Flutter 的帧预算是多少?掉帧怎么定位?

A:60fps → 16.67ms/帧;UI 线程(构建布局)与 Raster 线程(光栅化)都要在一帧内完成。 用 profile 模式(不是 debug)+ DevTools 的 Performance 与 "Track Widget Builds" 定位到具体 Widget。

→ 详见 B22

Q:常见的性能手段有哪些?

A:① 多用 const 构造(减少重建);② 把重建范围下沉(拆分 Widget、Consumer 局部消费); ③ RepaintBoundary 隔离重绘;④ 长列表用 ListView.builder 懒加载; ⑤ 图片指定 cacheWidth 控制内存;⑥ 避免在 build 里做重计算。

→ 详见 B22

Q:常见的内存泄漏点?

A:未 dispose 的 TextEditingController/AnimationController/ScrollController; 未取消的定时器与 Stream 订阅;全局缓存持有 BuildContext;图片缓存过大。

→ 详见 B22


八、原生与平台能力 ​

Q:Platform Channel 有哪几种?

A:MethodChannel(方法调用,最常用)、EventChannel(原生主动推流,如传感器)、 BasicMessageChannel(双向消息)。注意线程:原生侧回调要回到主线程。

→ 详见 B21

Q:混合开发(Add-to-App)要注意什么?

A:引擎复用与预热、路由栈与原生导航协调、内存占用(多引擎 vs 单引擎)、 生命周期同步、包体积增量、调试链路。

→ 详见 B26

Q:权限与设备能力怎么申请?有哪些坑?(内联)

A:流程:在清单里声明(Android AndroidManifest.xml / iOS Info.plist 的用途说明)→ 运行时申请 → 处理"永久拒绝"。 坑:① iOS 没写用途描述会直接崩溃;② Android 13+ 细化了通知与媒体权限; ③ 被永久拒后系统不再弹框,要引导去设置页;④ 定位/相册有"仅使用时允许"与"仅限本次"两档。

→ 详见 B15 · 权限与设备能力

Q:推送与深链(Deep Link)怎么落地?(内联)

A:推送分 FCM/APNs 通道与厂商通道(国内必须接小米/华为/OPPO/vivo,否则收不到); 注意冷启动点击的处理、通知权限、token 上报与失效刷新。 深链:App Links(Android) / Universal Links(iOS) 需要服务端放 assetlinks.json / apple-app-site-association 做域名归属校验; Scheme 方式兼容好但会被劫持与弹窗询问。冷启动时深链要延后到首页就绪再跳转,否则路由栈未初始化会丢。

→ 详见 B24 · 推送通知与深链接


九、测试与质量 ​

Q:Flutter 的三层测试怎么写?

类型测什么速度
单元测试纯函数、Model、状态管理(Riverpod 可脱离 UI 测)最快
Widget 测试单个组件的渲染与交互(testWidgets + pumpWidget)中
集成测试完整流程,真机/模拟器跑(integration_test)最慢

要点:Riverpod 用 ProviderContainer 做 override,可以不依赖 BuildContext 测逻辑; Widget 测试用 Finder 找组件,交互用 tester.tap + pumpAndSettle; 网络层用 mockito/mocktail 打桩,别在单测里打真实接口。

→ 详见 B19 · 测试实战

Q:覆盖率有意义吗?多少合适?(内联)

和前端同理:覆盖率是发现盲区的工具,不是 KPI。 优先保证纯逻辑与状态层的高覆盖,UI 层靠少量关键路径的 Widget/集成测试。 追求 100% 会把测试写成"实现快照",一改就红,反而拖垮迭代。


十、线上监控与崩溃 ​

Q:Flutter 的错误怎么捕获?为什么有些错误抓不到?

A:三层都要接,缺一层就会漏:

层做法
Flutter 框架层FlutterError.onError(构建/布局/绘制异常)
Dart 异步层runZonedGuarded 的 onError(未捕获的异步异常)
原生层iOS 的 NSException/信号、Android 的 Thread.UncaughtExceptionHandler(原生崩溃 Dart 层看不见)

另外要把 ErrorWidget.builder 换成友好页面,避免线上红屏。

→ 详见 B20 · 异常捕获与错误监控

Q:崩溃上报后怎么定位?(内联)

A:关键是符号化——Release 包必须开 --obfuscate 并保留符号表文件, 拿到崩溃堆栈后用 flutter symbolize 还原成真实方法名。 再加:用户操作面包屑、日志、版本/机型/系统维度聚合。 没有符号表的混淆包,崩溃日志基本没法看——这是最容易漏的一步。


十一、架构分层与演进 ​

Q:项目怎么分层?什么时候该重构?

A:常见演进路径:"全写在 Widget 里" → 抽 Provider/Notifier → 分数据层 → 分领域层。 分层原则:依赖向内、UI 不直接碰数据源、跨层走接口。 判断该重构的信号:一个 Widget 文件超过 300 行且混着网络请求、业务逻辑无法单测、改一处要在 5 个地方同步改。

→ 详见 B31 · 架构模式与分层演进 / B07 · 项目架构总览

Q:Clean Architecture 在 Flutter 里要不要上?(内联)

A:看项目规模。小项目全套 Clean(entity/usecase/repository/datasource 四层)会产生大量样板代码,得不偿失。 务实做法:先把"UI / 状态 / 数据"三层切干净,等业务复杂到出现"多个页面复用同一段业务规则"时,再抽 usecase 层。 判断标准:有没有"跨页面复用的业务规则",没有就别上。

Q:包与插件怎么抽?什么时候该做插件?(内联)

A:抽包时机:第三处用到才抽(Rule of Three)。 插件 plugin(含原生代码)vs 纯 Dart package(无原生)。 发布前要有:示例工程、README、CHANGELOG、单元测试、flutter analyze 干净、平台声明。

→ 详见 B30 · 包与插件开发


十二、工程化与发布 ​

Q:Freezed 的 when 和 maybeWhen 区别?

A:when 必须穷尽所有分支(新增分支时编译报错,安全); maybeWhen 用 orElse 兜底(新增分支不会报错,注意漏改)。状态机建议用 when。

→ 详见 速查 08 · Freezed

Q:build_runner 报冲突怎么办?

A:build --delete-conflicting-outputs;仍不行就 clean 后重建。 嵌套对象序列化报 Converting object to an encodable object failed → 开 explicitToJson。

→ 详见 速查 08

Q:包体积怎么优化?

A:--split-per-abi/--obfuscate + 符号表、移除未用资源、字体子集化、压缩图片(WebP)、 --tree-shake-icons、延迟加载组件(deferred components)。

→ 详见 B23

Q:国际化(i18n)怎么做?有哪些坑?(内联)

A:主流用 flutter_localizations + ARB 文件(intl / flutter_intl 生成), 语言切换要重建顶层 Widget 并持久化选择。 坑:① 复数/性别/日期与数字的本地化格式(用 intl 的 NumberFormat/DateFormat,别手写); ② 文案里有参数要用占位符,别字符串拼接(语序在不同语言里不同); ③ 从右到左语言(RTL)需要 Directionality 与布局双向测试。

→ 详见 B14 · 国际化

Q:发布流程有哪些必须卡的点?(内联)

清单:版本号与 build number 递增 → 混淆并归档符号表 → 分 ABI 包 → 多机型回归(至少 iOS + 一个低端安卓)→ 灰度(按百分比/渠道)→ 监控崩溃率与关键指标 → 保留回滚能力。 国内安卓还要各应用商店的渠道包与合规(隐私清单、权限说明)。


十三、跨端对比 ​

Q:Flutter 和 RN / uni-app 的本质差异?

A:Flutter 自绘(Skia/Impeller 直接渲染,不依赖原生控件)→ 跨端一致性最好、性能好,但包体积大、生态需自建; RN 用原生控件(JS 桥接/JSI)→ 体积小、接近原生观感,但跨端一致性差、升级易踩坑; uni-app 是 WebView/小程序方案,生态与上手成本低,性能与体验上限较低。

→ 详见 A01 · 跨端方案横向对比

Q:Flutter Web 有什么限制?为什么不适合做需要 SEO 的站点?

A:编译产物是 JS + canvas/DOM 渲染器,天然只能 CSR,没有服务端 HTML 生成, 爬虫与首屏体验都不友好。另外有 CORS、路由 fallback、首屏体积三个必踩的坑。

→ 详见 B25 / A13 渲染模式


十四、进阶补充主题(新题) ​

Q:Flutter 的无障碍是什么?为什么"视觉还原"不等于"无障碍达标"?

A:Flutter 每一帧会从 Widget 树推导一棵语义树(Semantics Tree),读屏只读这棵树。 内建 Text / IconButton 自带部分语义,但纯图标按钮、图片、自定义绘制、手势都不会自动有语义—— 要用 Semantics / MergeSemantics / excludeSemantics 手动补。视觉还原只证明"看起来对",不代表读屏用户能用。

→ 详见 B35 无障碍

Q:App 被系统杀掉进程后,状态怎么恢复?

A:默认不恢复,要用 RestorationMixin + RestorableProperty(RestorableInt / RestorableTextEditingController 等) 在 RestorationScope 下注册。注意 Riverpod 的 Provider 状态在内存里,进程被杀就没了,需要恢复的是用户可见关键态 (Tab、输入、滚动),跨会话数据仍走本地存储。

→ 详见 B36 状态恢复

Q:Flutter 怎么做后台任务?定时器能在后台一直跑吗?

A:不能。三层能力要分清:Isolate(App 活着时并行计算)、workmanager(系统调度,Android 受 Doze、iOS 由 BGTask 决定时机)、 推送(实时唤醒)。"每天提醒"应走本地通知/服务端推送,而不是后台轮询;保活要靠厂商推送通道,不是前台 Service 硬扛。

→ 详见 B37 后台任务

Q:埋点的关键设计点有哪些?

A:先定事件字典(统一 schema)→ 用事件总线 + Provider 统一入口 → 批量上报 + 离线补偿 + 去重 → 曝光不等于进页面(要判定可见)→ 隐私合规(先授权后采集、最小化、可关闭)。

→ 详见 B38 埋点

Q:Flutter 怎么和 IoT 设备通信?能源 App 怎么选?

A:三条链路:BLE(近场配网,flutter_blue_plus,注意 Android 权限分裂与并发写入排队)、 MQTT(云端遥测,QoS / 遗嘱 / retain,配退避重连)、Modbus(工业设备,寄存器映射 + CRC)。 统一抽象 DeviceChannel,离线判定靠心跳 + 遗嘱,告警走服务端推送。

→ 详见 B39 IoT 协议

Q:Flutter 图表选型怎么定?

A:常规折线/柱状/饼图用 fl_chart;数据量大/视觉要完全控制用 CustomPainter 自绘; 只有超复杂图表(桑基/3D/地图)才考虑 WebView + ECharts(桥接成本高)。配色要接 AppTokens,空态要占位。

→ 详见 B40 图表可视化

Q:Flutter 支持 Shader 吗?什么时候用?

A:支持 FragmentShader + GLSL(.frag 资源),适合图表水波、背景动效等增强;但它是逐像素计算, 性能敏感,全屏常驻要慎重。注意 Skia / Impeller / Web 的 Shader 差异,必须真机验证。

→ 详见 B41 Shader

Q:移动 App 安全纵深有哪些层?

A:传输层(SSL Pinning + 证书轮换)→ 存储层(flutter_secure_storage,密钥不进 Dart 源码)→ 代码层(R8 混淆 + 加固,混淆不是安全边界)→ 运行时(root/越狱检测、反调试,关键判定下沉原生)。

→ 详见 B42 移动安全

Q:Flutter Monorepo 是什么?什么时候不该用?

A:Melos / pub workspace 管理"App + 内部包"在同一仓库协同演进(melos bootstrap 链接、melos run 批量命令)。 但单 App 无复用诉求、团队无 CI/发版规范时,硬拆多包反而增加成本。

→ 详见 B43 Monorepo

Q:Flutter 能线上热更新吗?

A:没有官方一键热更新。iOS 禁止动态下发可执行代码,Android 技术上可行但链路成本高。 绝大多数"不想发版"的需求用远程配置 + 灰度解决(缓存 / 兜底 / 灰度 / 埋点四要素),代码热更新是最后选项。

→ 详见 B34 动态化与热更新


自检清单 ​

  • [ ] 能讲清三棵树与 setState 之后的完整流程
  • [ ] 能解释"约束向下、尺寸向上"并会解溢出报错
  • [ ] 能说清 Riverpod 的 watch/read/listen 与 select 优化
  • [ ] 能列出 5 条性能手段与 4 个内存泄漏点
  • [ ] 能说清 Flutter 与 RN / uni-app 的本质差异,以及 Flutter Web 的限制
  • [ ] 能说清本地存储的选型(SharedPreferences / SQLite / Isar / 安全存储)与数据分层
  • [ ] 能说清 Flutter 错误的三层捕获与符号化还原
  • [ ] 能给出测试金字塔与"覆盖率不是 KPI"的判断
  • [ ] 能说清分层的演进路径,以及什么时候不该上 Clean Architecture
  • [ ] 能说清无障碍语义树、状态恢复、后台任务与埋点的核心设计
  • [ ] 能给出移动安全纵深与 IoT / 图表 / Shader / Monorepo 的选型结论

相关文档 ​