Flutter 使用 Dart 描述 Widget 树,由框架与渲染引擎协作完成界面。它的 Web 输出不等同于手写 HTML/CSS,移动端也不等同于把网页装进浏览器。共享业务与 UI 代码仍需处理平台能力、输入、可访问性和发布差异。

先掌握需要的 Dart

Dart 有静态类型与空安全。String? 允许 null;在确认非空后再使用,不能为了通过编译到处加 !。final 绑定不能重新赋值,对象内部是否可变取决于类型。List、Map 和泛型表达集合关系,Future 表达异步结果,await 不会把 CPU 工作自动移到另一个 isolate。

class Lesson {
  const Lesson(this.id, this.title, {this.done = false});
  final String id;
  final String title;
  final bool done;

  Lesson withDone(bool value) => Lesson(id, title, done: value);
}

这个不可变模型让更新边界更清楚。const 构造允许符合条件的编译期常量,但并不意味着整个应用从此没有重建成本。

三棵树与状态位置

Widget 是不可变配置;Element 维护树中的实例关系;RenderObject 负责部分布局、绘制等工作。build 可多次调用,应便宜并避免副作用。StatefulWidget 本身仍是不可变配置,变化保存在对应 State 中。

setState 同步更新 State 并请求重建,不需要包住每一个普通变量赋值。共享状态可提升到共同祖先,规模增长后再按需求采用 ChangeNotifier 或其他方案。Key 帮助框架匹配元素身份,列表用稳定业务 ID,不能随机变化。

约束决定尺寸

父级向下传约束,子级向上返回尺寸,父级决定子级位置。Column 中的长列表通常需要 Expanded 等提供有界空间;把 ListView 放进无界高度而不处理约束,是常见报错来源。滚动布局应结合数据量和嵌套关系设计,不能一遇到问题就加 shrinkWrap。

Column(
  children: [
    const Text('学习记录'),
    Expanded(
      child: ListView.builder(
        itemCount: lessons.length,
        itemBuilder: (context, index) => Text(lessons[index].title),
      ),
    ),
  ],
)

这是布局片段,lessons 由页面提供。长列表优先按需构建,复杂高度与滚动状态要测试真实内容。

生命周期与平台边界

TextEditingController、AnimationController、订阅等应在不再使用时 dispose 或取消。异步完成后若要更新 UI,确认 State 仍 mounted;这只能避免已卸载对象更新,不能解决旧请求覆盖新请求,需要独立的顺序保护。BuildContext 属于树中位置,跨异步使用时同样检查有效性。

热重载方便迭代,但不能代替完整重启、生产构建和平台验证。平台插件可能依赖原生配置、权限和版本;Web 上能运行不代表 Android/iOS 行为已验证。继续完成Flutter 记录板。

练习:解释为什么 Expanded 不能随意放进无界滚动区。给模型加预计时长和校验,再说明空安全为什么不能自动证明 JSON 合法。

面试自查:Widget 和 State 分别保存什么?build 为什么不能当作只运行一次的初始化函数?为什么列表需要稳定 Key,为什么 Future 完成后仍要确认 mounted?

参考:Dart 语言、Flutter 架构、布局约束、状态管理入门。