13 KiB
TODO — Flutter Wisetronic Dart 3 空安全迁移
✅ Web 构建已成功!(2026-08-01)
flutter build web --no-tree-shake-icons→ ✓ Built build/web/(main.dart.js 4.5M)
dart analyze lib: 仅剩 6 error,全部在utils/util_io.dart(原生路径,Web 已排除,见 §5 延后)。- Web 可达错误: 0。
- 峰值 2374 → 6(全部 util_io 原生)。0 语法损坏。分支
dev2。- 构建脚本:
./build_web.sh(构建)/./build_web.sh run(Chrome 运行)。- 关键修复:
cross_fileoverride 到0.3.4+2(旧版 0.3.1+4 用了 Dart3 移除的UnmodifiableUint8ListView;0.3.5+ 需 Dart 3.10)。
0. 快速恢复(Resume 起点)
cd /Users/peima/StudioProjects/flutter_wisetronic
git log --oneline -1 # 确认在最新提交
dart analyze lib | tail -5 # 当前: 仅 6 error(util_io.dart)
./build_web.sh # 构建: ✓ Built build/web/
./build_web.sh run # Chrome 运行调试
# 重跑自动修复脚本(安全,带损坏自检,可随时跑)—— 已无 web 可达错误,通常无需再跑
python3 tools/nullfix.py --max-rounds 12
核心原则:每次改动后用 dart analyze lib | grep -cE '\s+error\s+-' 数错误;脚本若报"检测到语法损坏"会自动中止,git checkout -- lib 回滚。
1. 剩余错误分布(118 个)
| 规则 | 数量 | 说明 |
|---|---|---|
unchecked_use_of_nullable_value |
44 | 可空值当非空用(方法调用/条件/迭代器,多为多行) |
invalid_assignment |
34 | 赋值类型不匹配(复杂表达式) |
argument_type_not_assignable |
22 | 参数类型不匹配(三元/比较/Map字面量) |
return_of_invalid_type_from_closure |
4 | 闭包返回类型 |
undefined_getter |
3 | 多为 .buttonColor(Flutter 3 移除) |
return_of_invalid_type |
3 | 函数 return null 但返回类型非空 |
undefined_named_parameter |
2 | |
undefined_method |
2 | |
creation_with_non_type |
2 | 类名拼错(util_io 内) |
extra_positional_arguments_could_be_named |
1 | |
body_might_complete_normally |
1 |
错误最多的文件(详细见 dart analyze lib):
6 utils/util_io.dart ← 延后(仅原生,Web 构建已排除)
5 widgets/desktop/desktop_edit_address.dart ← .trim() 多行(见 §3,已知修法)
5 widgets/mobile/mobile_edit_address.dart ← 同上
5 widgets/general/add_remove_button.dart
5 widgets/general/parabolic_animation_widget.dart
5 widgets/general/sliding_up_panel.dart
4 widgets/desktop/desktop_me.dart
4 widgets/mobile/mobile_attribute_selection.dart
4 widgets/mobile/mobile_order_detail.dart
3 widgets/desktop/desktop_coupons.dart / desktop_my_support / desktop_order_detail / shop_products
3 widgets/general/animation_point_manager / popup_animation_widget
3 widgets/mobile/mobile_coupons / mobile_me / mobile_my_support / mobile_orders / shopping_cart_bar
2 约 12 个文件(blog/orders/product_item/shopping_cart 等)
1 约 25 个文件(单点错误)
2. 推荐处理顺序
- edit_address(5+5) — 已知修法,见 §3,最快。
- parabolic/popup_animation(5+3) —
late字段已修,可能还有 unchecked_use(offset 计算方法调用),手动看。 - sliding_up_panel(5) — 第三方面板 widget 适配,手动。
- add_remove_button(5) — badges 相关,手动。
- 散落的 2-error / 1-error 文件 — 用
dart analyze lib/<file>逐个看,多为加!。 - util_io.dart(6) — 最后处理(见 §5),不影响 Web 构建。
目标:先清掉 Web 可达错误(flutter build web 的 112),让 Web 能跑;util_io 原生路径可延后。
3. 已知具体修法(直接套用)
3.1 edit_address .trim() 多行(desktop + mobile,各 5 处)
错误:A nullable expression can't be used to invoke 'trim'。代码结构:
validator: (String? value) {
if (value // ← value 是 String?
.trim()
.isEmpty) {
修法:把 if (value 行改成 if (value!。
# 上次脚本 bug:用了 rstrip() 应该用 strip()。正确版:
for p in ['lib/widgets/desktop/desktop_edit_address.dart','lib/widgets/mobile/mobile_edit_address.dart']:
lines=open(p).read().split('\n'); n=0
for i in range(len(lines)):
if lines[i].strip()=='if (value' and lines[i+1].lstrip().startswith('.trim()'):
lines[i]=lines[i].rstrip()+'!'; n+=1 # 注意 rstrip() 保留前导缩进
open(p,'w').write('\n'.join(lines))
(.buttonColor→.colorScheme.primary 已在两个文件修好。)
3.2 通用模式(手动加 ! / ?? 默认 / == true)
String? → String参数:field→field!(确认运行时非空)。num?/int?/double?比较:subtotal > minPrice→subtotal > minPrice!或(minPrice ?? 0)。bool?当条件:if (x)→if (x == true);if (!x)→if (x != true)。return null;但返回类型非空:把函数返回类型加?(如CategoryProducts? foo())。- 局部变量赋值 nullable:声明加
?(如Map<String,dynamic>? x = ...),下游 nullfix 会补!。 - 多行方法调用
.toUpperCase()/.trim():接收者在上一行末尾,在该行末尾加!。
3.3 可复用的定向脚本(一次性,贴进 bash 跑)
a) 条件修复(analyzer 驱动,只修简单 if (chain)) — 已用过,安全:
python3 -c "
import re,subprocess
ERR=re.compile(r'\s+error\s+-\s+([\w/.\-]+):(\d+):(\d+)\s+-\s+(.*?)\s+-\s+([a-z_]+)\s*\$')
pat=re.compile(r'^(\s*if \()(!?)([\w.!\[\]]+)(\)\s*\{?\s*)\$')
out=subprocess.run(['dart','analyze','lib'],capture_output=True,text=True).stdout
tg={}
for ln in out.splitlines():
m=ERR.match(ln)
if m and 'used as a condition' in m.group(4): tg.setdefault('lib/'+m.group(1),set()).add(int(m.group(2)))
n=0
for p,ls in tg.items():
L=open(p).read().split('\n');ch=False
for i,x in enumerate(L,1):
if i in ls:
m=pat.match(x.rstrip('\n'))
if m: L[i-1]=m.group(1)+m.group(3)+(' != true' if m.group(2) else ' == true')+m.group(4);ch=True;n+=1
if ch: open(p,'w').write('\n'.join(L))
print('fixed',n)
"
b) 局部变量改可空(严格类型,已验证安全) — 跑完再 python3 tools/nullfix.py 吃级联:
python3 -c "
import re,subprocess
ERR=re.compile(r'\s+error\s+-\s+([\w/.\-]+):(\d+):(\d+)\s+-\s+(.*?)\s+-\s+([a-z_]+)\s*\$')
NAMEQ=re.compile(r\"'([^']+)'\"); TYPE=r'(?:[A-Z]\w*(?:<[^;]*>)?|int|double|String|bool|num|dynamic|List<[^;]*>|Map<[^;]*>|Set<[^;]*>)'
out=subprocess.run(['dart','analyze','lib'],capture_output=True,text=True).stdout;tg={}
for ln in out.splitlines():
m=ERR.match(ln)
if m and m.group(5)=='not_assigned_potentially_non_nullable_local_variable':
nm=NAMEQ.search(m.group(4))
if nm: tg.setdefault('lib/'+m.group(1),[]).append((int(m.group(2)),nm.group(1)))
dp=re.compile(r'^(\s+(?:final\s+|const\s+)?)('+TYPE+r')\s+(\w+)\s*;\s*\$');n=0
for p,it in tg.items():
L=open(p).read().split('\n');ch=False;done=set()
for ul,nm in it:
if (ul,nm) in done: continue
done.add((ul,nm))
for i in range(ul-2,-1,-1):
m=dp.match(L[i])
if m and m.group(3)==nm and '?' not in m.group(2): L[i]=m.group(1)+m.group(2)+'? '+nm+';';ch=True;n+=1;break
if ch: open(p,'w').write('\n'.join(L))
print('fixed',n)
"
4. 工具脚本说明(tools/)
tools/nullfix.py — 主力自动修复
python3 tools/nullfix.py # 修整个 lib,默认 5 轮
python3 tools/nullfix.py --max-rounds 12
python3 tools/nullfix.py lib/widgets/mobile/shop.dart # 只修单文件
- 每轮重新
dart analyze lib,按 error 的 (file,line,col) 精确定位修改。 - 带腐败自检:每轮后检测
expected_token/missing_identifier,一旦 >0 立即中止并提示git diff。 - 已实现的 handler:
insert_bang:成员访问/方法调用/迭代器/return 加!(按 analyzer 分类)insert_arg_bang:argument_type/invalid_assignment在参数值末尾加!(值必须是简单标识符链[\w.\[\]()!],含运算符/空格跳过)late实例字段、参数空安全、field-formal 字段空安全missing_default_value_for_parameter、@required→required已并入
- 已禁用的危险 handler:基于正则的
if(x)→if(x==true)(曾破坏 89 文件)。条件修复改用 §3.3a 的 analyzer 驱动版。 - 已知边界:多行表达式(接收者跨行)、含空格/逗号的函数调用参数、三元/比较表达式 → 跳过,需手动。
tools/badge_migrate.py — 未验证,已不需要
badges 3.x 迁移已手动完成(badgeStyle/badgeAnimation),此脚本可忽略/删除。
5. 延后项
lib/utils/util_io.dart(6 error,仅原生)
- 通过
dart.library.html条件导入从 Web 构建中排除,不影响flutter build web。 - 错误来自
flutter_local_notificationsv17 API 变更:IOSInitializationSettings/IOSNotificationDetails→ v17 改名(DarwinInitializationSettings/DarwinNotificationDetails)onSelectNotification参数已移除extra_positional_arguments(构造参数变化)body_might_complete_normally(Future<void>缺 return)
- 原生(Android/iOS)构建时才需要修。Web 不阻塞。
6. 关键上下文 / 决策(避免踩坑)
late用于实例字段:避免模型fromJson初始化列表 + State 类的级联错误。模型字段置为可空,调用点加!。- 脚本必须 analyzer 驱动:盲目正则会损坏代码(如
return? item;、this.?field)。nullfix.py用 analyzer 的列/列定位 + 腐败自检是关键。 - badges 实际解析 3.2.0(不是 2.0.3):参数是
badgeStyle:/badgeAnimation:(不是badgeColor/animationType/animation)。 - pinput 实际 5.0.2:类名
Pinput(非PinPut),导入package:pinput/pinput.dart,length/onCompleted/defaultPinDecoration。 - youtube_player_iframe 5.2.2:
YoutubePlayerController.fromVideoId(videoId:, startSeconds:, params:)。 - BreadCrumb.route 已改可空:
String? route,调用点BreadCrumb(X, null)合法。 - e-Transfer:Stripe 已全移除,付款走
payment@wisetronic.com,备注写R#<receipt.id>。 - 搜索框:自写轻量
lib/widgets/general/search_bar.dart(非第三方包)。 - 不升级全局 Flutter SDK:用本地 Flutter 3.29.3 / Dart 3.7.2。
7. 提交历史(基线 f8a90ad 之后)
最新 phase3: mobile/shop clean ... 131->118 ← resume 起点
phase3: checkout files clean ... 142->131
phase3: fix attribute widgets ... ~30 fixed
phase3: FIX arg-bang regex (missing \]) ... 235->183 ← 关键 bug 修复
...(共约 28 个 phase3 提交)
f8a90ad 基线
8. 最终验收
flutter build web # 目标:无 error,生成 build/web
flutter run -d chrome # 目标:能打开、首页/商品/购物车/结账流程跑通
9. 运行时问题(应用已能运行,以下为后续优化,非阻塞)
应用 flutter run -d chrome 已成功启动并加载首页/API 数据/数据库。控制台有以下运行时警告/异常:
9.1 Scrollbar 缺 ScrollPosition(真实,影响滚动条显示)
4 个 Scrollbar( 未配 controller,新版 Flutter 的 Scrollbar 找不到 ScrollPosition:
lib/pages/home.dart:101— ✅ 已修:给SingleChildScrollView加primary: truelib/pages/blog.dart:24— 子是DesktopBlog(自定义组件)lib/pages/download.dart:63— 子是DesktopDownloadApps(自定义组件)lib/widgets/desktop/shop.dart:137— 包裹NotificationListener
后 3 处的子是自定义组件,需让内部 ListView/ScrollView 暴露 controller(设 primary: true)或 Scrollbar 改用 NotificationListener 关联。仅影响滚动条渲染,不影响内容加载/显示。
9.2 CustomersLogo setState-after-dispose(小 bug)
lib/widgets/desktop/desktop_customers_logo.dart 的 Timer.periodic(每 10s 刷新 logo)未在 dispose() 里 cancel(),组件卸载后 timer 仍触发 setState。建议在 dispose 里 timer?.cancel()。热重载时易触发,干净启动偶发。
9.3 _business LateInitializationError(热重载假象)
_business 是店铺页面组件(shop_promote.dart/shop.dart 等)的 late Business 字段。热重载重建 widget 树时字段状态丢失触发,干净启动(浏览器刷新 Cmd+R)不会出现。
验证方式:在浏览器里 Cmd+R 刷新页面(而非热重载),9.2/9.3 类异常应消失,仅剩 9.1 的滚动条警告。