移动端是产品的第一现场。Flutter 用一套 Dart 代码同时产出 Android / iOS / 桌面 / Web 应用,靠自绘引擎做到接近原生的流畅度。本页聚焦 Flutter 框架 + Dart 语言 + Android 原生集成 + 上架性能这条完整知识链——既讲"怎么写出来",也讲"什么时候必须落到原生层、怎么上架、怎么不卡"。我会像讲 Java 进阶那样,把每个知识点拆成"它是什么 → 为什么这样设计 → 怎么用 → 踩过什么坑"。基线:Flutter 3.3+(Dart 3,全面 Null Safety)、Android Studio / VS Code、Android Gradle Plugin 8.x。
为什么选 Flutter,以及把环境搭起来
跨端方案不少:React Native 靠桥接调原生组件,性能有损耗;原生双端各写一套成本高。Flutter 走的是"自绘 + 编译到原生"路线——控件自己画,不依赖系统控件,所以两端长得一样、跑得一样快。这一章先把"值不值得学"和"怎么跑起来"讲清楚。
Flutter 的三张底牌:它凭什么跨端还流畅
很多人以为"跨端"等于"妥协性能"。Flutter 偏偏反其道:它不借系统控件,而是自己拿笔画。理解这三张牌,后面所有设计才好懂。
论Flutter 凭什么又跨又流畅
自绘引擎(Skia / Impeller):所有 Widget 最终由 Flutter 自己渲染到画布,不依赖平台控件,跨端一致性极强,也不会出现"安卓圆角一个样、iOS 圆角另一个样"。
Dart 编译到原生:Release 模式把 Dart 编译成机器码(AOT),没有 JS 桥接的解释损耗,启动快、运行快。
热重载(Hot Reload):改一行 UI 毫秒级 reload,状态还保留,开发体验是移动端里最顺的之一。
三种跨端方案横向对比:别只看宣传
选型不是"谁好谁坏",而是"你的场景吃哪套"。下面这张表把主流方案的取舍摆出来,免得踩了才知道。
| 方案 | 渲染方式 | 优点 | 代价 |
|---|---|---|---|
| 原生双端 | 各平台原生控件 | 最原生、性能最好、生态全 | 成本高、两套人、迭代慢 |
| React Native | 桥接调用原生控件 | 复用 Web 技术栈、社区大 | 桥接损耗、两端易不一致 |
| Flutter | 自绘(不依赖系统控件) | 一致、流畅、热重载爽 | 包体略大、离系统能力远 |
Flutter 的"一套代码"指 业务逻辑与 UI 复用。但原生集成、签名上架、权限声明仍是各平台各一套。真要做"两套端都合规上架",原生那些坑一个都绕不掉。先有心理预期,后面第 5、6 章会专门打。
环境搭建:三步跑通
官方提供 flutter doctor 当"体检表",缺什么提示什么。下面是标准安装路径。
| 组件 | 装什么 | 验证 |
|---|---|---|
| Flutter SDK | 官网下载 Stable 版,解压并把 bin 加进 PATH。 | flutter --version 出版本即成功。 |
| Android 侧 | 装 Android Studio + Android SDK;想真机调试开 USB 调试。 | flutter doctor 里 Android 工具链全绿。 |
| iOS 侧(需 Mac) | 装 Xcode;sudo xcodebuild -license 同意协议。 | flutter doctor 里 iOS 工具链通过。 |
把 Flutter 加进 PATH(macOS / Linux 示例)
flutter doctor 与新手第一坑
flutter doctor 的输出里,绿色对勾是通过,红色叉是有问题。它列的是"缺什么"而不是"你错了",逐条补上即可。
① 一堆红叉就慌:按提示逐条装,别跳过。尤其容易漏 Android 许可证和"没连设备"。② 装了 Xcode 却没跑 license:会卡在 iOS 工具链,记得 sudo xcodebuild -license accept。③ PATH 没生效:新开终端才认得到 flutter 命令,老终端要重开或 source。
第一个工程与热重载:毫秒级看到改动
新建工程后,flutter run 连上设备即启动。开发时改完代码,按 r(热重载)或 R(热重启),界面立刻变,而且当前页面状态还在——比如你填了一半的表单不会丢。
论热重载为什么这么爽
传统原生改个布局要重新编译安装,动辄几十秒到几分钟。Flutter 把改动的代码注入正在运行的 Dart VM,只重建受影响的 Widget 树,所以亚秒级。代价是:改了全局变量初始化、改了 main()、改了枚举,热重载可能不灵,这时按 R 热重启即可。
工程目录速览:别被一堆文件夹吓到
flutter create 生成的工程里,你日常只动 lib/。其余是各平台的原生壳和构建配置。
| 路径 | 干什么 |
|---|---|
lib/ | 你的 Dart 源码,90% 时间在这里。 |
android/ · ios/ | 各平台原生工程壳(第 5 章改这里接原生)。 |
pubspec.yaml | 依赖与资源配置,等价于 package.json。 |
test/ | 单元测试与 widget 测试。 |
Dart 语言:够写 Flutter 用的那部分
Dart 语法像 Java 和 JS 的混血,上手很快。Flutter 3 之后全面强制 Null Safety——这是新手最容易卡的地方,先讲透。这一章把"空安全、异步、泛型、扩展方法"这一组写 Flutter 天天要碰的东西讲清。
空安全:变量要么有值,要么明说"可能为空"
Null Safety 的核心一句话:编译期就逼你区分"一定有值"和"可能为空"。这把一大类 NullPointerException 式的崩溃扼杀在编译阶段。
① 滥用 ! 强解包:nickname! 等于对编译器喊"它肯定不为空",一旦真为空就崩。只在你 100% 确定时再用。② late 没赋值就访问:LateInitializationError,和空指针一样疼。late 适合"构造时还拿不到、但用前一定赋"的场景(如 initState 里取数据)。
final / const:Flutter 性能的第一道护城河
final 让引用不变,const 让对象在编译期就确定、可全局复用。在重绘频繁的移动端,它们不是"风格偏好",而是性能开关。
论为什么 Flutter 爱用 final / const
是什么:final 引用不可变;const 对象编译期确定,全局只存一份。
为什么重要:Flutter 重绘频率高(每秒几十次)。给不变化的 Widget 加 const 构造,框架能直接跳过重建、复用旧对象,这是移动端流畅度的最基础优化。反过来说,漏了 const 又没改的属性,会被反复 new 出来,GC 压力大、卡顿多。
集合与函数式操作:链式写法很顺
Dart 的集合 API 和 Kotlin/Swift 很像,where/map/fold 一套链式下来,很少需要手写 for 循环。
异步 Future 与 async/await:把回调拍平
Future 代表"将来某个时刻会有结果"。用 async 标记函数、await 等结果,写法就像同步代码,避免层层嵌套。
论为什么用 async/await 而不是 .then 链
.then().then().catchError() 写多了会向右缩进成金字塔,且错误传播不直观。async/await 把异步当成同步读,try/catch 照常抓异常,可读性和可维护性明显更好。UI 层调异步记得配合第 4 章的状态管理,别在 build 里直接 await。
Stream 与异步流:一堆 Future 的顺序到达
Future 是"一个结果",Stream 是"一串结果"——下载进度、WebSocket 消息、按钮连点都适合用 Stream。用 await for 或 listen 消费。
泛型与扩展方法:写通用、写顺手
泛型让组件"类型安全又通用"(比如 List<User>)。扩展方法(extension)可以给别人写的类"加方法"而不改源码——Flutter 生态大量用它扩充 Widget 能力。
类、mixin 与 sealed:组合优于继承
Dart 单继承,但用 mixin 做能力组合,用 sealed 做"封闭类层次"(配合模式匹配穷尽检查)。这是写干净 Flutter 代码的两个法宝。
Widget 与声明式 UI:一切皆控件
Flutter 里按钮、文字、布局、间距全是 Widget,它们组成一棵"控件树"。你写的不是"怎么改界面",而是"界面在当前状态下应该长什么样"——这就是声明式 UI。这一章把"心智模型、三棵树、Stateless/Stateful、布局、渲染"讲透。
声明式 UI 的心智模型:你描述的是"状态→界面"
命令式(传统原生)是你拿着"View 对象"手动改它的文本、颜色。声明式是你写一个函数 UI = f(state):状态变了,框架把整棵 UI 按新状态重画一遍。你不关心"怎么改",只声明"现在该什么样"。
论声明式为什么更不容易出 bug
命令式最怕"界面和状态对不上"——比如改了数据却忘了刷新某个 View,界面就 stale 了。声明式下,状态是唯一真相,UI 永远由状态推导,不会出现"数据改了界面没改"。代价是你要习惯"状态驱动一切"的写法,而不是到处拿引用去 setText。
三棵树:Widget / Element / RenderObject
新手常问"Widget 难道每次都重新创建?性能不炸?"答案是 Flutter 内部有三棵树,重绘没你想的那么贵。
| 树 | 干什么 |
|---|---|
| Widget 树 | 你写的配置(轻量、 immutable)。每次 build 都重建,很便宜。 |
| Element 树 | Widget 的实例,长期存在。负责"diff"新旧 Widget,决定哪些要更新。 |
| RenderObject 树 | 真正负责布局与绘制的对象,最重。尽量不被重建。 |
论为什么"Widget 随便 new"不慢
Widget 是廉价配置,每帧重建它几乎零成本;真正贵的是 RenderObject。Element 树做 diff:类型/key 相同的 Widget 复用旧 Element 和 RenderObject,只更新必要属性。所以放心写声明式,但记得给不变的子项加 const,让 Element 直接跳过 diff。
Stateless 与 Stateful:什么时候需要"状态"
无状态控件(StatelessWidget)纯靠入参;有状态控件(StatefulWidget)把"会变的部分"放进 State,调用 setState 通知框架重绘。
State 是"这个控件自己的临时状态"。如果状态要在多个页面共享(登录用户、购物车),不该塞进某个 Widget 的 State,否则父控件一重建它就丢。这种情况请跳到第 4 章用状态管理方案。
常用布局控件:从 div 思维切过来
如果你做过 Web,下面这张类比能让你秒懂 Flutter 的布局控件。
| 控件 | 作用 | 类比 |
|---|---|---|
Container | 盒子:尺寸/边距/背景/圆角 | HTML 的 div + CSS |
Row / Column | 横向 / 纵向排列子项 | flex 行/列 |
Stack | 层叠定位 | 绝对定位 |
ListView | 可滚动列表(务必用 builder 复用) | 虚拟滚动列表 |
Scaffold | 页面骨架:顶栏/底栏/抽屉/浮钮 | App 框架 |
Expanded | 占满剩余空间(配合 Row/Column) | flex: 1 |
滚动列表与 ListView.builder:上千条也不卡
新手最容易犯的错:把 1000 条数据直接 children: [ ... ] 堆进去,结果一次性创建 1000 个 Widget,内存炸、首屏慢。ListView.builder 只构建"屏幕上可见的",滑到哪建到哪。
列表项有状态(比如展开/选中)时,不给 Key 会让 Flutter 在增删项时复用错对象,出现"明明删了第 3 个,第 5 个的状态却没了"。用 ValueKey(业务id) 或 UniqueKey() 稳住身份。
渲染原理一句话版:布局→绘制→合成
你不必背细节,但要懂大致流程,调性能时才知往哪使劲。
论一帧是怎么画出来的
① Build:你的 build 产出 Widget 树。② Layout:每个 RenderObject 算自己的尺寸与位置(约束向下、尺寸向上)。③ Paint:把像素画到图层。④ Composite:图层合成送 GPU。任何一阶段超 16.6ms(60fps)就掉帧。第 6 章的 DevTools 就是帮你看哪一阶段慢。
状态管理:小项目靠 setState,大项目要分层
按钮内部计数用 setState 就够了。一旦状态要在多个页面共享(登录用户、购物车、主题),就得上状态管理方案——否则数据会在控件树里层层传递,又乱又难改。这一章对比主流方案并给出选型。
状态管理的动机:别让数据在树里"旅行"
没有状态管理时,要跨三层传一个值,得一层层当参数往下塞(prop drilling)。改一下结构就全崩。状态管理让"全局可观察的状态"被任意组件读取,不用层层传递。
论状态的"属地原则"
核心判断只有一句:状态离使用它的控件越近越好,跨页面的才提升到全局。一个按钮的"按了几次"就该待在它自己的 State 里;而"当前登录用户"天然属于全局。别一上来就把所有状态丢进全局 Store,那是另一种过度设计。
setState:局部状态的第一选择
只要状态只影响当前控件,setState 是最简单正确的方案,别为了"架构感"硬上重型框架。
Provider:官方入门级,依赖注入式
Provider 把"数据对象"放在树上某层,下方任意组件用 context.read/context.watch 取。新手友好,但大型项目里类型安全和可测性略弱。
Riverpod:Provider 的进化版,更稳更可测
Riverpod 解决了 Provider 的痛点:不依赖 BuildContext、编译期检查依赖、易写单测。中大型新项目我更推荐它。
论为什么我更推荐 Riverpod 起步
Provider 的坑在于:provider 没挂上去就 watch 会运行时报错;依赖关系靠字符串/类型隐式。Riverpod 把这些都搬到了编译期——漏挂、循环依赖、类型错配都在编译阶段炸,而不是用户手里崩。新项目直接上 Riverpod,省后面返工。
Bloc:事件→状态单向流,强约束适合大团队
Bloc 把交互建模成"事件进、状态出"的纯函数流水线,可预测、可回溯、好测试,但样板代码最多。复杂业务或多人协作时它的强约束是优点。
选型对比:没有最好,只有最合适
| 方案 | 风格 | 何时选 | 代价 |
|---|---|---|---|
setState | 内置,控件内自管 | 局部、一次性状态 | 跨组件难共享 |
Provider | 官方入门级,依赖注入式 | 中小项目,易上手 | 大型可测性弱 |
Riverpod | Provider 进化版,编译期安全 | 中大型新项目首选 | 概念稍多 |
Bloc | 事件→状态 单向流,强约束 | 复杂业务、团队协作 | 样板多 |
① 全用 setState:状态一多就 prop drilling 到崩溃。② 全用全局 Store:一个计数器都丢进全局,重构时牵一发动全身。记住属地原则:能局部的别全局,要共享的才提升。
平台通道:当 Flutter 不够用时落回原生
Flutter 覆盖 90% 的 UI 与业务逻辑,但调用原生能力(摄像头底层、蓝牙、特定 SDK、系统级权限、后台服务)还得走"平台通道"——Dart 发指令,原生代码执行后回传结果。这一章把 MethodChannel、EventChannel、以及高频调用的坑讲清,并给出 Android(Kotlin)/iOS(Swift) 两端接收端写法。
什么时候必须落回原生
先划清边界,能不写原生就不写——原生代码维护成本翻倍(两套语言、两套调试)。
| Flutter 直接能搞定 | 必须走平台通道 |
|---|---|
| UI、动画、列表、网络请求 | 系统级权限、后台服务 |
| SQLite / 文件 / 大部分插件 | 厂商私有 SDK(支付/推送) |
| 常见传感器(camera 插件等) | 蓝牙 LE 精细控制、USB 外设 |
| 本地通知(靠插件) | 需要极致性能的 Native 库 |
MethodChannel:Dart 调 Android(Kotlin)
MethodChannel 是"一次性请求-响应"通道:Dart 发一个方法名 + 参数,原生执行后回传一个结果。两端用同一个 channel 名对上暗号。
MethodChannel:Dart 调 iOS(Swift)
同一套语义,iOS 端用 Swift 在 AppDelegate / FlutterAppDelegate 里注册。注意 iOS 回传用 result(),异常用 result.error。
EventChannel:持续事件流,而非一次性请求
电池变化、传感器、下载进度这类"持续推送"的数据,用 EventChannel——原生端开一个流,Dart 端用 receiveBroadcastStream 监听。
论MethodChannel 还是 EventChannel?
一句话:要一个结果用 MethodChannel(问一次答一次);要一串结果用 EventChannel(订阅后持续收)。别用 MethodChannel 去轮询"每秒问一次电量",那既浪费又慢,应改成 EventChannel 让原生主动推。
高频调用注意:通道是异步序列化的,别滥用
通道走的是二进制信使协议,每次调用都要序列化参数、跨线程、再反序列化。在热路径里逐帧来回调,性能会塌。
① 别逐帧传大对象:比如每帧把摄像头图像从原生传回 Dart,序列化直接卡死。应在原生侧处理好,只回传必要的小结果(坐标、状态)。② 别在 UI 主线程做重活:原生侧接收端可能在主线程,重计算会卡原生 UI。③ 大批量数据用大数据通道:超大数组考虑 BasicMessageChannel 或直接在原生渲染。
插件开发基础:把原生能力封装成 pub 包
如果你这套原生能力要在多个项目复用,或分享给社区,就做成 Flutter 插件(flutter create --template=plugin)。平台相关代码放在各原生目录,Dart 侧提供统一 API。
打包发布与性能:从"能跑"到"能上架"
本地能跑只是第一步。这一章覆盖 Android 签名构建、iOS 分发、性能四板斧、动画与自定义绘制、离线存储、以及用 DevTools 实证性能——把"能跑的小 demo"变成"能上架、不卡、离线也能用"的产品。
Android 签名与构建:上架的第一道关卡
Android 要求每个发布包用密钥签名,否则无法安装/更新。keytool 生成一次密钥库,之后构建时引用它。Google Play 现在推 App Bundle(AAB),比 APK 更小更智能。
Android 用签名密钥标识应用身份。上传密钥一旦丢失,你将无法向 Google Play 推送更新(只能新建应用,老用户无法平滑升级)。把 upload-keystore.jks 和口令离线备份(密码管理器/冷存储),比代码还重要。
iOS 构建与分发:证书体系绕不开
iOS 比 Android 更严:调试和上架都要证书 + 描述文件。日常开发用开发证书,上架用分发证书,TestFlight/App Store 走不同通道。
论为什么 iOS 签名这么烦
苹果用"证书 + 描述文件 + Bundle ID + 设备 UDID"四件套,确保只有被你授权的代码能在被你授权的设备上跑。这是它生态封闭、但 malware 少的核心机制。接受它、用 Xcode 自动签名(Automatically manage signing)能省掉八成手动配置。
性能四板斧:移动端卡顿最常见的四个原因
卡顿不要"凭感觉优化"。下面四个是 Flutter 卡顿的常客,按频率排序。
论移动端卡顿最常见的四个原因
① 没加 const:本可复用的 Widget 被反复重建,diff 成本累积。
② 长列表用错控件:用 ListView.builder 而非直接堆子项,否则上千条直接 OOM/首屏慢。
③ 主线程干重活:解析大 JSON、算大图放到 compute 或 isolate 里跑,别堵 UI 线程。
④ 不看数据凭感觉:用 DevTools 的 Performance / Memory 面板看帧率和内存,比猜靠谱。
动画与自定义绘制:CustomPaint 自己画图
Flutter 自带丰富的隐式/显式动画(AnimatedContainer、AnimationController)。需要画图表、波形等独特图形时,用 CustomPaint + CustomPainter 拿到 Canvas 自己画。
离线存储:Hive 与 SQLite(sqflite)
没网络的场景(地铁、飞机)也要能用,就需要本地存储。简单键值用 Hive(纯 Dart、快、免原生),结构化查询用 sqflite(SQLite)。
| 方案 | 适合 | 代价 |
|---|---|---|
shared_preferences | 极简键值(开关/配置) | 不宜存大量/结构化数据 |
Hive | 本地对象、快、无原生依赖 | 复杂关联查询弱 |
sqflite | 结构化、可 SQL 查询 | 要写 SQL、略重 |
用 DevTools 实证性能:别猜,看帧
Flutter DevTools 是浏览器里的调试面板。Performance 视图能看每一帧的 UI/GPU 耗时,红色条就是掉帧;Memory 视图看对象分配与泄漏。
论优化要先有基线
和 Java 调优一样:先量再改。在 DevTools 里跑一遍关键路径,找到"哪一帧超 16.6ms、花在 Layout 还是 Paint",再针对性加 const、改 builder、搬 isolate。没有基线就改,等于蒙眼调参。
持续集成与自动发布:把"手动上架"变成流水线
本地手点 Xcode/命令行打包,既慢又容易漏步骤。把构建、测试、签名、上传做成 CI 流水线,每次 push 自动跑,才是生产级做法。移动端常用 fastlane(脚本化打包上传)或 Codemagic(云端托管)。
论为什么值得上 CI
可重复、可审计、省人力。本地打包依赖于"某人电脑上对不对",CI 用干净环境跑,证书与密钥走环境变量注入(别硬编码进仓库)。新人 clone 下来,fastlane beta 就能复现整套发布,不用把老手的"手工秘籍"记在脑子里。
Flutter 用"自绘 + AOT 编译"做到跨端一致且流畅:三张牌是自绘引擎、Dart 编译到原生、热重载。Dart 的空安全把空指针扼杀在编译期,final/const 是性能根基;异步用 Future/Stream + async/await,扩展方法与 mixin 让代码更组合化。UI 走声明式控件树,内部三棵树(Widget/Element/RenderObject)保证"随便 new Widget"也很廉价;Stateless/Stateful 按是否有状态区分,布局用 Row/Column/Stack,列表务必 ListView.builder 并给 key。状态管理遵循"属地原则":局部用 setState,跨页共享用 Provider/Riverpod/Bloc,Bloc 强约束适合大团队。原生能力通过 MethodChannel(一次性)/ EventChannel(持续流)桥接,但高频/大对象调用要谨慎、尽量在原生侧处理好再回传。发布靠签名构建(Android keystore 务必备份、iOS 证书体系),性能靠 const、builder、isolate、DevTools 实证而非感觉;离线存储按复杂度选 shared_preferences / Hive / sqflite。
1.Flutter 和 React Native 最大的实现路线区别是什么?各有什么取舍?
查看答案
Flutter 自绘控件(不依赖系统控件),RN 通过桥接调用原生控件。Flutter 跨端一致性更好、性能更稳,但包体略大、离系统能力更远;RN 复用原生控件更"原生感",但桥接有性能损耗、两端易不一致。
2.为什么 Flutter 强烈建议给不变的 Widget 加 const?不加会怎样?
查看答案
重绘时框架会跳过 const Widget 的重建直接复用 Element/RenderObject,减少对象分配与 diff 成本,是移动端流畅度的关键优化。漏加 const 会让本可复用的对象反复 new,GC 压力与掉帧风险上升。
3.MethodChannel 和 EventChannel 分别该怎么选?
查看答案
要"问一次答一次"的单个结果用 MethodChannel;要"订阅后持续收一串事件"(电量变化、传感器、进度)用 EventChannel。别用 MethodChannel 轮询持续数据。
4.平台通道高频/逐帧传大对象为什么危险?正确做法是什么?
查看答案
通道走二进制信使协议,每次调用都要跨线程序列化/反序列化;逐帧传图像或大数组会直接卡死 UI。正确做法:在原生侧处理好,只回传必要的小结果(状态/坐标);超大批量考虑 BasicMessageChannel 或在原生直接渲染。
5.Android 上传密钥(keystore)丢失会有什么后果?为什么它比代码还重要?
查看答案
Android 用签名密钥标识应用身份,上传密钥丢失后将无法向 Google Play 推送更新(只能新建应用,老用户无法平滑升级)。因此 keystore 与口令必须离线备份,优先级高于源码。
下一步往哪走
路学完 Flutter 之后
① 做真实 App:结合你手上的 MJNexus-Reader 这类项目,把"阅读列表 + 本地模型调用 + 离线缓存"落地成一个能装的包。
② 补原生:Android 侧熟练 Kotlin(本章第 5 章已接 MethodChannel),iOS 侧看下一页 Swift,能独立写两端的通道接收端。
③ 进阶:动画与手势深入、国际化(i18n)、平台样式适配(Cupertino vs Material)、自动化测试(widget test)与 CI 发布流水线(fastlane / Codemagic)。