Files
2026-09-04 15:22:20 +08:00

5.0 KiB
Raw Permalink Blame History

【Demo】Align 对齐布局 + Riverpod 全局用户信息

源文件:lib/pages/demo/align_demo.dart | 分类:基础布局 一句话:把子组件放到父容器任意位置,顺便演示本项目最小化的 Riverpod 状态读取。


一、这个 Demo 解决什么问题

新手写 UI 时常遇到一个需求:"把某个卡片放到盒子的左下角 / 右上角 / 居中"。 Align 就是 Flutter 里的"定位器"——给定一个区域,把子组件按 alignment 摆到指定位置。

本 Demo 额外演示了 Riverpod 全局用户信息的最小用法:

  • 页面改成 ConsumerWidget,用 ref.watch(currentUserProvider) 读全局用户;
  • 登录后任何页面都能拿到用户(用户名 / 班级 / 积分),且数据变化时卡片自动刷新。

对照 Vue3:Align 类似 position: absolute + 容器相对定位; ref.watch 类似 Pinia 的响应式 state —— 一处修改,处处生效。


二、怎么运行 / 演示内容

runApp(const ProviderScope(
  child: MaterialApp(home: AlignDemoPage()),
));

页面主体是一个灰色 300×300 盒子,左下角悬浮一张蓝色"用户信息卡"。 登录后卡片内容会跟着变(未登录显示"未登录")。


三、核心知识点(新手向)

1. Flutter 布局的两条铁律

  • 父组件给子组件传"约束"(Constraints),子组件在约束范围内决定自己的"大小";
  • 所有布局问题(溢出、撑不开)都可以回到这条规则上找原因。

2. Align 的工作方式

  1. Align 会把自身区域扩大到父组件允许的最大范围(默认撑满);
  2. 然后按 alignment 把子组件放到指定位置;
  3. 子组件如果不写宽高,Align 取它的固有尺寸;写了就按写的来。
Align(
  alignment: Alignment.bottomLeft, // 子组件放到父盒子左下角
  child: Container(...),           // 用户信息卡片
)

3. Alignment 坐标系

Alignment(x, y) 的 x、y 范围都是 -1.0 ~ 1.0:

值 位置
(-1, -1) 左上
(0, 0) 居中
(1, 1) 右下
Alignment.centerLeft 左中

常用常量:topLeft / topCenter / topRight / centerLeft / center / centerRight / bottomLeft / bottomCenter / bottomRight。

4. Riverpod 最小用法(本项目统一模式)

class AlignDemoPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final user = ref.watch(currentUserProvider).value; // 订阅全局用户
    // user 为 null = 未登录;登录了就是 CurrentUser 对象
  }
}
  • ref.watch 必须在 build 里调用(声明式依赖,状态变了自动重建);
  • ref.read 只能放回调里(一次性读取,不建立依赖)。

四、关键代码剖析

body: Container(
  width: 300.w,
  height: 300.h,
  color: Colors.grey[300],
  child: Align(
    alignment: Alignment.bottomLeft,
    child: Container(
      width: 240.w,                       // 写了宽高 → 由子组件自己决定
      padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.h),
      decoration: BoxDecoration(
        color: Colors.blue,
        borderRadius: BorderRadius.circular(8.w),
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,   // 卡片高度跟随内容
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('用户:${user?.userName ?? '未登录'}'),
          Text('班级:${user?.className ?? '-'}'),
          Text('积分:${user?.userPoints ?? 0}'),
        ],
      ),
    ),
  ),
),

要点:

  • mainAxisSize: MainAxisSize.min —— Column 不占满父级,高度跟随内容(否则卡片会撑满整个 Align 区域);
  • user?.xxx ?? 默认值 —— 空安全写法:用户为空时显示占位。

五、踩坑记录

  1. 用了 ref 的组件必须继承 ConsumerWidget / ConsumerStatefulWidget,普通 StatelessWidget 拿不到 ref(编译报错)。
  2. ref.watch 放 build 里,ref.read 放回调里,反了会导致不刷新或性能问题。
  3. 加了 .w/.h/.sp 的 widget 不能再用 const —— 尺寸是运行期算的,const 会编译报错(const_eval_extension_method)。
  4. Align 的 alignment 只对没被 Positioned 包裹的子组件生效(Positioned 是 Stack 的专属,见 stack_demo.md)。

六、扩展思考

  • Center 就是 Align(alignment: Alignment.center) 的别名,记住 Align 就记住了 Center;
  • 同族定位组件:Padding(内边距)、FractionallySizedBox(按比例尺寸)、SizedBox(固定尺寸);
  • 想"按百分比偏移"用 Alignment(x, y);想"按像素偏移"把 Align 换成 Padding 或包一层 Transform.translate;
  • Riverpod 进阶:ref.watch(provider).value 是 AsyncValue 的取值方式,三态(loading/error/data)用法见 exam_demo.md 与 docs/exam_architecture.md 的"零、新手前置"章节。