# 【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 —— 一处修改,处处生效。 --- ## 二、怎么运行 / 演示内容 ```dart runApp(const ProviderScope( child: MaterialApp(home: AlignDemoPage()), )); ``` 页面主体是一个灰色 300×300 盒子,左下角悬浮一张蓝色"用户信息卡"。 登录后卡片内容会跟着变(未登录显示"未登录")。 --- ## 三、核心知识点(新手向) ### 1. Flutter 布局的两条铁律 - **父组件给子组件传"约束"(Constraints),子组件在约束范围内决定自己的"大小"**; - 所有布局问题(溢出、撑不开)都可以回到这条规则上找原因。 ### 2. Align 的工作方式 1. Align 会把自身区域**扩大到父组件允许的最大范围**(默认撑满); 2. 然后按 `alignment` 把子组件放到指定位置; 3. 子组件如果不写宽高,Align 取它的**固有尺寸**;写了就按写的来。 ```dart 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 最小用法(本项目统一模式) ```dart 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` 只能放回调里**(一次性读取,不建立依赖)。 --- ## 四、关键代码剖析 ```dart 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](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](exam_demo.md) 与 `docs/exam_architecture.md` 的"零、新手前置"章节。