Files
App.K12.Student.Miniapp_Flu…/docs/demos/align_demo.md
T
2026-09-04 15:22:20 +08:00

131 lines
5.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 【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` 的"零、新手前置"章节。