131 lines
5.0 KiB
Markdown
131 lines
5.0 KiB
Markdown
# 【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` 的"零、新手前置"章节。
|