flutter架 构项目基础

This commit is contained in:
崔宝新
2026-09-04 15:22:20 +08:00
parent 70967229d0
commit 0c5aee59c0
265 changed files with 35484 additions and 0 deletions
+130
View File
@@ -0,0 +1,130 @@
# 【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` 的"零、新手前置"章节。