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
+144
View File
@@ -0,0 +1,144 @@
# 【Demo】ListView 列表(懒加载 / 刷新 / 触底加载 / 回顶)
> 源文件:`lib/pages/demo/list_demo.dart` | 分类:基础布局
> 一句话:列表是 App 的骨架 —— 本页把列表的"基础、分隔、下拉刷新、上拉加载、回到顶部"一次讲透。
---
## 一、这个 Demo 解决什么问题
列表占了移动 App 80% 的页面形态。本 Demo 演示列表开发的全套基础:
1. `ListView(children)` vs `ListView.builder` 的区别(重点!);
2. `ListView.separated` 自动分隔线;
3. `RefreshIndicator` 下拉刷新;
4. `ScrollController` 监听触底 → 上拉加载更多;
5. 滚动超过 200px 显示"回到顶部"悬浮按钮。
---
## 二、怎么运行 / 演示内容
```dart
runApp(const MaterialApp(home: ListDemoPage()));
```
- Demo1:一次性渲染的 ListView(公告 + ListTile + for 循环混装);
- Demo2:可下拉刷新、可触底加载更多、带回到顶部按钮的完整列表。
---
## 三、核心知识点(新手向)
### 1. 最核心的区别(面试常问)
| 写法 | 行为 | 适用 |
|---|---|---|
| `ListView(children: [...])` | **一次性创建并渲染全部** item | 少量(<30)或异类组件混装 |
| `ListView.builder` | **懒加载**,只渲染可见项,滚到才创建 | 大量数据(聊天记录、长列表) |
```dart
ListView.builder(
itemCount: _items.length, // 数据总条数(决定滚动范围)
itemBuilder: (context, index) => ListTile(...), // 按下标生成每一行
)
```
### 2. 同族 API
- `ListView.builder`:纯列表,没有分隔线;
- `ListView.separated`:自动插分隔线,多一个 `separatorBuilder`(比在 item 里手动加 Divider 规范)。
### 3. 下拉刷新三件套
```dart
RefreshIndicator(
onRefresh: _onRefresh, // 必须返回 Future(转圈直到 Future 完成)
child: ListView.separated(
physics: const AlwaysScrollableScrollPhysics(), // 内容不满一屏也能下拉!
...
),
)
```
### 4. 触底加载的原理
`ScrollController.addListener` 监听滚动位置:
```dart
if (pos.pixels >= pos.maxScrollExtent - 200) {
_loadMore(); // 距底部 200px 时预加载
}
```
- `pos.pixels`:当前滚了多少;
- `pos.maxScrollExtent`:总共能滚多少;
- `hasClients`:控制器是否已挂载到滚动组件上(没挂载就访问会崩)。
### 5. Dart 3 记录类型(顺带学会)
```dart
List<(int, String)> _items = []; // (编号, 来源标签) 元组
final (num, tag) = _items[index]; // 解构
final newStart = _items.first.$1 + 100; // $1 取第一个字段
```
---
## 四、关键代码剖析
### 防重入的 _loadMore
```dart
Future<void> _loadMore() async {
if (_isLoadingMore || !_hasMore) return; // 防重入:滚动事件会高频触发
_isLoadingMore = true;
setState(() {}); // 先刷新,显示底部 loading
await Future.delayed(const Duration(milliseconds: 800)); // 模拟网络
if (!mounted) return; // 异步后必须判 mounted
setState(() {
_items = [..._items, ...List.generate(10, (i) => (start + i, '加载更多'))];
_page++;
_hasMore = _page < 5; // 模拟:到第 5 页后没有更多
});
_isLoadingMore = false;
}
```
### 回到顶部按钮(Stack + AnimatedOpacity + IgnorePointer)
```dart
Positioned(
right: 8,
bottom: 8,
child: AnimatedOpacity(
opacity: _showBackToTop ? 1 : 0,
duration: const Duration(milliseconds: 200),
child: IgnorePointer(
// opacity: 0 的按钮还能被点到!必须 IgnorePointer 禁用点击
ignoring: !_showBackToTop,
child: FloatingActionButton.small(
heroTag: null, // 避免多个 FAB 的 Hero 动画冲突
onPressed: _scrollToTop,
child: const Icon(Icons.arrow_upward, color: Colors.white),
),
),
),
)
```
`_scrollToTop` = `_scrollController.animateTo(0, duration: 300ms, curve: Curves.easeOut)`。
> 显隐判断用 `if (shouldShow != _showBackToTop)` 而不是直接 setState —— 滚动事件每像素都触发,
> 只有状态真的变了才重建,避免性能浪费。
---
## 五、踩坑记录
1. **ListView 放进 Column/Row 必须给固定高度(SizedBox)或 Expanded**,否则报
`Vertical viewport was given unbounded height` —— 列表是"无限高"的,放在"无限高"的 Column 里矛盾。
2. **RefreshIndicator 必须配 `AlwaysScrollableScrollPhysics()`**,否则内容不满一屏时拉不动。
3. **`ScrollController` 必须在 dispose 释放**(与 TextEditingController 同理,防内存泄漏)。
4. **异步后 setState 前必须 `if (!mounted) return`**——await 期间用户可能已切走页面。
5. 触底判断用 `maxScrollExtent - 200` 提前量,不然在底部"刚好"才加载会有白屏等待感。
---
## 六、扩展思考
- **性能红线**:长列表用 builder,不要用 children + for;1000 条数据一次性渲染会掉帧;
- 数据源接 Riverpod 后,`_items` 换成 `ref.watch(xxxProvider)`,刷新逻辑交给 provider(见 exam 系列);
- 分页的通用模式:`page` 页码 + `hasMore` 标记 + `_isLoadingMore` 防重入,三件套缺一不可;
- 想整页只滚一次(AppBar 折叠 + 列表混排)→ 升级到 [sliver_demo.md](sliver_demo.md) 的 CustomScrollView。