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
+315
View File
@@ -0,0 +1,315 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
/// ============================================================
/// ListView 列表演示页
///
/// 一、两种使用方式:
/// 1. 直接运行本页面:runApp(const MaterialApp(home: ListDemoPage()))
/// 2. 复制某段核心代码到任意 Scaffold 的 body 中。
///
/// 二、核心区别(重点记忆):
/// ListView(children) : 一次性创建并渲染【全部】item,适合少量(<30)或异类组件
/// ListView.builder : 懒加载,只渲染屏幕可见的 item,滑到才创建,适合大量数据
///
/// 三、ListView.builder 必填两个参数:
/// itemCount : 数据总条数(决定可滚动范围)
/// itemBuilder : (context, index) 根据下标生成每一行
///
/// 四、同族 API:
/// ListView.builder : 纯列表,item 之间没有分隔线
/// ListView.separated: item 之间自动插分隔线,需额外传 separatorBuilder
/// (比在 itemBuilder 里手动加 Divider 更规范)
///
/// 五、高频坑:
/// ListView 放进 Column/Row 必须给固定高度(SizedBox)或 Expanded,
/// 否则报 "Vertical viewport was given unbounded height"。
/// ============================================================
class ListDemoPage extends StatelessWidget {
const ListDemoPage({super.key});
// 演示数据(静态常量,可以写进 const 构建里)
static const List<String> _fruits = [
'苹果', '香蕉', '橙子', '葡萄', '西瓜', '草莓', '芒果', '菠萝',
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('ListView 列表 Demo')),
body: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _SectionTitle('Demo1:ListView(children) 一次性全部渲染'),
Text(
'混装不同组件(卡片 + ListTile + for 循环生成),'
'item 全部创建,适合 <30 条',
style: TextStyle(fontSize: 13.sp, color: Color(0xFF888888)),
),
SizedBox(height: 8.h),
SizedBox(
height: 300.h, // 关键:ListView 必须要有确定高度
child: ListView(
children: [
// ① 不同种类的组件直接混装
Container(
margin: EdgeInsets.only(bottom: 8.h),
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: const Color(0xFFE3F2FD),
borderRadius: BorderRadius.circular(8.r),
),
child: const Text(
'顶部公告:今天学习 ListView!',
style: TextStyle(color: Color(0xFF1565C0)),
),
),
const ListTile(
leading: Icon(Icons.home, color: Color(0xFF3B82F6)),
title: Text('首页入口'),
trailing: Icon(Icons.chevron_right),
),
Divider(height: 1.h, indent: 16, endIndent: 16),
// ② children 里直接写 for 循环生成同构条目
for (final fruit in _fruits)
ListTile(
leading: CircleAvatar(
backgroundColor: const Color(0xFFEDF2FF),
child: Text(
fruit.substring(0, 1),
style: const TextStyle(color: Color(0xFF3B82F6)),
),
),
title: Text(fruit),
trailing: const Icon(Icons.chevron_right),
),
],
),
),
SizedBox(height: 32.h),
const _SectionTitle('Demo2:ListView.separated + 下拉刷新'),
Text(
'separated 自动插分隔线 + RefreshIndicator 下拉刷新,'
'下拉后新数据插到顶部',
style: TextStyle(fontSize: 13.sp, color: Color(0xFF888888)),
),
SizedBox(height: 8.h),
const _RefreshableList(),
],
),
),
);
}
}
/// 区块标题
class _SectionTitle extends StatelessWidget {
const _SectionTitle(this.title);
final String title;
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(bottom: 4.h),
child: Text(
title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
color: Color(0xFF333333),
),
),
);
}
}
/// 下拉刷新 + 上拉加载列表 —— StatefulWidget 版本
/// 关键点:
/// 1. RefreshIndicator 包住可滚动组件,onRefresh 必须返回 Future;
/// 2. physics: AlwaysScrollableScrollPhysics() 内容不满一屏也能下拉;
/// 3. ScrollController 监听滚动位置,触底前 200px 触发 _loadMore;
/// 4. ScrollController 必须在 dispose() 里释放,防内存泄漏;
/// 5. _isLoadingMore 防重入(滚动事件会高频触发)、_hasMore 判断是否还有下一页;
/// 6. 数据项用 Dart 3 记录类型 (编号, 来源),区分初始/刷新/加载更多。
class _RefreshableList extends StatefulWidget {
const _RefreshableList();
@override
State<_RefreshableList> createState() => _RefreshableListState();
}
class _RefreshableListState extends State<_RefreshableList> {
// 滚动控制器:监听触底
final ScrollController _scrollController = ScrollController();
// 数据源:Dart3 记录 (编号, 来源标签)
List<(int, String)> _items = [];
int _page = 1; // 当前页码(模拟分页)
bool _hasMore = true; // 是否还有下一页
bool _isLoadingMore = false; // 防止滚动事件重复触发加载
bool _showBackToTop = false; // 是否显示"回到顶部"按钮
@override
void initState() {
super.initState();
_items = List.generate(10, (i) => (i + 1, '初始数据'));
_scrollController.addListener(_onScroll);
}
@override
void dispose() {
_scrollController.dispose(); // 必须释放,防内存泄漏
super.dispose();
}
/// 滚动监听:触底加载 + 控制"回到顶部"按钮显隐
void _onScroll() {
if (!_scrollController.hasClients) return; // hasClients:这个控制器有没有真正挂载到滚动组件上。
final pos = _scrollController.position;
// 触底前 200px 加载下一页
if (pos.pixels >= pos.maxScrollExtent - 200) {
_loadMore();
}
// 滚过 200px 才显示"回到顶部"按钮(用 != 对比,避免每像素都 setState)
final shouldShow = pos.pixels > 200;
if (shouldShow != _showBackToTop) {
setState(() => _showBackToTop = shouldShow);
}
}
/// 平滑滚回顶部
void _scrollToTop() {
_scrollController.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
/// 下拉刷新:把新数据插到列表最前面
Future<void> _onRefresh() async {
// 模拟网络请求延迟 1 秒
await Future.delayed(const Duration(seconds: 1));
if (!mounted) return; // 页面可能已被销毁
// await 1 秒后页面可能已被用户切走,此时 context 已失效,直接操作会崩。 if (!mounted) return; 是异步回调的通用兜底。
setState(() {
// 刷新:模拟从服务端拉到新数据,插到列表最前面
final newStart = _items.first.$1 + 100;
_items = [
...List.generate(5, (i) => (newStart + i, '刷新新增')),
..._items,
];
});
// 刷新完成提示
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('刷新成功,新增了 5 条数据')),
);
}
/// 触底加载下一页:追加到列表末尾
Future<void> _loadMore() async {
if (_isLoadingMore || !_hasMore) return; // 防重入
_isLoadingMore = true;
setState(() {}); // 先刷新,显示底部 loading
await Future.delayed(const Duration(milliseconds: 800)); // 模拟网络
if (!mounted) return;
setState(() {
final start = _items.last.$1 + 1;
_items = [..._items, ...List.generate(10, (i) => (start + i, '加载更多'))];
_page++;
_hasMore = _page < 5; // 模拟:到第 5 页后没有更多数据
});
_isLoadingMore = false;
}
/// 列表末尾的 footer:加载中 / 没有更多
Widget _buildFooter() {
return Padding(
padding: EdgeInsets.symmetric(vertical: 16.h),
child: Center(
child: _hasMore
? SizedBox(
width: 20.w,
height: 20.h,
child: const CircularProgressIndicator(strokeWidth: 2),
)
: Text(
'— 没有更多了 —',
style: TextStyle(fontSize: 12.sp, color: Color(0xFF999999)),
),
),
);
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
SizedBox(
height: 300.h, // 固定高度,否则放 Column 里会报 unbounded height
child: RefreshIndicator(
onRefresh: _onRefresh,
child: ListView.separated(
// 关键:内容不满一屏时也能触发下拉手势
physics: const AlwaysScrollableScrollPhysics(),
controller: _scrollController,
itemCount: _items.length + 1, // 多出的一行是 footer
separatorBuilder: (_, i) =>
Divider(height: 1.h, indent: 16, endIndent: 16),
itemBuilder: (context, index) {
// 最后一行渲染 footer
if (index == _items.length) return _buildFooter();
final (num, tag) = _items[index]; // 记录解构
// 按来源标签决定颜色(Dart3 switch 表达式)
final Color bg = switch (tag) {
'刷新新增' => const Color(0xFFE8F5E9),
'加载更多' => const Color(0xFFE3F2FD),
_ => const Color(0xFFFFF3E0),
};
final Color fg = switch (tag) {
'刷新新增' => const Color(0xFF2E7D32),
'加载更多' => const Color(0xFF1565C0),
_ => const Color(0xFFE65100),
};
return ListTile(
leading: CircleAvatar(
backgroundColor: bg,
child: Text('$num', style: TextStyle(color: fg)),
),
title: Text('数据 #$num'),
subtitle: Text(tag),
);
},
),
),
),
// 回到顶部悬浮按钮:Stack 定位在列表右下角
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 动画冲突
backgroundColor: const Color(0xFF3B82F6),
onPressed: _scrollToTop,
child: const Icon(Icons.arrow_upward, color: Colors.white),
),
),
),
),
],
);
}
}