在这里插入图片描述
在这里插入图片描述

子玥酱 (掘金 / 知乎 / CSDN / 简书 同名)

大家好,我是 子玥酱,一名长期深耕在一线的前端程序媛 👩‍💻。曾就职于多家知名互联网大厂,目前在某国企负责前端软件研发相关工作,主要聚焦于业务型系统的工程化建设与长期维护。

我持续输出和沉淀前端领域的实战经验,日常关注并分享的技术方向包括 前端工程化、小程序、React / RN、Flutter、跨端方案
在复杂业务落地、组件抽象、性能优化以及多端协作方面积累了大量真实项目经验。

技术方向:前端 / 跨端 / 小程序 / 移动端工程化
内容平台:
掘金、知乎、CSDN、简书
创作特点:
实战导向、源码拆解、少空谈多落地
文章状态:
长期稳定更新,大量原创输出

我的内容主要围绕 前端技术实战、真实业务踩坑总结、框架与方案选型思考、行业趋势解读 展开。文章不会停留在“API 怎么用”,而是更关注为什么这么设计、在什么场景下容易踩坑、真实项目中如何取舍,希望能帮你在实际工作中少走弯路。

子玥酱 · 前端成长记录官 ✨
👋 如果你正在做前端,或准备长期走前端这条路
📚 关注我,第一时间获取前端行业趋势与实践总结
🎁 可领取 11 类前端进阶学习资源(工程化 / 框架 / 跨端 / 面试 / 架构)
💡 一起把技术学“明白”,也用“到位”

持续写作,持续进阶。
愿我们都能在代码和生活里,走得更稳一点 🌱

很多 Flutter 项目在中后期都会出现一种怪象:

  • 状态越来越多
  • Provider 越套越深
  • 但一问「这个状态为什么在这里」,没人说得清

大家会下意识去找:

  • 状态管理方案
  • 架构模式
  • MVVM / Clean / Redux

但真正被忽略的,其实是一个非常 Flutter-specific 的事实

Flutter 中,唯一稳定、可感知、不可伪造的边界,是 UI 树。

不是模块,不是文件夹,也不是你脑子里“逻辑上应该在哪”。

为什么在 Flutter 里,“逻辑边界”经常失效?

在前端或 RN 里,我们习惯用这些东西当边界:

  • 页面
  • 路由
  • 模块
  • store 命名空间

但在 Flutter 中,这些边界都不够硬

页面 ≠ 生命周期边界

Navigator.push(
  context,
  MaterialPageRoute(builder: (_) => PageA()),
);

直觉上你会以为:

PageA pop 掉,PageA 的状态就结束了

但实际上:

  • Provider 可能在更上层
  • State 可能被缓存
  • 页面 Widget 销毁,但状态还活着

页面只是 UI 节点,不是状态边界。

模块 ≠ 作用域

文件结构:

feature/
  ├─ page.dart
  ├─ state.dart
  └─ service.dart

看起来很“干净”,但 Flutter 并不关心你怎么分文件。

只要 Provider 注入在上层:

ChangeNotifierProvider(
  create: (_) => FeatureState(),
  child: App(),
)

这个状态就已经:

  • 脱离了 feature
  • 成为了“全局生命周期的一部分”

模块只是人类视角,不是运行时事实。

Flutter 真正的状态边界,只存在于 Widget 树中

在 Flutter 里,有且只有一件事能决定状态的“生死”:

它挂在 Widget 树的哪一层。

这不是设计理念,而是运行时机制。

状态的生命周期 = Widget 子树的生命周期

class PageA extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Provider(
      create: (_) => PageAState(),
      child: PageAContent(),
    );
  }
}

这里非常清楚:

  • PageAState 的生命周期
  • PageAContent 所在的子树完全一致

一旦这棵子树被移除:

  • state dispose
  • 监听释放
  • 内存回收

这是 Flutter 给你的最硬边界。

一旦脱离 UI 树,状态就失去“自然边界”

final pageAProvider = ChangeNotifierProvider<PageAState>(
  (ref) => PageAState(),
);

然后在 App 顶层注入:

ProviderScope(
  child: MyApp(),
)

此时:

  • PageAState 的生命周期 = App 生命周期
  • 是否还叫 PageAState 已经不重要了
  • 它已经变成“伪全局状态”

但代码里看不出来

为什么 Flutter 项目容易“错判状态边界”?

Flutter 的灵活性,本身就是陷阱

在 Flutter 中:

  • Provider 可以放在任何层级
  • Widget 可以任意组合
  • 状态可以被随意 lift

这带来的副作用是:

你可以写出生命周期完全错位,但运行正常的代码。

而且很长一段时间内都“没问题”。

“方便访问”会压倒“正确归属”

// 放在这里,哪都能用,真方便
final selectedItemProvider = StateProvider<Item?>((ref) => null);

但你很少会问:

  • 这个选中状态应该活多久?
  • 页面切走后是否应该清空?
  • 它是不是和某棵 UI 子树强绑定?

一旦没问,UI 树边界就被绕过了。

UI 树视角下,状态该如何“归位”?

真正成熟的 Flutter 状态设计,往往遵循一个非常朴素的规则:

状态应该尽可能贴着“使用它的 UI 子树”存在。

典型例子:列表页 + 选中态

常见写法:

final selectedIndexProvider = StateProvider<int?>((ref) => null);

UI 树绑定写法:

class ListPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Provider(
      create: (_) => SelectedIndexState(),
      child: ListViewContent(),
    );
  }
}

这样做的直接收益:

  • 页面销毁,状态自然消失
  • 不需要“手动清理”
  • 不存在“状态残留”

再看 Tab 场景

TabBarView(
  children: [
    Provider(
      create: (_) => TabAState(),
      child: TabAPage(),
    ),
    Provider(
      create: (_) => TabBState(),
      child: TabBPage(),
    ),
  ],
)

这在视觉上可能“多写了几行”,但在长期维护上:

  • 状态边界清晰
  • Tab 常驻 ≠ 状态混乱
  • 重构时影响面可控

当你把 UI 树当成边界,很多争论会自动消失

在团队里,常见的争论是:

  • 这个状态算不算全局?
  • 放 Provider 还是放页面?
  • 要不要抽成共享状态?

如果你换一个问题问:

这个状态,属于哪一棵 UI 子树?

答案往往立刻清晰。

总结:Flutter 不是“状态中心化”的系统

最后给一个非常锋利、但非常真实的结论:

Flutter 的架构核心,不是状态管理,而是 UI 树管理。

  • UI 树决定生命周期
  • 生命周期决定状态边界
  • 状态边界决定系统是否可控

一旦你绕开 UI 树去设计状态:

  • 短期灵活
  • 中期混乱
  • 后期几乎不可重构
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐