Flutter Started
创建项目flutter create —org com.gbuilderchina george_pick_mate
—org:是组织名称
组织名称是干什么的?
Section titled “组织名称是干什么的?”- 尽量保证全世界不撞车
商店里、系统里识别你的应用,不靠「应用显示名」,而靠这个 ID。用「反写域名」风格(如
com.公司.产品)是行业习惯:域名一般是你们控制的,所以和别人重复的概率低。 - 跟系统能力绑定 很多能力会按这个 ID 做校验或隔离,例如:应用签名、推送、部分 SDK 配置、深度链接、Android 里同签名应用升级 等。ID 一旦上线随便改,往往等于「另一个新应用」。
- 不是给用户看的“公司全称”
用户看到的是应用图标下的应用名称(可中文)。
--org生成的是技术标识,偏内部和平台侧。
和「公司组织」是什么关系?
Section titled “和「公司组织」是什么关系?”Flutter 用「org」这个词,只是叫法;你填的应是 你们能长期占用的标识,常见写法是:
- 有官网域名
groe.com→ 可能用com.groe或com.groe.pad这类风格; - 没有域名时也会用
com.项目名、dev.团队名等,但以后要统一、尽量别用com.example(那是示例占位)。
一句话:组织前缀 = 应用在 Android/iOS 上的“身份证号”的前半段,用来区分这是谁家的、哪个产品;不是 Flutter 里单独的一种「公司注册信息」字段。
第一章、flutter 渲染机制
Section titled “第一章、flutter 渲染机制”这“三棵树”协同工作的机制是其性能强大的核心原因。到 2026 年,虽然底层的渲染引擎演进到了 Impeller,但其三树联动逻辑依然遵循以下经典架构
1.1、三棵树的角色定位
Section titled “1.1、三棵树的角色定位”| 树的名称 | 官方名称 | 俗称 / 比喻 | 核心职责 |
|---|---|---|---|
| Widget Tree | Widget Tree | 配置树(层) / 说明书 | 只读的轻量快照。定义 UI 的结构、配置和参数(如:颜色、文字内容)。它是不可变的(Immutable),销毁和创建成本极低。 |
| Element Tree | Element Tree | 管理树(层) / 粘合剂 | 逻辑节点。负责维护 Widget 和 RenderObject 之间的引用,管理状态(State)的生命周期。它是 Widget 实例化后的体现。 |
| RenderObject Tree | RenderObject Tree | 渲染树(层) / 施工队 | 实际绘制节点。负责具体的布局(Layout)计算、尺寸测量(Size)和绘制(Paint)。它是最重的一个对象。 |
Element Tree 会记录一些位置的引用,不会重新创建会修改应用指向最新的快照,会通过widget tree的改变,来知道是否有新的配置可用,可用的话会传递给
render对象,这样就会重新渲染
build方法执行只代表 widget 这部分快照被重建了
1.2、运行机制与时机(生命周期)
Section titled “1.2、运行机制与时机(生命周期)”这三棵树并不是同时瞬间生成的,而是通过一个深度优先遍历的过程逐一挂载:
1.2.1、第一阶段:初始化(挂载)
Section titled “1.2.1、第一阶段:初始化(挂载)”- Widget 生成:当你调用
runApp(MyApp())时,Flutter 首先根据你的代码构建Widget Tree。 - Element 挂载:Flutter 调用 Widget 的
createElement()方法,在Element Tree中创建一个对应的 Element 节点。 - RenderObject 生成:Element 挂载后,会调用 Widget 的
createRenderObject()方法,在RenderObject Tree中生成对应的节点。- 注意:并不是所有的 Widget 都会生成 RenderObject(例如
StatelessWidget无状态组件只是配置组合),只有继承自RenderObjectWidget的组件才会生成物理渲染节点。
- 注意:并不是所有的 Widget 都会生成 RenderObject(例如
非渲染型 Widget (如 StatelessWidget, StatefulWidget)
这类 Widget 就像是一个**“包装盒”或者“组合指令”**。
- 它们渲染成什么? 它们在
RenderObject Tree中完全没有对应节点。 - 它们的作用: 它们唯一的任务就是通过
build()方法返回另一个(或一组)Widget。 - 运行逻辑:
StatelessWidget会生成一个ComponentElement。- 这个 Element 的工作是:调用你的
build方法,拿到里面的子 Widget。 - 它不停地往下拆解,直到遇到一个真正的
RenderObjectWidget为止。
- 渲染型 Widget (如
SizedBox,Column,Stack,Padding)
这些 Widget 继承自 RenderObjectWidget(例如 LeafRenderObjectWidget 或 SingleChildRenderObjectWidget)。
- 它们渲染成什么? 它们会创建真正的
RenderObject节点(如RenderBox)。 - 它们的作用: 它们负责实际的尺寸计算、布局、偏移和在屏幕上涂色。
三棵树的实际映射关系:
| Widget Tree (逻辑结构) | Element Tree (管理结构) | RenderObject Tree (渲染实体) |
|---|---|---|
MyContainer (组合) | ComponentElement | (无对应节点,塌陷) |
Padding (渲染) | RenderObjectElement | RenderPadding |
Text (组合/代理) | StatelessElement | (无对应节点,塌陷) |
RichText (Text内部的渲染) | RenderObjectElement | RenderParagraph |
结果: 在最终的渲染树里,RenderPadding 的直接子节点就是 RenderParagraph。MyContainer 和 Text 就像消失了一样,它们只存在于“图纸”和“管理层”中。
1.2.2、第二阶段:更新(Diff 算法)
Section titled “1.2.2、第二阶段:更新(Diff 算法)”这是 Flutter 性能优化的精髓。当你调用 setState() 时:
- 标记为 Dirty:对应的 Element 会被标记为“脏”。
- 触发 Rebuild:在下一帧刷新时,Flutter 重新运行 Widget 的
build方法,生成一颗新的 Widget Tree。 - 核心对比(The Diff):
- Flutter 将新的 Widget 与 旧的 Element 指向的旧 Widget 进行对比。
- 判断条件:如果
runtimeType和key都相同,Element 就不动,只是更新它内部的引用,并通知RenderObject修改属性(如颜色变了)。 - 结果:这样做避免了重新创建昂贵的
RenderObject,只需修改其属性,从而实现极速刷新。
- 运行流程图解
假设你有一个简单的结构:Container -> Text
- Widget Tree:
Container对象 ->Text对象(这只是两块描述性的内存数据)。 - Element Tree:
StatelessElement->ComponentElement(它们持有对 Widget 的引用,“地理坐标”:定位自己在树中的位置)。 - RenderObject Tree:
RenderPadding->RenderParagraph(这里存着具体的像素坐标、字体度量数据,是真正干活的地方)。
为什么需要三棵树?(2026 年的视角)
如果只有一棵树,每次改一个字都要重新计算整个页面的布局和绘制,手机会非常烫。
- Widget 层解决了“方便开发”的问题:开发者只需声明 UI 长什么样,不需要手动操作 DOM。
- Element 层解决了“效率”的问题:它像一个缓存层,通过对比(Diff)找出最小变动范围,决定哪些需要重绘,哪些可以复用。
- **RenderObject **层解决了“性能”的问题:它通过局部布局(Relayout Boundary)和局部重绘(Repaint Boundary),确保只有发生变化的区域才消耗 GPU 资源。
总结
- Widget 是你写的源代码。
- Element 是 Flutter 的运行上下文。
- RenderObject 是屏幕上显示的像素。
你可以把 Widget 想象成房屋的设计图纸(可以随手画几十张),Element 是现场的项目经理,而 RenderObject 就是那座真实的房子。改图纸很快,但拆了房子重盖很慢,所以经理(Element)会尽量根据新图纸只改动房子里的零件,而不是重建。
1.2.3、深度优先的“边拆边建”流程
Section titled “1.2.3、深度优先的“边拆边建”流程”当你启动应用时,流程是这样的:
- 第一步: 拿到根 Widget(比如
MyApp)。 - 第二步: 立即为
MyApp创建对应的 Element,并将其挂载(Mount)到树上。 - 第三步: 立即运行该 Element 的
build()方法。这个方法会返回其直接子 Widget。 - 第四步: 拿到子 Widget 后,立即为它创建 Element…、
- 循环往复: 如此递归下去,直到遇到叶子节点(也就是没有子组件的节点)
1.3、flutter生命周期
Section titled “1.3、flutter生命周期”initState(): widget重建时不会执行,因为是分开独立管理的,只会在初始化state的时候执行
通常用于发送网络请求
didUpdateWidget(): setState 当改状态所属的空间更新是会执行执行之后会在执行build
build(); setState 会重新构建build;
dispose(): ifelse 导致控件移除销毁的时候State 对象移除会调用,重新创建控件替换的这种不算
deactivate(): 当 State 对象从树中被暂时移除时调用(例如在 Navigator 跳转时)Tab 切换、使用 GlobalKey 移动组件
void deactivate() { // 此时组件已断开链接,但尚未销毁 print('--- 链接已断开 (deactivate) ---'); super.deactivate();}
@overridevoid activate() { super.activate(); // 此时组件重新回到了树中 print('--- 重新建立链接 (activate) ---');}didChangeDependencies(): 当 State 对象的依赖(如 InheritedWidget 或 Theme)发生变化时调用。它在 initState 之后也会立即调用一次。
当你的组件通过 context 获取了某个父级节点的数据,而这个父级节点具备“向下广播”数据的能力时,你的组件就与它建立了依赖关系。
💡 特别提醒:如果是指“页面切换”
如果你是因为 Navigator 页面跳转 导致的“断开”和“重连”(比如从购物车返回商品页),deactivate/activate 可能不会如你预期般触发,因为页面栈的逻辑更复杂。
这种情况下,你需要使用 RouteObserver 来判断:
- didPushNext:当你跳到下一个页面(当前页面“断开”)。
- didPopNext:当用户从下个页面返回(当前页面“重连”)。
第二章、flutter基础语法
Section titled “第二章、flutter基础语法”1、dart基础数据类型
Section titled “1、dart基础数据类型”- 数字 (Numbers)
Dart 提供了三种处理数值的类型:
-
int:整数值,通常为 64 位(取决于平台)。 -
double:64 位双精度浮点数。 -
num:int和double的父类。当一个变量既可以是整数又可以是浮点数时使用。 -
字符串 (Strings)
-
String:UTF-16 编码的字符序列。可以使用单引号或双引号定义,支持插值表达式${expression}。 -
布尔值 (Booleans)
-
bool:仅有两个对象,即布尔字面量true和false。 -
集合 (Collections)
Dart 内置了强大的集合支持:
-
List:有序的项目集合(类似于数组)。 -
Set:无序且元素唯一的集合。 -
Map:键值对映射集合。 -
记录 (Record)
-
Record:在 Dart 3.0 引入,允许将多个值组合成一个单一对象,类似于匿名结构体。例如:(String, int) record = ('A', 1);。 -
其他核心类型
-
Runes(及其替代方案):用于表示字符串中的 Unicode 字符点。 -
Symbol:用于表示在 Dart 程序中声明的运算符或标识符。 -
Null:表示空值的类型,只有一个值null。 -
dynamic:显式告知编译器关闭静态类型检查(慎用)。 -
void:通常用于表示函数不返回任何值。 -
特殊底层类型
-
Object:除Null外所有 Dart 类的基类。 -
Never:表示表达式永远无法成功完成评估(通常用于总是抛出异常的函数)。
注意: 在 Dart 中,一切皆为对象(包括数字和函数),所有类型都直接或间接继承自 Object。随着 2026 年 Dart 宏(Macros)等特性的成熟,类型系统在静态元编程方面得到了进一步增强。
dart中的map value 没有静态检查
在 Map<String, dynamic> 中,虽然键(Key)是确定的,但值(Value)通常被声明为 dynamic。
- 所以当你从 Map 中取出值时,Dart 编译器并不知道它具体是什么类型。
- 你必须手动进行类型转换(如
as String),或者在运行时承担崩溃的风险。编译器无法在编译阶段提醒你“这里应该是数字而不是字符串”。
// 动态的Map<String, dynamic> userJson = { "id": 101, // int "name": "张三", // String "isVIP": true, // bool "tags": ["A", "B"] // List};
// var声明 var是自动推断类型,这样的推断相当于 Map<String, dynamic> 或者Map<String, Object?>var data = {"id": "101", "name": null};
Map<int, String> numMap = { 0: 'zero', 1: 'one', 2: 'two',};print(numMap);numMap.remove(1);print(numMap);
---->[控制台输出]----{0: zero, 1: one, 2: two}{0: zero, 2: two}
Map<int, String> numMap = { 0: 'zero', 1: 'one', 2: 'two',};numMap[3] = 'three';numMap[4] = 'four';print(numMap);
---->[控制台输出]----{0: zero, 1: one, 2: two, 3: three, 4: four}
Map<int, String> numMap = { 0: 'zero', 1: 'one', 2: 'two',};numMap.forEach((key, value) { print("${key} = $value");});
---->[控制台输出]----0 = zero1 = one2 = two3、Record
Section titled “3、Record”4.1、性能与内存优势
- Map:是一个复杂的哈希表结构,在内存中比较重,查找 Key 需要计算哈希值。
- Record:在底层接近于一组局部变量,内存占用极低,访问速度极快,没有哈希查找的过程。
4.2、自动实现“值相等”
如果你有两个 Map,即便内容一样,它们也不一定相等。但 Record 默认就实现了内容比较,但仅仅是 基本数据类型
var r1 = (a: 1, b: 2);var r2 = (a: 1, b: 2);print(r1 == r2); // 输出 true!1. 它是如何工作的?
Section titled “1. 它是如何工作的?”record 是一种匿名、不可变、聚合的类型。
-
(String, int):这是类型声明,表示这个变量必须包含一个字符串和一个整数。 -
('A', 1):这是赋值,它按照顺序将值组合在一起。 -
核心特性
-
固定长度:一旦定义,不能添加或删除字段。
-
类型安全:它不像
List只能存同一种类型,也不像Map失去了静态类型检查。它明确知道第一个是String,第二个是int。 -
值相等性:如果两个 Record 的内容完全一样,它们就是相等的(不需要重写
==和hashCode)。 -
如何取值?
Record 使用 .$索引 的方式来访问(索引从 1 开始):
dart
var record = ('A', 1);print(record.$1); // 输出 'A'print(record.$2); // 输出 1请谨慎使用此类代码。
2.为什么需要它?(最实用的场景)
Section titled “2.为什么需要它?(最实用的场景)”在没有 Record 之前,如果你的函数想返回两个值(比如用户姓名和年龄),你得定义个类或者返回一个 Map。现在你可以直接写:
dart
// 函数定义:返回一个包含姓名和年龄的记录(String, int) getUserInfo() { return ("张三", 25);}
void main() { // 调用并使用结构赋值(Destructuring) var (name, age) = getUserInfo(); print("姓名: $name, 年龄: $age");}请谨慎使用此类代码。
3.进阶:命名命名字段
Section titled “3.进阶:命名命名字段”你还可以给记录里的字段起名字,这样代码可读性更强:
dart
// 定义带名字的记录({String name, int age}) userInfo = (name: "张三", age: 25);
print(userInfo.name); // 直接通过名字访问,不再用 $1请谨慎使用此类代码。
总结
(String, int) 就像是一个轻量级的临时小容器。它比类更轻(不用写那么多代码),比 List 更强(可以存不同类型且类型安全),是 2026 年处理多值传递的首选方案。
4.深度对比:Map vs Record
Section titled “4.深度对比:Map vs Record”| 特性 | Map (运行时) | Record (静态/2026主流) |
|---|---|---|
| 定义 | Map<String, dynamic> | ({String name, int age}) |
| 访问成员 | user["name"] (可能返回 null) | user.name (必定存在且类型正确) |
| 拼写检查 | 无(写错 key 运行才知) | 有(写错属性名编译报错) |
| 类型保障 | 弱(需要频繁 as 强转) | 强(编译器严格锁定类型) |
| 性能 | 略慢(需要哈希查找) | 极快(类似于局部变量) |
4、空安全 !
Section titled “4、空安全 !”Dart 是一个空安全的语言,也就是说,你无法将一个非空类型对象值设为 null :
void main() { int a = null//这个是不行的 int? a = null // 这样可以}
void payWay(string? name) { // 或者这样}5、只允许在当前文件下访问
Section titled “5、只允许在当前文件下访问”要在class 、变量等命名的时候前面加_下划线
第三章、修饰符
Section titled “第三章、修饰符”注意这里是修改符,并不是声明变量的关键词,如果没有主动设置变量类型会进行自动推导,但是它是用来修饰变量的
1、编译时常量const
Section titled “1、编译时常量const”在 Dart 中,
const和final都用于定义不可变的变量,一旦赋值就不能再修改。但在 2026 年的现代 Dart 开发中,它们的区别主要体现在赋值时机和内存表现上。
-
const(编译时常量):它的值必须在编译阶段就能确定。这就意味着你只能用字面量(如123,"hello")或其他const变量给它赋值。不能用函数返回值 -
const:在内存中是单例的。如果代码中出现了多次相同的const对象,Dart 编译只会创建一个内存实例,并进行复用。这对于优化 Flutter 渲染性能至关重要(例如const Text('Hello'))。 -
final(运行时常量)::常用于定义类中的属性。你可以在构造函数中初始化,它每次初始化都会分配新的内存空间(除非是基本值类型)。它的值可以在程序运行时初始化确定。你可以用函数返回值、网络请求结果或用户输入给它赋值。
-
const字段:不能”直接”定义在类中,除非它同时被声明为static。因为class中的是变量,我发在编译期间确定值
-
final集合:变量本身的引用不能改,但集合内部的内容是可以修改的(除非集合本身也是不可变的)。 -
const集合:整个集合及其内部元素都是绝对不可变的。深度不可变性 (Deep Immutability) -
在 Flutter/Dart 开发中,遵循 “Const First” 原则:能用
const的地方永远优先使用const,因为它能显著提升应用的运行效率和减少内存开销。如果值只有在运行时才能拿到,再使用final。 -
StatefulWidget 也可以使用const canonical
class TestPage extends StatelessWidget { const TestPage({super.key}); @override Widget build(BuildContext context) { /* 对象本身几乎没区别,但变量语义有区别。 c1(const 变量) 编译期常量绑定 不能重新赋值 c2(var 变量,初始值是 const 对象) 当前先指向同一个 const 对象 但后面可以重新赋值到别的对象 所以: 在“当前这两行刚声明后”,identical(c1, c2) 通常是 true 但从语言语义上它们不是一回事:c2 是可变引用,c1 不是。 */ // 保证参数值是一样的,不变的,不能是变量 const c1 = Textwrapper(text: 'Apple'); var c2 = const Textwrapper(text: 'Apple'); print('Textwrapper identical = ${identical(c1, c2)}'); //out true
}}
class Textwrapper extends StatelessWidget { final String text; // 这里的const 只是语法,不然实例化的时候不能用const const Textwrapper({required this.text,super.key}); @override Widget build(BuildContext context) { return Container( child: Text(text), ); }}2、运行时常量 final
Section titled “2、运行时常量 final”在类(Class)中声明 final 变量时,Dart 并不强制要求你立刻给它赋值。它只要求:在对象创建完成(即构造函数运行结束)之前,该变量必须被初始化且只能被赋值一次。
class A { final String name; final int age;
// 正确:先声明字段,再使用 this 语法糖赋值,这里等同于初始化列表,本质上就是初始化列表的自动化缩写。 A(this.name, this.age);}1、自动类型推断var
Section titled “1、自动类型推断var”var name = "张三"; // Dart 自动推断 name 是 String 类型var age = 25; // Dart 自动推断 age 是 int 类型var data = {"id": 1}; // Dart 自动推断 data 是 Map<String, int> 类型var data = {"id": 1, 'name': null}; // Dart 自动推断 data 是 Map<String, dynamic> 类型1.1、关键特性:它是“强类型”的
Section titled “1.1、关键特性:它是“强类型”的”很多人误以为 var 像 JavaScript 那样可以随便改变类型,但在 Dart 中,一旦推断出类型,就不能再更改。
1.2、与 dynamic 的区别
Section titled “1.2、与 dynamic 的区别”这是初学者最容易混淆的地方:
var:类型是固定的。编译器在编译时就帮你填好了类型,运行速度快,安全。dynamic:类型是动态的。变量可以在运行期间从int变成String。它会跳过类型检查,风险较高。
1.3、 什么时候用 var?
Section titled “1.3、 什么时候用 var?”在 2026 年的 Dart 开发规范中,推荐做法是:
-
*个人觉得就是用来快速接收变量使用的
-
在局部变量(函数内部)使用
var:让代码更简洁。- 推荐:
var user = User(); - 不推荐:
User user = User();(类型写了两遍,冗余)
- 推荐:
-
在类的成员变量(Field)中建议显式声明类型:这样看类定义时一眼就能知道属性是什么类型。
class MyApp { // 这里不能自动推断的可以手动声明 final int age; // 这里有默认值可以自动推断的就使用var var name = 'zhagnsan'; MyApp(this.age) {
}}1.4、如果不立即赋值会怎样?
Section titled “1.4、如果不立即赋值会怎样?”如果你声明 var 但没有初始化,它的类型会变成 dynamic:
var temp; // 类型被推断为 dynamictemp = 1;temp = "hello"; // 不会报错注意: 为了代码安全,应尽量避免这种写法,最好在声明时就初始化。
总结
var 就像是一个懒人工具,它告诉编译器:“你这么聪明,看我右边写的是什么,你自己把左边的类型填上吧!”它既保持了代码的简洁,又没有牺牲 Dart 作为强类型语言的安全性。
第四章、flutter Class
Section titled “第四章、flutter Class”class foo { int _internal = 0; // 这里前面的下划线代表私有的}1、构造函数
Section titled “1、构造函数”- 普通构造函数
- 命名构造函数
- 工厂构造函数
1.1、初始化列表 (Initializer List)
Section titled “1.1、初始化列表 (Initializer List)”它是构造函数中一个非常特殊且强大的区域,执行时机处于**“对象刚分配内存”之后**,但**“构造函数体** {} 运行”之前。
以下是这种写法的核心含义:
- 为什么用冒号
:而不是写在大括号里?
如果你在类中定义了 final 变量(比如 createdAt),它们必须在对象创建完成前就被赋值。
-
大括号
{}内部:属于“赋值”阶段。对于final变量来说,到这一步已经太晚了(final必须在“初始化”阶段完成)。 -
初始化列表
::属于“初始化”阶段。这是给final变量赋值的唯一合法位置(除了直接在声明处赋值)。 -
这里举例子用的是命名构造函数,普通的构造函数也可以,但是工厂构造函数不行
class ApiService { final String url; final DateTime createdAt; final String status;
// 使用逗号分隔多个初始化项, ApiService._private(this.url) : createdAt = DateTime.now(), status = 'loading', assert(url.isNotEmpty) { // 也可以包含断言 print("所有 final 字段已就绪"); }}一句话理解冒号 : 的存在:
它是 Dart 为 final 变量设置的“最后通牒区”——在对象内存完全锁死之前,你有最后一次机会通过冒号后的代码把值填进去。
至于为什么不在初始化的时候在赋值,是因为final定义的变量要接收变量,或者创建对象前要赋值
断言
断言既可以在初始化列表中使用也可以在函数体中使用,区别就是对象有没有出现在内存中,通常除了在初始化列表中要断言计算后的值,其他断言基本可以放在初始化列表
//这样也可以只不过是多算了一遍表达式Score(List<int> points) : total = points.reduce((a, b) => a + b), assert(points.reduce((a, b) => a + b) <= 100); // 只能重算一遍表达式在 Dart 中,this.name(Initializing Formals)的执行时机等同于初始化列表。
class A { final String name; final int age;
// 正确:先声明字段,再使用 this 语法糖赋值 A(this.name, this.age);}1.1.1、初始化为什么不在定义的时候赋值?
Section titled “1.1.1、初始化为什么不在定义的时候赋值?”虽然你可以在定义时直接赋值(如 final String status = 'active';),但在实际开发中,有以下三个理由让你“不得不”在构造函数中赋值:
-
依赖外部传入的数据:很多
final变量的值取决于你创建对象时传入的参数(例如url)。定义时你并不知道用户会传什么。 -
计算逻辑:有些值需要根据传入的参数计算出来。
dart
final String domain;// 定义时无法赋值,因为需要解析传入的 urlApiService(String url) : domain = Uri.parse(url).host;请谨慎使用此类代码。
-
节省内存/延迟创建:如果在定义时赋值,那么每个实例的初始值都是一样的。通过构造函数赋值,可以让每个对象拥有自己独特的、不可变的“基因”。
2、工厂函数factory
Section titled “2、工厂函数factory”| 特点 | 普通函数 (Regular Function) | 工厂函数 (Factory Method) |
|---|---|---|
| 核心职责 | 执行任务、处理数据、改变状态 | 生产并返回实例对象 |
| 命名契约 | 通常是动词(run, print) | 通常含 create, from, get |
| 对类依赖 | 处理一段逻辑返回想要的数据 | 返回的组件对象 |
| 2026 实践 | 解决局部的小逻辑 | 负责模块间的解耦和对象初始化 |
- 工厂函数能做的普通函数也能做,工厂函数做不了的普通函数也能做,我为什么要用工厂函数
那就是说普通实例工厂函数和普通实例函数在特性和特点上没有任何区别,唯一的区别就是语义的区别,不考虑语义规范,实际上普通函数可以完全替代工厂函数
但是其实是一种编程哲学,主要区别在语义上
语义上的“防呆设计”(降低认知负担)
在大型项目中,代码的可读性高于一切,防止你的代码在快速迭代中不至于变成一团乱麻。
- 普通函数:如果你看到一个函数叫
processUser(),你不知道它是修改了用户数据,还是删除了用户,还是返回了一个新用户。 - 工厂函数:如果你看到
User.fromJSON()或createUser(),你的大脑会自动切换到“造物模式”。你明确知道:调用它一定会得到一个对象,且不会对现有数据产生副作用。 - 结论:工厂函数建立了一种标准协议,让成千上万行代码的意图变得一眼可见。
- 只有工厂函数能实现的“无感解耦”
3、工厂构造函数
Section titled “3、工厂构造函数”
factory(工厂构造函数)的核心作用是打破了“构造函数必须创建新对象”的限制。
作用:
- 数据清洗与逻辑前置
| 特性 | 普通构造函数 | 工厂构造函数 (factory) |
|---|---|---|
| 关键字 | 无 | 必须使用 factory |
| 内存表现 | 必定产生新内存地址 | 不一定,可以复用旧地址 |
| return 语句 | 禁止写 return, 必须返回当前类新实例, 不能返回缓存对象/子类/别的实例 | 必须写 return 可以决定返回什么实例, 适合 fromJson、单例、缓存复用、参数分流 |
| this 访问 | 可以访问 this | 禁止访问 this(因为对象可能还没造出来) |
| 子类化 | 只能产出本类 | 可以产出本类或任意子类 |
3.1、实现单例模式
Section titled “3.1、实现单例模式”- 这里是主要作用返回缓存对象,应为普通的构造函数不能return
class ApiService { final String url;
// 1. 定义一个私有的静态变量,用来在内存中缓存唯一的实例 static ApiService? _instance;
// 2. 这是你提到的命名构造函数(负责真正的内存分配和初始化) // 开头的下划线 "_" 确保了外部文件无法直接通过 ApiService._private() 来创建对象 ApiService._private(this.url) { print("【底层逻辑】正在为 ApiService 分配内存,并初始化 URL: $url"); }
// 3. 工厂构造函数:它是外部访问的唯一入口 // 它的逻辑是:如果缓存里有,就给旧的;没有,才调用上面的 _private 构造器造个新的 factory ApiService() { if (_instance == null) { print("【工厂逻辑】内存中未发现实例,准备调用私有构造器..."); // 调用上面的命名构造函数 _instance = ApiService._private("https://api.example.com"); } else { print("【工厂逻辑】内存中已存在实例,直接返回旧对象。"); } return _instance!; }
void getData() { print("正在通过 $url 请求数据..."); }}
void main() { print("--- 第一次调用 ApiService() ---"); var service1 = ApiService(); service1.getData();
print("\n--- 第二次调用 ApiService() ---"); var service2 = ApiService(); service2.getData();
// 验证内存地址 print("\nservice1 和 service2 是否为同一个对象: ${identical(service1, service2)}");}3.2、返回子类实例
Section titled “3.2、返回子类实例”abstract class Shape { factory Shape.circle() = Circle; // Circle 必须实现/继承 Shape}class Circle implements Shape {}3.4、数据清洗与逻辑前置
Section titled “3.4、数据清洗与逻辑前置”class User { final int id; final String name;
User._internal(this.id, this.name);
// 工厂构造函数:在真正调用 _internal 生产对象前,先“洗”一遍数据 factory User.fromJson(Map<String, dynamic> json) { // 1. 数据清洗:强制转换类型(防止后端乱传 String 类型的 id) final rawId = json['id']; int id = rawId is String ? int.parse(rawId) : (rawId as int);
// 2. 逻辑前置:数据补全 String name = json['name'] ?? "匿名用户";
// 3. 逻辑拦截:如果 id 不合法,可以抛出异常或返回特定对象 if (id < 0) { throw Exception("非法的用户 ID"); }
// 4. 全部清洗完毕,交给真正的构造函数 return User._internal(id, name); }}
void main() { var data = {"id": "101", "name": null}; var user = User.fromJson(data); print("清洗后的数据: ID=${user.id}, Name=${user.name}"); // ID=101, Name=匿名用户}一句话总结: 普通构造函数是死的(只能创建新对象因为不能return);工厂构造函数是活的(它可以根据内存情况、传入参数,决定是给你一个旧的、一个新的、还是给一个子类的对象)。
4、StatefulWidget 和 StatelessWidget
Section titled “4、StatefulWidget 和 StatelessWidget”4.1、为什么build 会放到state中
Section titled “4.1、为什么build 会放到state中”当某些情况下会导致组件重新实例化
- 依赖的“环境上下文”发生变化
- 屏幕旋转/尺寸改变:使用了
MediaQuery.of(context)。 - 主题切换:使用了
Theme.of(context)。 - 多语言切换:使用了
Localizations.of(context)。 - 系统字体缩放:用户在系统设置里调大了字体。
- ** 路由管理与页面切换**
- 模态框/弹窗
- 页面返回
4、父子组件通信
Section titled “4、父子组件通信”4.1,父组件获取子组件state调用
Section titled “4.1,父组件获取子组件state调用”import 'package:flutter/material.dart';
// 子组件class ChildWidget extends StatefulWidget { ChildWidget({Key? key}) : super(key: key);
@override ChildState createState() => ChildState();}
class ChildState extends State<ChildWidget> { String _message = "等待中...";
void updateText(String text) { setState(() => _message = text); }
@override Widget build(BuildContext context) => Text("子组件状态: $_message");}
// 父组件class ParentWithKey extends StatelessWidget { // 定义一个 GlobalKey,泛型指定为子组件的 State 类 final GlobalKey<ChildState> _childKey = GlobalKey<ChildState>();
@override Widget build(BuildContext context) { return Column( children: [ ChildWidget(key: _childKey), // 将 Key 传给子组件 ElevatedButton( onPressed: () => _childKey.currentState?.updateText("来自父组件的操作"), child: Text("点击改变子组件"), ), ], ); }}4.2、of 静态方法获取 State
Section titled “4.2、of 静态方法获取 State”这种方式遵循 Flutter 惯例(如 Theme.of),用于子组件主动获取父组件的状态。
import 'package:flutter/material.dart';
// 父组件class ParentWidget extends StatefulWidget { @override ParentState createState() => ParentState();
// 惯用的 static of 方法 static ParentState? of(BuildContext context) { // 这里为什那么向上查找因为因为上下文是当前调用的上下文 return context.findAncestorStateOfType<ParentState>(); }}
class ParentState extends State<ParentWidget> { int count = 0;
void increment() => setState(() => count++);
@override Widget build(BuildContext context) { return Column( children: [ Text("父组件计数: $count"), Divider(), ChildWidget(), // 正常的后代组件 ], ); }}
// 子组件class ChildWidget extends StatelessWidget { @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () { // 通过 ParentWidget.of 向上寻找 State final parentState = ParentWidget.of(context); parentState?.increment(); }, child: Text("让父组件自增"), ); }}注意这样方式会有有获取不到的可能性
- 不在同一个 Element Tree 分支上:
这是最常见的原因。
context是 Element Tree 中的一个位置。如果你通过Overlay(例如showDialog、showModalBottomSheet)弹出子组件,这些弹窗实际上被挂载到了顶级Navigator的Overlay中,它们在树中的位置与原父组件是并列的,而不是其后代,因此无法向上搜寻到父 State。 - 泛型匹配失败:
Flutter 严格匹配泛型类型。如果你在调用时写错了 State 的类名,或者由于 Package 导入路径不一致(例如一个用
package:my_app/main.dart导入,另一个用相对路径../main.dart导入),Dart 可能会将它们视为两个不同的类型。 - 尚未挂载(Unmounted):
如果在
State.initState中直接调用(此时context还没完全与树关联好),或者在异步操作(如await)之后组件已经从树中移除时调用,也会找不到。 - 跨路由跳转:
如果你通过
Navigator.push跳转到了一个新页面,新页面是整个屏幕的重绘,它不再是旧页面组件的子节点,自然找不到旧页面的 State。
4.3、回调函数的方式
Section titled “4.3、回调函数的方式”import 'package:flutter/material.dart';
// 1. 父组件:持有状态和修改逻辑class ParentWrapper extends StatefulWidget { @override _ParentWrapperState createState() => _ParentWrapperState();}
class _ParentWrapperState extends State<ParentWrapper> { String _sharedText = "初始状态";
// 定义回调函数:供子组件 A 调用 void _updateSharedStatus(String newValue) { setState(() { _sharedText = newValue; }); }
@override Widget build(BuildContext context) { return Column( children: [ Text("父组件监控到状态: $_sharedText", style: TextStyle(fontWeight: FontWeight.bold)), Divider(), // 传入回调函数给 A SiblingA(onAction: _updateSharedStatus), // 传入最新数据给 B SiblingB(displayData: _sharedText), ], ); }}
// 2. 兄弟组件 A:触发者class SiblingA extends StatelessWidget { final Function(String) onAction; // 接收父组件传来的回调
SiblingA({required this.onAction});
@override Widget build(BuildContext context) { return ElevatedButton( onPressed: () => onAction("A 修改了状态!"), // 执行回调,通知父组件 child: Text("我是 A:点击通知兄弟 B"), ); }}
// 3. 兄弟组件 B:展示者class SiblingB extends StatelessWidget { final String displayData; // 接收父组件传来的数据
SiblingB({required this.displayData});
@override Widget build(BuildContext context) { return Container( padding: EdgeInsets.all(10), color: Colors.blue[50], child: Text("我是 B:收到的实时数据是 -> $displayData"), ); }}第四章、路由
Section titled “第四章、路由”1.MaterialPageRoute
Section titled “1.MaterialPageRoute”MaterialPageRoute({ WidgetBuilder builder, RouteSettings settings, // 包含路由的配置信息,如路由名称、是否初始路由(首页) bool maintainState = true, // 默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中,如果想在路由没用的时候释放其所占用的所有资源,可以设置maintainState为 false。 bool fullscreenDialog = false, // 表示新的路由页面是否是一个全屏的模态对话框,在 iOS 中,如果fullscreenDialog为true,新页面将会从屏幕底部滑入(而不是水平方向)。 })案例
class RouterTestRoute extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: ElevatedButton( onPressed: () async { // 打开`TipRoute`,并等待返回结果 var result = await Navigator.push( context, //这个是Material 页面路由 MaterialPageRoute( builder: (context) { // 这个是一个新组件 return TipRoute( // 路由参数 text: "我是提示xxxx", ); }, ), ); //输出`TipRoute`路由返回结果 print("路由返回值: $result"); }, child: Text("打开提示页"), ), ); }}2. Navigator
Section titled “2. Navigator”Navigator是一个路由管理的组件,它提供了打开和退出路由页方法。Navigator通过一个栈来管理活动路由集合。通常当前屏幕显示的页面就是栈顶的路由。Navigator提供了一系列方法来管理路由栈,在此我们只介绍其最常用的两个方法:
#1. Future push(BuildContext context, Route route)
Section titled “#1. Future push(BuildContext context, Route route)”将给定的路由入栈(即打开新的页面),返回值是一个Future对象,用以接收新路由出栈(即关闭)时的返回数据。
#2. bool pop(BuildContext context, [ result ])
Section titled “#2. bool pop(BuildContext context, [ result ])”将栈顶路由出栈,result 为页面关闭时返回给上一个页面的数据。
Navigator 还有很多其他方法,如Navigator.replace、Navigator.popUntil等,详情请参考API文档或SDK 源码注释,在此不再赘述。下面我们还需要介绍一下路由相关的另一个概念“命名路由”。
#3. 实例方法
Section titled “#3. 实例方法”Navigator类中第一个参数为context的静态方法都对应一个Navigator的实例方法, 比如Navigator.push(BuildContext context, Route route)等价于Navigator.of(context).push(Route route) ,下面命名路由相关的方法也是一样的。
3. Go router
Section titled “3. Go router”import 'package:go_router/go_router.dart';import 'package:groe_app_pad/app/router/app_routes.dart';import 'package:groe_app_pad/features/auth/presentation/providers/session_controller.dart';import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'app_router.g.dart';
@Riverpod(keepAlive: true)GoRouter appRouter(Ref ref) { final sessionState = ref.watch(sessionControllerProvider);
final bool isLoading = sessionState.isLoading; final bool isLoggedIn = sessionState.asData?.value.isAuthenticated ?? false;
return GoRouter( routes: $appRoutes, // 初始化路由 initialLocation: const SplashRoute().location, // redirect 触发时机,1.路由跳转 2.依赖变化,路由重建 /* 1.先拿 initialLocation(你这里是 /splash) 2.立即跑 redirect 3.若返回新地址就跳转,否则留在当前地址,可能觉的 立即跑 redirect 也会回到SplashRoute,但是是有必要的 -可能此时 session 已经是已登录状态,直接就该去首页,不该停 splash -可能是未登录,应该去 login -可能有深链或其他状态约束,需要第一时间裁决 你当前代码里第一次通常会返回 null(留在 splash),所有要等isLoading依赖变化的时候才会在调用一次跳到指定的页面 */ redirect: (context, state) { final atSplash = state.matchedLocation == const SplashRoute().location; final atLogin = state.matchedLocation == const LoginRoute().location;
if (isLoading) { return atSplash ? null : const SplashRoute().location; }
if (!isLoggedIn) { return atLogin ? null : const LoginRoute().location; }
if (atLogin || atSplash) return const HomeRoute().location; return null; }, );}第四章、JSON转换
Section titled “第四章、JSON转换”第五章、断言
Section titled “第五章、断言”| 特性 | 断言 (assert) | 代码逻辑 (if/else/throw) |
|---|---|---|
| 生效环境 | 仅 Debug 模式 | 所有模式 (Debug/Release) |
| 解决的问题 | 程序员的 Bug(代码写错了) | 用户的操作/环境(网络断、余额不足) |
| 运行后果 | 失败即闪退/红屏,强制开发者修复 | 捕获异常后程序继续运行,通过 UI 提示用户 |
| 性能开销 | Release 模式下被剔除,零开销 | 始终存在,有微量开销 |
代码中的断言
void processPayment(double amount, double balance) { // 1. 断言:检查程序员的逻辑错误(内部状态假设) // 理由:如果余额为负数,说明之前的逻辑出大 Bug 了,调试期必须停下来 assert(balance >= 0, '内部错误:检测到账户余额为负数,请检查数据库逻辑');
// 2. 逻辑:处理运行时环境/用户行为 // 理由:余额不足是正常的业务情况,发布版也要处理,不能用断言 if (amount > balance) { print("提示:余额不足,支付失败。"); return; }
// 执行支付... print("支付成功:$amount");
// 3. 断言:校验执行结果 assert(balance >= 0, '支付后余额异常');}构造函数中的断言
class AppConfig { final String apiEndpoint; final int timeout; late String _logMessage;
AppConfig({required this.apiEndpoint, required this.timeout}) // --- 阶段 A: 初始化列表与断言 --- : assert(apiEndpoint.startsWith('https'), '安全警告:API 必须使用 https'), assert(timeout > 0, '配置错误:超时时间必须大于 0') {
// --- 阶段 B: 构造函数逻辑体 --- _logMessage = "配置已加载:$apiEndpoint"; print(_logMessage);
// 逻辑处理:如果超时过长,在调试期给个提醒 if (timeout > 10000) { print("注意:当前设置的超时时间较长。"); } }}第六章 函数
Section titled “第六章 函数”匿名函数写法区别
- 返回值的区别
- 是否多行代码区别
| 特性 | () {} | () => add() |
|---|---|---|
| 全称 | 匿名函数体 | 箭头函数(语法糖) |
| 等价写法 | - | { return add(); } |
| 返回值 | void 或显式 return 的值 | 自动返回 add() 的值 |
| 逻辑行数 | 可以写很多行 | 只能写一行表达式 |
| 函数类型 | 关键标志 | 注意事项 |
|---|---|---|
| 普通方法 | name() | 必须有小括号 () |
| Getter | get name | 禁止加小括号 () |
| Setter | set name(v) | 必须有一个参数,禁止写返回类型 |
| 箭头函数 | => | 只能写一行表达式,隐含 return |
| 构造函数简写 | this.name | 只能在构造函数参数位使用,不能写类型 |
| 异步函数 | async | 返回类型必须被 Future<T> 包裹 |
一句话记忆:
只要是“干活”的动作(方法),带括号 ();只要是“拿东西”或“设东西”的动作(属性访问),不带括号。
1. 基础函数// 显式声明返回类型和参数类型int add(int a, int b) { return a + b;}
// 如果没有返回值,使用 voidvoid printMessage(String msg) { print(msg);}
2. 箭头函数// 相当于 { return a + b; }int add(int a, int b) => a + b;
// 常用在 Flutter 的组件构建中Widget build(BuildContext context) => Scaffold(body: Center());
3. 取值器与设置器class Rectangle { double width = 0; double height = 0;
// Getter: 取值器,使用 get 关键字,不带 () double get area => width * height;
// Setter: 设置器,使用 set 关键字,必须带 (参数) set side(double value) { width = value; height = value; }}
// 调用示例:var rect = Rectangle();rect.side = 10; // 像赋值一样调用 setterprint(rect.area); // 像访问属性一样调用 getter
4. 构造函数class User { final String name; final int age;
// 1. 默认构造函数(初始化简写) User(this.name, this.age);
// 2. 命名构造函数 (Named Constructor) User.guest() : name = "访客", age = 0;
// 3. 工厂构造函数 (Factory Constructor) // 不一定创建新实例,可以返回缓存或子类 factory User.fromJson(Map<String, dynamic> json) { return User(json['name'], json['age']); }
// 4. 常量构造函数 const User.constant(this.name, this.age);}
5. 匿名函数与闭包 (Anonymous Functions)void main() { var list = ['Apple', 'Banana'];
// 匿名箭头函数 list.forEach((item) => print(item));
// 匿名大括号函数 list.forEach((item) { var upper = item.toUpperCase(); print(upper); });}
6. 异步函数 (Asynchronous Functions)// 返回 Future<T>,使用 async 标记,内部使用 awaitFuture<String> fetchData() async { var result = await http.get('...'); return result.body;}
// 如果没有返回值,写 Future<void>Future<void> saveSettings() async { await storage.write(...);}
7. 静态函数 (Static Functions)class Utils { // 静态工具方法,直接通过 Utils.formatDate 调用 static String formatDate(DateTime date) => "${date.year}-${date.month}";}第七章、异常
Section titled “第七章、异常”捕获异常:Try-Catch
try { // 可能抛出错误的代码 var result = 10 ~/ 0; // 整数除以零
} on IntegerDivisionByZeroException { // 相当于if和switch的作用 如果当前异常是IntegerDivisionByZeroException就执行这个代码块 print('不能除以零!');} catch (e, s) { // 捕获所有其他异常 // e 是异常对象,s 是堆栈信息(StackTrace) print('未知错误: $e'); print('堆栈追踪: $s');} finally { // 无论是否报错,最后都会执行(通常用于关闭资源) print('清理工作完毕');}抛出异常
void setAge(int age) { if (age < 0) { // 1. 直接抛出一个异常对象 throw Exception('年龄不能为负数:$age'); } print('年龄设置为: $age');}自定义业务异常
// 自定义异常,实现 Exception 接口class InsufficientBalanceException implements Exception { final double balance; final double required;
InsufficientBalanceException(this.balance, this.required);
@override String toString() => '余额不足:当前 $balance, 需要 $required';}
void pay(double price, double myBalance) { if (myBalance < price) { // 抛出自定义异常 throw InsufficientBalanceException(myBalance, price); }}重新抛出异常
try { pay(100, 50);} on InsufficientBalanceException catch (e) { print('记录日志: 用户尝试支付失败');
// 重新抛出,让 UI 层也能捕获到并弹出提示框 rethrow;}拦截全局异常
import 'dart:ui';import 'package:flutter/material.dart';
void main() { // 确保 Flutter 绑定初始化(使用异步拦截时必须先调这句) WidgetsFlutterBinding.ensureInitialized();
// A. 框架错误拦截 FlutterError.onError = (details) { FlutterError.presentError(details); // TODO: 调用上报接口 };
// B. 异步/根错误拦截 PlatformDispatcher.instance.onError = (error, stack) { // TODO: 调用上报接口 return true; };
// C. UI 错误视图自定义 ErrorWidget.builder = (details) => MyCustomErrorPage(details: details);
runApp(const MyApp());}第八章、混入Mixin|with|on
Section titled “第八章、混入Mixin|with|on”mixin Logger { void log(String msg) => print('Log: $msg');}
class MyService with Logger { // 使用 with 关键字 void doWork() => log('Working...');}on 关键字用于限制 mixin 只能被哪些类使用。
class Bird { void fly() => print('Flying...');}
// 限制:该 mixin 只能被 Bird 或 Bird 的子类混入mixin Sing on Bird { void singAndFly() { print('Singing...'); fly(); // 因为有 'on Bird',所以这里可以直接调用 Bird 的 fly() }}
// 错误:Human 不是 Bird,编译报错// class Human with Sing {}
// 正确:Eale 继承自 Birdclass Eagle extends Bird with Sing {}第十二章、Dart Macros
Section titled “第十二章、Dart Macros”在 2026 年,Dart Macros(宏) 已经成为 Dart 语言的稳定特性。它最大的价值在于:在内存中实时生成代码,彻底告别 .g.dart 文件和 build_runner 扫描。
以下是在 2026 年使用 Macro 的具体步骤和实例:
- 环境准备
由于 Macro 是深度编译器集成的特性,你需要确保:
- SDK 版本:
pubspec.yaml中的 SDK 约束至少在3.5.0或更高。 - 开启实验特性(如果你使用的是预览版):在
analysis_options.yaml或运行命令中启用macros实验性标志。 - 安装支持宏的库
在 2026 年,主流库都推出了宏版本。例如,原来的 json_serializable 进化为了支持宏的注解。
yaml
dependencies: json: ^2.0.0 # 假设为 2026 年支持宏的 json 库请谨慎使用此类代码。
- 实际代码演示:Json 宏
注意:不再需要 part 'xxx.g.dart';,也不再需要运行 build_runner。
dart
import 'package:json/json.dart'; // 引入宏库
@JsonCodable() // 这是一个 Macro 注解class User { final int id; final String name;
// 你只需要定义字段,宏会自动在内存中帮你生成: // 1. User.fromJson(Map<String, Object?> json) // 2. Map<String, Object?> toJson()}
void main() { // 直接调用宏生成的构造函数,IDE 不会报错,因为宏是实时生成的 var user = User.fromJson({'id': 1, 'name': '张三'}); print(user.toJson());}请谨慎使用此类代码。
- 宏与
build_runner的使用区别
| 特性 | 旧方案 (build_runner) | 新方案 (Macros) |
|---|---|---|
| 生成文件 | 产生大量 .g.dart 或 .freezed.dart | 零文件,代码存在于内存中 |
| 执行命令 | 必须手动运行 dart run build_runner build | 全自动,保存代码时编译器实时处理 |
| 报错反馈 | 运行完命令才知道错 | 秒级反馈,像写普通代码一样报错 |
| IDE 支持 | 经常需要重启分析引擎才能看到生成的代码 | IDE 完美支持,属性提示实时更新 |
- 如何在 Riverpod 中使用 Macro?
在 2026 年,Riverpod 3.0+ 已经完全拥抱了宏。
dart
import 'package:riverpod/riverpod.dart';
@Riverpod() // 宏版注解class Counter extends _$Counter { @override int build() => 0;
void increment() => state++;}
// 宏会自动在内存中创建 counterProvider请谨慎使用此类代码。
注意: 此时你依然需要写 extends _$Counter,但这个 _$Counter 不再存在于硬盘上的 .g.dart 里,而是由编译器直接注入。
- 自定义宏(进阶)
如果你想自己写一个宏(比如自动生成单例模式的宏),你需要定义一个实现了 ClassDeclarationsMacro 等接口的类。这通常是框架开发者的工作。
总结
在 2026 年,Macro 的用法就是“加个注解就完事了”。
- 它解决了代码冗余。
- 它消灭了漫长的等待编译时间。
- 如果你发现你的项目还在用
build_runner,请检查是否可以升级到 Macro 版本 的库,这能极大地提升开发体验。 Dart 官方宏开发指南 是目前最高级的参考资料。 [1], [2]
第十三章、目录结构
Section titled “第十三章、目录结构”lib/├── main.dart # 程序入口,配置 ProviderScope 和全局拦截├── app.dart # MaterialApp 配置,处理路由、主题、国际化代理├── core/ # 核心共享层(不依赖具体业务)│ ├── theme/ # 主题定义(Light/Dark/Pad特定样式)│ ├── l10n/ # 国际化配置(ARB文件及生成类)│ ├── network/ # Dio 封装、拦截器、BaseUrl 配置│ ├── router/ # GoRouter 路由定义(支持响应式布局跳转)│ ├── utils/ # 通用工具类│ └── common_widgets/ # 基础原子组件(按钮、输入框)├── features/ # 业务功能层(按模块划分)│ ├── home/ # 首页模块│ │ ├── data/ # 数据源(API、DTO模型)│ │ │ ├──dto/ # dto的组成有可能是多个实体构成的│ │ │ └──mapper/ # 用于拆分转换dto│ │ │ └──data_source/ # 定义数据的来源 api get post│ │ │ └──home_repository_imp.dart/ Repository(仓库) 而不是 Api 或 Service,是因为它在设计模式中扮演着“数据守护者”的角色,而不仅仅是发个请求。│ │ ├── domain/ # 业务实体(Entity)、仓库接口 ├── entities/ # 存放实体类 │ └── product_entity.dart # 纯净的业务模型 └── repositories/ # 存放接口 └── i_product_repository.dart # 定义方法的契约│ │ ├── presentation/ # UI 逻辑│ │ │ ├── widgets/ # 局部组件│ │ │ ├── screen_m.dart # 手机端页面│ │ │ └── screen_p.dart # Pad 端页面(或响应式适配逻辑)│ │ └── home_controller.dart # 逻辑控制器 (可选) 相当于java controller 用于接受界面操纵│ ├── cart/ # 购物车模块│ ├── product/ # 商品详情模块│ └── profile/ # 个人中心模块└── shared/ # 业务共享层├── models/ # 全局通用模型(如 User、Product)└── providers/ # 全局状态(如 Auth、Theme、Locale)第十五章、常见的命令
Section titled “第十五章、常见的命令”- 基础更新(最常用)
如果你只是想根据 pubspec.yaml 中定义的版本范围获取更新:
bash
flutter pub get请谨慎使用此类代码。
- 作用:根据
pubspec.yaml下载依赖,并生成/更新pubspec.lock文件。 - 尝试升级到兼容的最新版本
如果你想在不改变 pubspec.yaml 版本约束的前提下,尝试将依赖升级到范围内的最高版本:
bash
flutter pub upgrade请谨慎使用此类代码。
- 作用:它会尝试更新
pubspec.lock,将所有包升级到你允许的最高版本(例如^3.0.0会升级到3.9.9但不会升级到4.0.0)。 - 升级到主版本(2026 推荐做法)
如果你想让项目彻底跟上 2026 年的最新节奏,直接升级到最新的大版本(可能会改动 pubspec.yaml):
bash
flutter pub upgrade --major-versions请谨慎使用此类代码。
- 作用:它会自动修改你的
pubspec.yaml文件,将版本号改为当前的最新版本。 - 注意:这可能会引入破坏性更新(Breaking Changes),升级后需要检查代码是否有报错。
- 强力清理并重新拉取
如果遇到依赖冲突或者某些包下载不完整,使用这个“组合拳”:
bash
flutter pub cache clean # 清理本地全局缓存flutter clean # 清理项目编译缓存rm pubspec.lock # 手动删除锁定文件(Windows 使用 del pubspec.lock)flutter pub get # 重新拉取请谨慎使用此类代码。
- 检查哪些包可以更新
在操作前,你可以先通过以下指令查看有哪些包出了新版本:
bash
flutter pub outdated请谨慎使用此类代码。
dart run build_runner build --delete-conflicting-outputs- 看点:它会列表对比你“当前使用的版本”、“当前允许的最高版本”以及“官方最新的版本”。
2026 年特别提示:
- Macro 库更新:由于 2026 年许多支持 Macro 的库处于快速迭代期,建议定期运行
flutter pub outdated检查更新,以获取更稳定的宏编译支持。 - SDK 约束:如果升级后报错,请检查你的
environment: sdk是否设置得过低,确保其满足最新库的要求。 Flutter 官方依赖管理指南 提供了关于版本解析的详细逻辑
第十六章、配置开发环境
Section titled “第十六章、配置开发环境”在 2026 年的 Flutter 开发中,配置环境变量(如 API 域名、密钥等)最专业、最标准的方式是使用 --dart-define 或 --dart-define-from-file。
这种方式比传统的 .env 文件更安全且性能更高,因为它在编译时就将变量注入到了代码中。
- 准备配置文件
在项目根目录下创建一个 env.json(或根据环境创建 dev.json, prod.json)。
config/dev.json
json
{ "BASE_URL": "https://dev-api.shop.com", "API_KEY": "dev_key_123", "IS_DEBUG": true}请谨慎使用此类代码。
- 在 Dart 代码中读取
使用 String.fromEnvironment、int.fromEnvironment 或 bool.fromEnvironment 来获取这些值。
lib/core/config/env_config.dart
dart
class EnvConfig { // 建议定义为 static const,这样在编译时就会被替换为常量 static const String baseUrl = String.fromEnvironment( 'BASE_URL', defaultValue: 'https://localhost:8080', );
static const String apiKey = String.fromEnvironment('API_KEY');
static const bool isDebug = bool.fromEnvironment('IS_DEBUG', defaultValue: false);}请谨慎使用此类代码。
- 运行与编译
在运行或打包时,通过参数指向你的配置文件。
-
运行开发版:
bash
Terminal window flutter run --dart-define-from-file=config/dev.json请谨慎使用此类代码。
-
打包正式版:
bash
Terminal window flutter build apk --dart-define-from-file=config/prod.json请谨慎使用此类代码。
- 2026 年进阶方案:配合 Riverpod 注入
为了在商城项目中更好地管理这些配置,建议将其封装进 Riverpod Provider。
dart
import 'package:riverpod_annotation/riverpod_annotation.dart';part 'env_provider.g.dart';
@riverpodclass AppEnv extends _$AppEnv { @override String build() => EnvConfig.baseUrl; // 使用上面定义的常量}请谨慎使用此类代码。
- 为什么 2026 年不推荐使用
.env库?
以前大家习惯用 flutter_dotenv 库,但在 2026 年这种做法已逐渐被淘汰,原因如下:
- 加载延迟:
.env需要在main函数中await加载,会增加白屏时间。 - 非强类型:
.env读取的全部是字符串,需要手动转型。 - 安全性:
--dart-define直接将值编译进二进制文件,不容易像.env文件那样被轻易从安装包中提取。
总结
- 创建 JSON:存放不同环境的配置。
- 代码常量化:使用
String.fromEnvironment定义。 - 编译注入:使用
--dart-define-from-file参数。
小贴士: 别忘了把你的 config/*.json 加入到 .gitignore 中,防止敏感密钥泄露到代码仓库! Flutter 官方关于环境配置的说明 详细介绍了编译时注入的机制。
第十七章、开发规范
Section titled “第十七章、开发规范”在 2026 年的 Flutter 开发规范中,你展示的这种结构被称为 功能优先的分层架构 (Feature-First Layered Architecture)。
这种设计是结合了 领域驱动设计 (DDD) 简化版与 整洁架构 (Clean Architecture) 的产物。它目前是中大型 Flutter 项目(尤其是使用 Riverpod 的项目)的行业标准。
以下是该架构的详细拆解:
- 核心设计理念:功能优先 (Feature-First)
与传统的“层级优先”(所有模型放一起,所有 UI 放一起)不同,这种方式**按业务逻辑(Home, Cart, Product)**进行纵向切分。
- 高内聚:修改首页逻辑时,你只需要在
home/文件夹下工作。 - 易删除/迁移:如果某个功能下线,直接删除对应的
feature/xxx目录即可,不会产生大量残留代码。 - 三层结构解析 (Layered Architecture)
在该结构中,每个功能内部又被横向切分为三层,实现了职责分离:
A. Data 层 (数据层)
- 职责:负责与外部通信(网络 API、本地数据库)。
- 内容:
data_source.dart(Dio 请求)、dto(后端返回的原始 JSON 模型)。 - 作用:解决“数据从哪来”的问题。
B. Domain 层 (领域层 - 核心)
- 职责:存放纯粹的业务逻辑和数据模型。
- 内容:
entity.dart(UI 使用的纯净模型)、repository_interface.dart(定义数据获取的协议)。 - 作用:它是最稳定的层,不关心 UI 长什么样,也不关心数据是来自 Dio 还是 Hive。
C. Presentation 层 (表现层/UI 层)
- 职责:负责将数据显示在屏幕上,并处理用户交互。
- 内容:
- Providers:Riverpod 状态管理器(取代了旧的 ViewModel)。
- Widgets/Screens:UI 界面。
- 2026 适配策略:你看到的
screen_m.dart(Mobile) 和screen_p.dart(Pad) 体现了多端适配策略,共用同一个 Provider 逻辑,但渲染不同的布局。
-
为什么 2026 年大家都在用它?
-
完美契合 Riverpod:Riverpod 的
ref.watch机制让 Presentation 层可以非常优雅地监听 Domain 层的状态变化。 -
代码生成友好:
riverpod_generator和freezed产生的代码可以完美放置在每个 feature 的data和providers目录下,互不干扰。 -
团队协作:开发者 A 负责
cart,开发者 B 负责home,两人修改同一个文件的概率极低,极大减少了 Git 冲突。 -
适配复杂性:通过将 UI 拆分为
_m和_p,可以保持代码整洁。逻辑在controller(或 Provider)里写一次,UI 根据屏幕尺寸选择加载哪个文件。
总结
你所使用的这种结构是 “分层架构”与“功能模块化” 的结合体。它在 2026 年被认为是最利于维护、扩展性最强的设计方案,非常适合你提到的需要兼容 Pad 端、处理国际化和复杂主题的商城 App。
第十八章、异步请求Future FutureOr
Section titled “第十八章、异步请求Future FutureOr”核心区别对比
| 特性 | Future<T> | FutureOr<T> |
|---|---|---|
| 本质 | 一个具体的类,代表异步结果。 | 一个类型别名(Union Type)。 |
| 赋值 | 只能给它 Future 对象。 | 既可以给 String,也可以给 Future<String>。 |
| await | 必须 await 才能拿值。 | 也可以 await(如果是同步值,await 会立即返回)。 |
| 使用位置 | 常用作返回值,表示“这事儿得等”。 | 常用作函数参数或接口定义,表示“等不等都行”。 |
就是使用上没什么区别 FutureOr 虽然可以兼容同步,但是使用的时候你无法区分当前执行的是同步还是异步
@overrideWidget build(BuildContext context, WidgetRef ref) { // 1. 监听异步 Provider final asyncValue = ref.watch(homeControllerProvider);
// 2. 使用 .when 自动处理 异步的 三种状态 return asyncValue.when( data: (products) => Text("拿到数据: ${products.length}"), loading: () => CircularProgressIndicator(), // 加载中 error: (err, stack) => Text("出错: $err"), // 出错 );}关键词 dart中 abstract interface sealed
Section titled “关键词 dart中 abstract interface sealed”Dart 3.0 引入它是为了实现更严谨的接口约束。
-
java:interface 只能被(多)实现, abstract 和 abstract 都不能实例化 ,但是可以被继承和实现抽象方法
-
dart: interface是3.0新增的(更多是语义化作用),有隐式接口:任何类(包括普通类、抽象类)都可以作为接口被
implements。所以interface可以实例化- 如果你
implements一个普通类,你必须重写它所有的字段和方法
- 如果你
-
abstract(抽象类)两者基本一致:都不能实例化,都用于被继承。
- Java:使用
abstract class。只能单继承。 - Dart:使用
abstract class。支持单继承,但 Dart 3 增加了类修饰符(如base,final,sealed)来更精细地控制继承权限。
- Java:使用
这个 sealed class 的设计核心在于:将“成功”和“失败”两种情况强制拆分开,并提供一种安全的方式来处理它们。