本页定位 · Flutter / Android

移动端是产品的第一现场。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。

1

为什么选 Flutter,以及把环境搭起来

Why Flutter · SDK Setup · flutter doctor

跨端方案不少: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 示例)

# 编辑 ~/.zshrc,把解压目录的 bin 加进去 export PATH="$PATH:$HOME/flutter/bin" source ~/.zshrc flutter --version # 能打印版本就说明 PATH 生效

flutter doctor 与新手第一坑

flutter doctor 的输出里,绿色对勾是通过,红色叉是有问题。它列的是"缺什么"而不是"你错了",逐条补上即可。

# 常见两个红叉的快速解法 flutter doctor --android-licenses # 同意 Android SDK 许可(最常漏) flutter emulators --launch Pixel_5 # 起一个 Android 模拟器 flutter devices # 确认有可用设备/模拟器
新手最常犯的 doctor 慌

① 一堆红叉就慌:按提示逐条装,别跳过。尤其容易漏 Android 许可证和"没连设备"。② 装了 Xcode 却没跑 license:会卡在 iOS 工具链,记得 sudo xcodebuild -license accept。③ PATH 没生效:新开终端才认得到 flutter 命令,老终端要重开或 source。

第一个工程与热重载:毫秒级看到改动

新建工程后,flutter run 连上设备即启动。开发时改完代码,按 r(热重载)或 R(热重启),界面立刻变,而且当前页面状态还在——比如你填了一半的表单不会丢。

$ flutter create my_app # 生成标准工程骨架 $ cd my_app $ flutter run # 连上设备/模拟器后启动,支持热重载 # 运行中:按 r 热重载(保留状态)|按 R 热重启(清空状态) # 按 q 退出;按 p 切换显示网格/性能图层

论热重载为什么这么爽

传统原生改个布局要重新编译安装,动辄几十秒到几分钟。Flutter 把改动的代码注入正在运行的 Dart VM,只重建受影响的 Widget 树,所以亚秒级。代价是:改了全局变量初始化、改了 main()、改了枚举,热重载可能不灵,这时按 R 热重启即可。

工程目录速览:别被一堆文件夹吓到

flutter create 生成的工程里,你日常只动 lib/。其余是各平台的原生壳和构建配置。

路径干什么
lib/你的 Dart 源码,90% 时间在这里。
android/ · ios/各平台原生工程壳(第 5 章改这里接原生)。
pubspec.yaml依赖与资源配置,等价于 package.json。
test/单元测试与 widget 测试。
2

Dart 语言:够写 Flutter 用的那部分

Dart Core · Null Safety · Async · Generics

Dart 语法像 Java 和 JS 的混血,上手很快。Flutter 3 之后全面强制 Null Safety——这是新手最容易卡的地方,先讲透。这一章把"空安全、异步、泛型、扩展方法"这一组写 Flutter 天天要碰的东西讲清。

空安全:变量要么有值,要么明说"可能为空"

Null Safety 的核心一句话:编译期就逼你区分"一定有值"和"可能为空"。这把一大类 NullPointerException 式的崩溃扼杀在编译阶段。

String name = 'MJ'; // 非空:编译期保证一定有值 String? nickname; // 可空:允许为 null,用 ? 标记 int len = nickname?.length ?? 0; // ?. 防空调用,?? 给默认值 late String token; // late:先声明,稍后赋值(保证用前已赋) final config = load(); // final:引用不可变(值可变看类型) const pi = 3.14159; // const:编译期常量,可放进常量池复用
新手最爱的两个 null 雷

① 滥用 ! 强解包:nickname! 等于对编译器喊"它肯定不为空",一旦真为空就崩。只在你 100% 确定时再用。② late 没赋值就访问:LateInitializationError,和空指针一样疼。late 适合"构造时还拿不到、但用前一定赋"的场景(如 initState 里取数据)。

final / const:Flutter 性能的第一道护城河

final 让引用不变,const 让对象在编译期就确定、可全局复用。在重绘频繁的移动端,它们不是"风格偏好",而是性能开关。

论为什么 Flutter 爱用 final / const

是什么:final 引用不可变;const 对象编译期确定,全局只存一份。

为什么重要:Flutter 重绘频率高(每秒几十次)。给不变化的 Widget 加 const 构造,框架能直接跳过重建、复用旧对象,这是移动端流畅度的最基础优化。反过来说,漏了 const 又没改的属性,会被反复 new 出来,GC 压力大、卡顿多。

// 这两个 Text 在重绘时会被复用,因为 const 构造 const Text('标题'); const SizedBox(height: 8); // 固定间距也该 const

集合与函数式操作:链式写法很顺

Dart 的集合 API 和 Kotlin/Swift 很像,where/map/fold 一套链式下来,很少需要手写 for 循环。

var list = ['a', 'b', 'cc']; var map = {'k': 1, 'j': 2}; // 链式:过滤长度>1,再转大写 list.where((e) => e.length > 1) .map((e) => e.toUpperCase()) .forEach(print); // 输出 CC // 字符串插值用 $ 或 ${} var msg = '共 ${list.length} 项';

异步 Future 与 async/await:把回调拍平

Future 代表"将来某个时刻会有结果"。用 async 标记函数、await 等结果,写法就像同步代码,避免层层嵌套。

Future<String> fetch() async { // 异步返回 Future final resp = await http.get(url); // await 等结果,写法像同步 return resp.body; } // 出错怎么办:try / catch 包住 await Future<String> safeFetch() async { try { return await fetch(); } catch (e) { return 'fallback'; // 给兜底,别让异常飞到 UI 线程 } }

论为什么用 async/await 而不是 .then 链

.then().then().catchError() 写多了会向右缩进成金字塔,且错误传播不直观。async/await 把异步当成同步读,try/catch 照常抓异常,可读性和可维护性明显更好。UI 层调异步记得配合第 4 章的状态管理,别在 build 里直接 await。

Stream 与异步流:一堆 Future 的顺序到达

Future 是"一个结果",Stream 是"一串结果"——下载进度、WebSocket 消息、按钮连点都适合用 Stream。用 await for 或 listen 消费。

// 每秒吐一个数字,共 3 个 Stream<int> countStream() async* { // async* 生成器 for (var i = 1; i <= 3; i++) { await Future.delayed(const Duration(seconds: 1)); yield i; // 一个一个往外吐 } } await for (final n in countStream()) { print(n); // 1 然后 2 然后 3 }

泛型与扩展方法:写通用、写顺手

泛型让组件"类型安全又通用"(比如 List<User>)。扩展方法(extension)可以给别人写的类"加方法"而不改源码——Flutter 生态大量用它扩充 Widget 能力。

// 扩展方法:给 int 加个"重复执行"的能力 extension Repeat on int { void times(void fn(int i)) { for (var i = 0; i < this; i++) fn(i); } } 3.times((i) => print(i)); // 打印 0 1 2 // 泛型函数:谁都能用 T first<T>(List<T> list) => list.first;

类、mixin 与 sealed:组合优于继承

Dart 单继承,但用 mixin 做能力组合,用 sealed 做"封闭类层次"(配合模式匹配穷尽检查)。这是写干净 Flutter 代码的两个法宝。

mixin Loggable { // 可插拔的日志能力 void log(String m) => print('[log] $m'); } class Service with Loggable { } // 混入,等于"拥有"日志能力 // sealed:所有子类都在本文件,switch 时编译器逼你覆盖全部分支 sealed class Result {} class Ok extends Result { final data; Ok(this.data); } class Err extends Result { final msg; Err(this.msg); }
3

Widget 与声明式 UI:一切皆控件

Widget Tree · Stateless / Stateful · Render

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 通知框架重绘。

class Hello extends StatelessWidget { final String name; const Hello(this.name, {super.key}); @override Widget build(BuildContext ctx) => Text('你好 $name'); // 无状态:纯靠入参 } class Counter extends StatefulWidget { const Counter({super.key}); @override State<Counter> createState() => _CounterState(); } class _CounterState extends State<Counter> { int n = 0; @override Widget build(BuildContext ctx) => ElevatedButton( onPressed: () => setState(() => n++), // setState 触发重绘 child: Text('点了 $n 次'), ); }
State 里塞了太多东西

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 只构建"屏幕上可见的",滑到哪建到哪。

// 危险写法:1000 个全建 ListView(children: items.map((e) => Item(e)).toList()); // 正确写法:按需构建 + 抽 key ListView.builder( itemCount: items.length, itemBuilder: (ctx, i) => Item( key: ValueKey(items[i].id), // 稳定 key,避免复用错位 items[i], ), );
列表里忘了给 key

列表项有状态(比如展开/选中)时,不给 Key 会让 Flutter 在增删项时复用错对象,出现"明明删了第 3 个,第 5 个的状态却没了"。用 ValueKey(业务id) 或 UniqueKey() 稳住身份。

渲染原理一句话版:布局→绘制→合成

你不必背细节,但要懂大致流程,调性能时才知往哪使劲。

论一帧是怎么画出来的

① Build:你的 build 产出 Widget 树。② Layout:每个 RenderObject 算自己的尺寸与位置(约束向下、尺寸向上)。③ Paint:把像素画到图层。④ Composite:图层合成送 GPU。任何一阶段超 16.6ms(60fps)就掉帧。第 6 章的 DevTools 就是帮你看哪一阶段慢。

4

状态管理:小项目靠 setState,大项目要分层

State Management · Provider / Riverpod / Bloc

按钮内部计数用 setState 就够了。一旦状态要在多个页面共享(登录用户、购物车、主题),就得上状态管理方案——否则数据会在控件树里层层传递,又乱又难改。这一章对比主流方案并给出选型。

状态管理的动机:别让数据在树里"旅行"

没有状态管理时,要跨三层传一个值,得一层层当参数往下塞(prop drilling)。改一下结构就全崩。状态管理让"全局可观察的状态"被任意组件读取,不用层层传递。

论状态的"属地原则"

核心判断只有一句:状态离使用它的控件越近越好,跨页面的才提升到全局。一个按钮的"按了几次"就该待在它自己的 State 里;而"当前登录用户"天然属于全局。别一上来就把所有状态丢进全局 Store,那是另一种过度设计。

setState:局部状态的第一选择

只要状态只影响当前控件,setState 是最简单正确的方案,别为了"架构感"硬上重型框架。

class Toggle extends StatefulWidget { const Toggle({super.key}); @override State<Toggle> createState() => _ToggleState(); } class _ToggleState extends State<Toggle> { bool on = false; @override Widget build(BuildContext c) => Switch( value: on, onChanged: (v) => setState(() => on = v), // 只管自己,setState 足矣 ); }

Provider:官方入门级,依赖注入式

Provider 把"数据对象"放在树上某层,下方任意组件用 context.read/context.watch 取。新手友好,但大型项目里类型安全和可测性略弱。

// 在顶层提供(provide)一个 Counter 对象 ChangeNotifierProvider( create: (_) => Counter(), child: MyApp(), ); // 子组件读取并响应变化 final counter = context.watch<Counter>(); Text('${counter.n}');

Riverpod:Provider 的进化版,更稳更可测

Riverpod 解决了 Provider 的痛点:不依赖 BuildContext、编译期检查依赖、易写单测。中大型新项目我更推荐它。

// 用 @riverpod 注解(或手写 Provider)声明一个全局可观察状态 final counterProvider = StateProvider<int>((ref) => 0); // 组件里消费 final n = ref.watch(counterProvider); ElevatedButton( onPressed: () => ref.read(counterProvider.notifier).state++, child: Text('$n'), );

论为什么我更推荐 Riverpod 起步

Provider 的坑在于:provider 没挂上去就 watch 会运行时报错;依赖关系靠字符串/类型隐式。Riverpod 把这些都搬到了编译期——漏挂、循环依赖、类型错配都在编译阶段炸,而不是用户手里崩。新项目直接上 Riverpod,省后面返工。

Bloc:事件→状态单向流,强约束适合大团队

Bloc 把交互建模成"事件进、状态出"的纯函数流水线,可预测、可回溯、好测试,但样板代码最多。复杂业务或多人协作时它的强约束是优点。

// 事件与状态分明,逻辑写在 mapEventToState sealed class CounterEvent {} class Increment extends CounterEvent {} class CounterBloc extends Bloc<CounterEvent, int> { CounterBloc() : super(0) { on<Increment>((e, emit) => emit(state + 1)); } } // UI 里:context.read<CounterBloc>().add(Increment());

选型对比:没有最好,只有最合适

方案风格何时选代价
setState内置,控件内自管局部、一次性状态跨组件难共享
Provider官方入门级,依赖注入式中小项目,易上手大型可测性弱
RiverpodProvider 进化版,编译期安全中大型新项目首选概念稍多
Bloc事件→状态 单向流,强约束复杂业务、团队协作样板多
选型的两个极端

① 全用 setState:状态一多就 prop drilling 到崩溃。② 全用全局 Store:一个计数器都丢进全局,重构时牵一发动全身。记住属地原则:能局部的别全局,要共享的才提升。

5

平台通道:当 Flutter 不够用时落回原生

Platform Channels · Android (Kotlin) / iOS (Swift)

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 名对上暗号。

// Dart 侧:调一个"获取电池电量"的原生方法 const channel = MethodChannel('samples.mj/battery'); final level = await channel.invokeMethod<int>('getBattery');
// Android 侧(Kotlin):在 MainActivity 里接住这个调用 override fun configureFlutterEngine(flutterEngine: FlutterEngine) { MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "samples.mj/battery").setMethodCallHandler { call, result -> if (call.method == "getBattery") { result.success(getBatteryLevel()) // 调 Android API,回传 Int } else result.notImplemented() } }

MethodChannel:Dart 调 iOS(Swift)

同一套语义,iOS 端用 Swift 在 AppDelegate / FlutterAppDelegate 里注册。注意 iOS 回传用 result(),异常用 result.error。

// iOS 侧(Swift):在 AppDelegate 注册同名 channel import Flutter override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { let controller = window?.rootViewController as! FlutterViewController let channel = FlutterMethodChannel( name: "samples.mj/battery", binaryMessenger: controller.binaryMessenger) channel.setMethodCallHandler { call, result in if call.method == "getBattery" { result(getBatteryLevel()) // 回传 Int } else { result(FlutterMethodNotImplemented) } } return true }

EventChannel:持续事件流,而非一次性请求

电池变化、传感器、下载进度这类"持续推送"的数据,用 EventChannel——原生端开一个流,Dart 端用 receiveBroadcastStream 监听。

// Dart 侧监听原生持续推送的事件 const stream = EventChannel('samples.mj/sensor'); stream.receiveBroadcastStream().listen( (event) => print('收到: $event'), onError: (e) => print('出错: $e'), );

论MethodChannel 还是 EventChannel?

一句话:要一个结果用 MethodChannel(问一次答一次);要一串结果用 EventChannel(订阅后持续收)。别用 MethodChannel 去轮询"每秒问一次电量",那既浪费又慢,应改成 EventChannel 让原生主动推。

高频调用注意:通道是异步序列化的,别滥用

通道走的是二进制信使协议,每次调用都要序列化参数、跨线程、再反序列化。在热路径里逐帧来回调,性能会塌。

平台通道的代价

① 别逐帧传大对象:比如每帧把摄像头图像从原生传回 Dart,序列化直接卡死。应在原生侧处理好,只回传必要的小结果(坐标、状态)。② 别在 UI 主线程做重活:原生侧接收端可能在主线程,重计算会卡原生 UI。③ 大批量数据用大数据通道:超大数组考虑 BasicMessageChannel 或直接在原生渲染。

插件开发基础:把原生能力封装成 pub 包

如果你这套原生能力要在多个项目复用,或分享给社区,就做成 Flutter 插件(flutter create --template=plugin)。平台相关代码放在各原生目录,Dart 侧提供统一 API。

# 生成一个带平台通道骨架的插件 flutter create --template=plugin --platforms=android,ios mj_battery # 结构里:lib/ 放 Dart API,android/src、ios/Classes 放原生实现
6

打包发布与性能:从"能跑"到"能上架"

Build & Release · Performance · Offline

本地能跑只是第一步。这一章覆盖 Android 签名构建、iOS 分发、性能四板斧、动画与自定义绘制、离线存储、以及用 DevTools 实证性能——把"能跑的小 demo"变成"能上架、不卡、离线也能用"的产品。

Android 签名与构建:上架的第一道关卡

Android 要求每个发布包用密钥签名,否则无法安装/更新。keytool 生成一次密钥库,之后构建时引用它。Google Play 现在推 App Bundle(AAB),比 APK 更小更智能。

# 生成签名密钥(仅一次,务必备份 keystore!) keytool -genkey -v -keystore upload-keystore.jks -alias upload \ -keyalg RSA -keysize 2048 -validity 10000 # 在 android/key.properties 里配置路径与别名(别提交进 git) # 构建发布包 flutter build appbundle # 上架 Google Play 用 AAB flutter build apk --split-per-abi # 或分架构 APK 直装
密钥丢了 = 这个应用身份丢了

Android 用签名密钥标识应用身份。上传密钥一旦丢失,你将无法向 Google Play 推送更新(只能新建应用,老用户无法平滑升级)。把 upload-keystore.jks 和口令离线备份(密码管理器/冷存储),比代码还重要。

iOS 构建与分发:证书体系绕不开

iOS 比 Android 更严:调试和上架都要证书 + 描述文件。日常开发用开发证书,上架用分发证书,TestFlight/App Store 走不同通道。

# 命令行构建 iOS 发布包(先 archive) flutter build ios --release # 产出 Runner.app # 再用 Xcode Organizer 或 xcodebuild 归档上传 App Store / TestFlight xcodebuild -workspace ios/Runner.xcworkspace \ -scheme Runner -archivePath build/Runner.xcarchive archive

论为什么 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 自己画。

// 自定义绘制:画一个圆 class CirclePainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint()..color = Colors.blue; canvas.drawCircle(size.center(Offset.zero), 40, paint); } @override bool shouldRepaint(covariant CustomPainter old) => false; } // 使用:CustomPaint(painter: CirclePainter())

离线存储:Hive 与 SQLite(sqflite)

没网络的场景(地铁、飞机)也要能用,就需要本地存储。简单键值用 Hive(纯 Dart、快、免原生),结构化查询用 sqflite(SQLite)。

// Hive:轻量键值/对象存储,无需原生依赖 await Hive.initFlutter(); var box = await Hive.openBox('settings'); box.put('theme', 'dark'); final theme = box.get('theme'); // 同步读取,离线即用 // sqflite:需要 SQL 查询时 final db = await openDatabase('app.db'); await db.execute('CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)');
方案适合代价
shared_preferences极简键值(开关/配置)不宜存大量/结构化数据
Hive本地对象、快、无原生依赖复杂关联查询弱
sqflite结构化、可 SQL 查询要写 SQL、略重

用 DevTools 实证性能:别猜,看帧

Flutter DevTools 是浏览器里的调试面板。Performance 视图能看每一帧的 UI/GPU 耗时,红色条就是掉帧;Memory 视图看对象分配与泄漏。

# 启动 DevTools(会自动开浏览器) flutter pub global activate devtools flutter devtools # 或直接在 Android Studio / VS Code 里点 "Open DevTools" # 关键操作:Performance 里开 "Show performance overlay" 看实时 FPS

论优化要先有基线

和 Java 调优一样:先量再改。在 DevTools 里跑一遍关键路径,找到"哪一帧超 16.6ms、花在 Layout 还是 Paint",再针对性加 const、改 builder、搬 isolate。没有基线就改,等于蒙眼调参。

持续集成与自动发布:把"手动上架"变成流水线

本地手点 Xcode/命令行打包,既慢又容易漏步骤。把构建、测试、签名、上传做成 CI 流水线,每次 push 自动跑,才是生产级做法。移动端常用 fastlane(脚本化打包上传)或 Codemagic(云端托管)。

# fastlane 的 Fastfile 片段:一条命令出包并传 TestFlight lane :beta do flutter_build(app: "release") # 等价于 flutter build upload_to_testflight(skip_waiting_for_build_processing: true) end # CI 里:每次 push 跑 `fastlane beta`,自动构建+测试+上传

论为什么值得上 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)。