SingleChildScrollView
固定数量的列表且不超过20行。否则用ListView.他会一次性加载所有的widget。
less
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(const MaterialApp(home: HomePage()));
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('TextField 全方位知识点')),
body: SingleChildScrollView(
padding: EdgeInsets.all(20),
child: Column(
children: List.generate(100, (i) {
return Container(
margin: EdgeInsets.only(top: 20),
height: 50,
alignment: Alignment.center,
width: double.infinity,
color: Colors.amber,
child: Text("第${i + 1}个"),
);
}),
),
),
);
}
}
SingleChildScrollView
less
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(const MaterialApp(home: HomePage()));
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('SingleChildScrollView 全方位知识点')),
body: ListView(
padding: const EdgeInsets.all(16),
children: const [
_Section1_Essence(),
_Section2_ListView(),
_Section3_ScrollDirection(),
_Section4_Padding(),
_Section5_Reverse(),
_Section6_Physics(),
_Section7_Controller(),
_Section8_ShrinkWrap(),
_Section9_KeyboardDismissBehavior(),
_Section10_ClipBehavior(),
_Section11_NestedScroll(),
_Section12_Summary(),
],
),
);
}
}
// ────────────────────────────────────────────────────────────────
// ① 本质 ------ 只有一个 child 的可滚动 Widget
// ────────────────────────────────────────────────────────────────
class _Section1_Essence extends StatelessWidget {
const _Section1_Essence();
@override
Widget build(BuildContext context) {
return _Card(
title: '① SingleChildScrollView 本质',
subtitle: '只有一个 child 的可滚动 Widget,vs ListView',
children: const [
Text(
'SingleChildScrollView 核心属性:\n'
'• Widget child 唯一的子组件\n'
'• Axis scrollDirection 滚动方向(vertical/horizontal)\n'
'• EdgeInsetsGeometry? padding 内边距\n'
'• bool reverse 是否反向滚动\n'
'• bool? primary 是否使用 PrimaryScrollController\n'
'• ScrollPhysics? physics 滚动物理效果\n'
'• ScrollController? controller 滚动控制器\n'
'• Clip clipBehavior 超出裁剪\n'
'• bool dragStartBehavior 拖拽起始行为\n'
'• ScrollKeyboardDismissBehavior? keyboardDismissBehavior\n'
'• String? restorationId 恢复标识',
style: TextStyle(fontSize: 11, fontFamily: 'monospace'),
),
SizedBox(height: 6),
Text('核心特点:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11)),
Text(
'• 只有一个 child(通常放 Column / Row)\n'
'• child 尺寸不受滚动物理限制 → 可以很长\n'
'• 子组件一次性全部构建 → 长列表性能差\n'
'• 适合:子组件数量少且数量固定的场景\n'
'• 不适合:大量子组件(用 ListView.builder)',
style: TextStyle(fontSize: 11),
),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ② vs ListView ------ 什么时候用哪个
// ────────────────────────────────────────────────────────────────
class _Section2_ListView extends StatelessWidget {
const _Section2_ListView();
@override
Widget build(BuildContext context) {
return _Card(
title: '② SingleChildScrollView vs ListView',
subtitle: '什么时候用哪个?核心区别:是否懒加载',
children: [
Row(
children: [
Expanded(
child: Column(
children: [
SizedBox(
height: 100,
child: SingleChildScrollView(
child: Column(
children: List.generate(100, (i) => Container(
height: 20,
margin: const EdgeInsets.all(1),
color: Colors.amber,
alignment: Alignment.center,
child: Text('${i + 1}', style: const TextStyle(fontSize: 10)),
)),
),
),
),
const SizedBox(height: 2),
const Text('SingleChildScrollView\n100 个全部同时构建',
style: TextStyle(fontSize: 9)),
],
),
),
const SizedBox(width: 8),
Expanded(
child: Column(
children: [
SizedBox(
height: 100,
child: ListView.builder(
itemCount: 100,
itemBuilder: (_, i) => Container(
height: 20,
margin: const EdgeInsets.all(1),
color: Colors.blue,
alignment: Alignment.center,
child: Text('${i + 1}', style: const TextStyle(fontSize: 10, color: Colors.white)),
),
),
),
const SizedBox(height: 2),
const Text('ListView.builder\n按需懒加载(只构建可见的)',
style: TextStyle(fontSize: 9)),
],
),
),
],
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
color: Colors.amber.shade50,
child: const Text(
'选择标准:\n\n'
'✅ 用 SingleChildScrollView:\n'
'• 表单页面(子组件数量固定且少)\n'
'• 详情页面(头部 + 正文 + 底部)\n'
'• 子组件需要整体交互的场景\n\n'
'✅ 用 ListView:\n'
'• 列表数据很多(> 20 条)\n'
'• 数据是动态的(网络请求回来的)\n'
'• 每个 item 结构相似\n\n'
'❌ 绝对不要用 SingleChildScrollView 装几百条数据!\n'
' 会一次性构建全部 Widget → 卡顿 + 占内存',
style: TextStyle(fontSize: 11),
),
),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ③ scrollDirection ------ 水平 vs 垂直
// ────────────────────────────────────────────────────────────────
class _Section3_ScrollDirection extends StatelessWidget {
const _Section3_ScrollDirection();
@override
Widget build(BuildContext context) {
return _Card(
title: '③ scrollDirection',
subtitle: 'Axis.vertical(默认)/ Axis.horizontal',
children: [
const Text('垂直滚动(默认):', style: TextStyle(fontSize: 11)),
const SizedBox(height: 4),
SizedBox(
height: 80,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Column(
children: List.generate(10, (i) => Container(
height: 25,
margin: const EdgeInsets.symmetric(vertical: 1),
color: Colors.amber,
alignment: Alignment.center,
child: Text('item ${i + 1}', style: const TextStyle(fontSize: 10)),
)),
),
),
),
const SizedBox(height: 8),
const Text('水平滚动:', style: TextStyle(fontSize: 11)),
const SizedBox(height: 4),
SizedBox(
height: 60,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(15, (i) => Container(
width: 50,
height: 50,
margin: const EdgeInsets.symmetric(horizontal: 2),
color: Colors.blue,
alignment: Alignment.center,
child: Text('${i + 1}', style: const TextStyle(color: Colors.white, fontSize: 11)),
)),
),
),
),
const SizedBox(height: 8),
const Text('注意:水平滚动时 child 必须是 Row,\n'
'且 Row 的 width 没有被约束(不能用 Expanded/Flexible 包裹 Row)',
style: TextStyle(fontSize: 11)),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ④ padding ------ 内边距(配合 Scaffold 的 safeArea)
// ────────────────────────────────────────────────────────────────
class _Section4_Padding extends StatelessWidget {
const _Section4_Padding();
@override
Widget build(BuildContext context) {
return _Card(
title: '④ padding',
subtitle: 'SingleChildScrollView 的 padding 在滚动内容的"内部"',
children: const [
Text('padding 和外层 Padding 的区别:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11)),
Text(
'SingleChildScrollView(\n'
' padding: EdgeInsets.all(16), // ← 在 Scrollable 内部\n'
' child: Column(...),\n'
')\n\n'
'// 等价于:\n'
'SingleChildScrollView(\n'
' child: Padding(\n'
' padding: EdgeInsets.all(16),\n'
' child: Column(...),\n'
' ),\n'
')\n\n'
'// 推荐 SingleChildScrollView 直接设 padding,更简洁。',
style: TextStyle(fontSize: 11, fontFamily: 'monospace'),
),
SizedBox(height: 6),
Text('⚠️ Scaffold 已经有 body padding 了(MediaQuery 会处理状态栏),\n'
'如果 Scaffold 里再包一层 SafeArea,通常不需要再手动加 padding',
style: TextStyle(fontSize: 11, color: Colors.orange),
),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑤ reverse ------ 反向滚动(聊天列表常用)
// ────────────────────────────────────────────────────────────────
class _Section5_Reverse extends StatelessWidget {
const _Section5_Reverse();
@override
Widget build(BuildContext context) {
return _Card(
title: '⑤ reverse',
subtitle: '反向滚动,聊天列表、评论区倒序常用',
children: [
Row(
children: [
Expanded(
child: Column(
children: [
SizedBox(
height: 100,
child: SingleChildScrollView(
reverse: false,
child: Column(
children: List.generate(5, (i) => Container(
height: 20,
margin: const EdgeInsets.symmetric(vertical: 2),
color: Colors.amber,
alignment: Alignment.center,
child: Text('item ${i + 1}', style: const TextStyle(fontSize: 10)),
)),
),
),
),
const SizedBox(height: 2),
const Text('reverse: false(默认)', style: TextStyle(fontSize: 10)),
],
),
),
const SizedBox(width: 8),
Expanded(
child: Column(
children: [
SizedBox(
height: 100,
child: SingleChildScrollView(
reverse: true,
child: Column(
children: List.generate(5, (i) => Container(
height: 20,
margin: const EdgeInsets.symmetric(vertical: 2),
color: Colors.blue,
alignment: Alignment.center,
child: Text('item ${i + 1}', style: const TextStyle(fontSize: 10, color: Colors.white)),
)),
),
),
),
const SizedBox(height: 2),
const Text('reverse: true(从底部开始)', style: TextStyle(fontSize: 10)),
],
),
),
],
),
const SizedBox(height: 8),
const Text('聊天列表反向滚动的典型用法:\n'
'reverse: true → 消息从底部显示\n'
'controller.jumpTo(controller.maxScrollExtent) → 新消息滚到底',
style: TextStyle(fontSize: 11)),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑥ physics ------ 滚动物理效果
// ────────────────────────────────────────────────────────────────
class _Section6_Physics extends StatelessWidget {
const _Section6_Physics();
@override
Widget build(BuildContext context) {
return _Card(
title: '⑥ physics',
subtitle: '滚动物理效果决定了滚动到边缘的行为',
children: [
Row(
children: [
Expanded(
child: Column(
children: [
SizedBox(
height: 80,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
children: List.generate(3, (i) => Container(
height: 25,
margin: const EdgeInsets.symmetric(vertical: 2),
color: Colors.amber,
alignment: Alignment.center,
child: Text('$i', style: const TextStyle(fontSize: 10)),
)),
),
),
),
const SizedBox(height: 2),
const Text('AlwaysScrollableScrollPhysics\n(内容少也能拉动)', style: TextStyle(fontSize: 9)),
],
),
),
const SizedBox(width: 8),
Expanded(
child: Column(
children: [
SizedBox(
height: 80,
child: SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: Column(
children: List.generate(3, (i) => Container(
height: 25,
margin: const EdgeInsets.symmetric(vertical: 2),
color: Colors.blue,
alignment: Alignment.center,
child: Text('$i', style: const TextStyle(fontSize: 10, color: Colors.white)),
)),
),
),
),
const SizedBox(height: 2),
const Text('ClampingScrollPhysics\n(Android 默认:不回弹)', style: TextStyle(fontSize: 9)),
],
),
),
const SizedBox(width: 8),
Expanded(
child: Column(
children: [
SizedBox(
height: 80,
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
children: List.generate(3, (i) => Container(
height: 25,
margin: const EdgeInsets.symmetric(vertical: 2),
color: Colors.green,
alignment: Alignment.center,
child: Text('$i', style: const TextStyle(fontSize: 10, color: Colors.white)),
)),
),
),
),
const SizedBox(height: 2),
const Text('BouncingScrollPhysics\n(iOS 默认:回弹)', style: TextStyle(fontSize: 9)),
],
),
),
],
),
const SizedBox(height: 8),
const Text('四种 ScrollPhysics:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11)),
const Text(
'• AlwaysScrollableScrollPhysics 内容很少也能拉动(默认 behavior 包一层)\n'
'• BouncingScrollPhysics iOS 风格:到边缘有回弹动画\n'
'• ClampingScrollPhysics Android 风格:到边缘有夹手感\n'
'• NeverScrollableScrollPhysics 完全禁止滚动\n\n'
'平台默认值:\n'
'Android → ClampingScrollPhysics\n'
'iOS → BouncingScrollPhysics\n\n'
'想用 iOS 风格:physics: const BouncingScrollPhysics()',
style: TextStyle(fontSize: 11),
),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑦ controller ------ ScrollController 监听滚动位置
// ────────────────────────────────────────────────────────────────
class _Section7_Controller extends StatefulWidget {
const _Section7_Controller();
@override
State<_Section7_Controller> createState() => _Section7_ControllerState();
}
class _Section7_ControllerState extends State<_Section7_Controller> {
final _controller = ScrollController();
double _offset = 0;
@override
void initState() {
super.initState();
_controller.addListener(() {
if (mounted) setState(() => _offset = _controller.offset);
});
}
@override
void dispose() {
_controller.dispose(); // ⚠️ 必须释放
super.dispose();
}
@override
Widget build(BuildContext context) {
return _Card(
title: '⑦ ScrollController',
subtitle: '监听滚动位置、跳转到指定位置',
children: [
Text('当前滚动位置: ${_offset.toStringAsFixed(1)}',
style: const TextStyle(fontSize: 12)),
const SizedBox(height: 4),
SizedBox(
height: 100,
child: SingleChildScrollView(
controller: _controller,
child: Column(
children: List.generate(20, (i) => Container(
height: 20,
margin: const EdgeInsets.symmetric(vertical: 1),
color: Colors.amber,
alignment: Alignment.center,
child: Text('item ${i + 1}', style: const TextStyle(fontSize: 10)),
)),
),
),
),
const SizedBox(height: 6),
Row(
children: [
ElevatedButton(
onPressed: () => _controller.animateTo(
50,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
),
child: const Text('滚到 50'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => _controller.jumpTo(200),
child: const Text('跳到 200'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {
_controller.animateTo(
_controller.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
},
child: const Text('滚到底'),
),
],
),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.all(8),
color: Colors.amber.shade50,
child: const Text(
'ScrollController 核心属性:\n'
'• offset 当前滚动位置\n'
'• position 当前 ScrollPosition\n'
'• initialScrollOffset 初始位置\n'
'• hasClients 是否被 Scrollable 监听\n'
'• maxScrollExtent 最大滚动距离\n\n'
'// 3 种跳转方式:\n'
'controller.jumpTo(100); // 立即跳\n'
'controller.animateTo(100, duration: ...); // 带动画跳\n'
'controller.position.maxScrollExtent // 滚到底\n\n'
'// ⚠️ 必须在 dispose 中释放!\n'
'@override\n'
'void dispose() {\n'
' controller.dispose();\n'
' super.dispose();\n'
'}',
style: TextStyle(fontSize: 10.5, fontFamily: 'monospace'),
),
),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑧ shrinkWrap ------ 配合 Column
// ────────────────────────────────────────────────────────────────
class _Section8_ShrinkWrap extends StatelessWidget {
const _Section8_ShrinkWrap();
@override
Widget build(BuildContext context) {
return _Card(
title: '⑧ shrinkWrap',
subtitle: 'SingleChildScrollView 的 child 里放 Row/Column 要注意',
children: const [
Text('问题场景:SingleChildScrollView(horizontal) 里放 Row',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11)),
Text(
'SingleChildScrollView(\n'
' scrollDirection: Axis.horizontal,\n'
' child: Row(\n'
' children: [ ... ], // ← 如果 children 总宽超出屏幕?\n'
' ),\n'
')\n\n'
'Row 默认会被父级约束撑开(minWidth = maxWidth = 屏幕宽)\n'
'→ Row 无法根据 children 自适应宽度\n'
'→ SingleChildScrollView 也拿不到足够的宽度\n\n'
'✅ 解决方案:\n'
'SingleChildScrollView(\n'
' scrollDirection: Axis.horizontal,\n'
' child: Row(\n'
' mainAxisSize: MainAxisSize.min, // ← Row 自适应\n'
' children: [...],\n'
' ),\n'
')\n\n'
'⚠️ SingleChildScrollView 本身没有 shrinkWrap 属性!\n'
'(那是 ListView 的)\n'
'需要改 child 内部组件的 mainAxisSize',
style: TextStyle(fontSize: 11),
),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑨ keyboardDismissBehavior ------ 键盘弹出时的行为
// ────────────────────────────────────────────────────────────────
class _Section9_KeyboardDismissBehavior extends StatelessWidget {
const _Section9_KeyboardDismissBehavior();
@override
Widget build(BuildContext context) {
return _Card(
title: '⑨ keyboardDismissBehavior',
subtitle: '点击滚动区域时,键盘是否收起',
children: [
const Text(
'三种行为:\n\n'
'• ScrollKeyboardDismissBehavior.onDrag\n'
' 拖动时收起键盘(默认)\n\n'
'• ScrollKeyboardDismissBehavior.manual\n'
' 手动调用 FocusManager.instance.primaryFocus?.unfocus()\n\n'
'• 未设置(默认值 onDrag)\n\n'
'典型场景:表单页面放 SingleChildScrollView\n'
'想让用户点击空白区域就收起键盘:',
style: TextStyle(fontSize: 11),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.all(8),
color: Colors.amber.shade50,
child: const Text(
'SingleChildScrollView(\n'
' keyboardDismissBehavior: ScrollKeyboardDismissBehavior.onDrag,\n'
' child: Column(\n'
' children: [ ... , TextField() ],\n'
' ),\n'
')',
style: TextStyle(fontSize: 11, fontFamily: 'monospace'),
),
),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑩ clipBehavior ------ 超出裁剪
// ────────────────────────────────────────────────────────────────
class _Section10_ClipBehavior extends StatelessWidget {
const _Section10_ClipBehavior();
@override
Widget build(BuildContext context) {
return _Card(
title: '⑩ clipBehavior',
subtitle: '滚动时超出可视区域的子组件是否裁剪',
children: [
Row(
children: [
Expanded(child: _ClipDemo(label: 'hardEdge(默认)', clip: Clip.hardEdge)),
const SizedBox(width: 8),
Expanded(child: _ClipDemo(label: 'none', clip: Clip.none)),
],
),
const SizedBox(height: 8),
const Text('默认 Clip.hardEdge:性能好,无抗锯齿\n'
'需要圆角裁剪时用 Clip.antiAlias\n'
'通常保持默认即可',
style: TextStyle(fontSize: 11)),
],
);
}
}
class _ClipDemo extends StatelessWidget {
final String label;
final Clip clip;
const _ClipDemo({required this.label, required this.clip});
@override
Widget build(BuildContext context) {
return Column(
children: [
SizedBox(
height: 80,
child: SingleChildScrollView(
clipBehavior: clip,
child: Column(
children: [
Container(
width: 300,
height: 40,
color: Colors.red,
),
Transform.translate(
offset: const Offset(-20, 0),
child: Container(
width: 300,
height: 40,
color: Colors.blue,
),
),
],
),
),
),
const SizedBox(height: 2),
Text(label, style: const TextStyle(fontSize: 10)),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑪ 嵌套滚动 ------ NestedScrollView vs 嵌套 SingleChildScrollView
// ────────────────────────────────────────────────────────────────
class _Section11_NestedScroll extends StatelessWidget {
const _Section11_NestedScroll();
@override
Widget build(BuildContext context) {
return _Card(
title: '⑪ 嵌套滚动',
subtitle: '父子滚动方向相同时,需要 CustomScrollView / NestedScrollView',
children: const [
Text('⚠️ 问题:父 SingleChildScrollView(vertical) 嵌套\n'
'子 SingleChildScrollView(vertical),手势会冲突!\n\n'
'Flutter 不鼓励直接嵌套两个同方向的滚动组件。',
style: TextStyle(fontSize: 11, color: Colors.red)),
SizedBox(height: 6),
Text('✅ 解决方案:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11)),
Text(
'1. 用 CustomScrollView + SliverToBoxAdapter\n'
' 把所有子内容都放进 CustomScrollView 的 slivers 里\n\n'
'2. 如果是不同方向(父 vertical,子 horizontal)\n'
' 可以直接嵌套,Flutter 会自动判断手势方向\n\n'
'3. 如果一定要嵌套同方向\n'
' 可以用 NeverScrollableScrollPhysics() 禁用内层\n'
' 让外层统一滚动\n\n'
'// 方案 3 示例:\n'
'SingleChildScrollView(\n'
' child: Column(\n'
' children: [\n'
' Container(...),\n'
' SingleChildScrollView(\n'
' physics: NeverScrollableScrollPhysics(), // ← 禁用内层\n'
' child: Column(children: [...]),\n'
' ),\n'
' ],\n'
' ),\n'
')',
style: TextStyle(fontSize: 11),
),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑫ 完整总结
// ────────────────────────────────────────────────────────────────
class _Section12_Summary extends StatelessWidget {
const _Section12_Summary();
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.brown.shade50,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'📌 SingleChildScrollView 完整总结\n\n'
'1. 本质:只有一个 child 的可滚动 Widget\n'
' 与 ListView 的核心区别:是否懒加载\n\n'
'2. 七大属性:\n'
' scrollDirection / reverse / padding\n'
' physics / controller / clipBehavior\n'
' keyboardDismissBehavior\n\n'
'3. physics 四种:\n'
' AlwaysScrollable / Bouncing(iOS 回弹)\n'
' Clamping(Android 夹手)/ NeverScrollable\n\n'
'4. ⚠️ 必做:controller.dispose()\n\n'
'5. 嵌套滚动:\n'
' • 同方向嵌套 → NestedScrollView 或 CustomScrollView\n'
' • 不同方向嵌套 → 直接嵌套\n'
' • 同方向内层禁用 → NeverScrollableScrollPhysics\n\n'
'6. 横向 ScrollView 里放 Row:\n'
' Row 要加 mainAxisSize: MainAxisSize.min\n\n'
'7. 选择建议:\n'
' 少量固定子组件 → SingleChildScrollView\n'
' 大量动态列表 → ListView.builder\n'
' 复杂视差滚动 → CustomScrollView',
style: TextStyle(fontSize: 12),
),
);
}
}
// ────────────────────────────────────────────────────────────────
// 通用卡片组件
// ────────────────────────────────────────────────────────────────
class _Card extends StatelessWidget {
final String title;
final String subtitle;
final List<Widget> children;
const _Card({required this.title, this.subtitle = '', required this.children});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey.shade300),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
if (subtitle.isNotEmpty) ...[
const SizedBox(height: 2),
Text(subtitle,
style: TextStyle(fontSize: 11, color: Colors.grey.shade600, fontStyle: FontStyle.italic)),
],
const SizedBox(height: 10),
...children,
],
),
);
}
}
ListView
非懒加载的模式,会一次性构建所有的widget
less
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(const MaterialApp(home: HomePage()));
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('ListView 全方位知识点')),
body: ListView(
children: List.generate(100, (index){
print(index);
return Container(
margin: EdgeInsets.only(top: 10),
color: Colors.amber,
alignment: Alignment.center,
child: Text("这是第$index个组件",style: TextStyle(color: Colors.white),),
);
}),
),
);
}
}
ListView 懒加载
适合长度很长,或者不固定的场景。
cacheExtent 如果不设置,默认会多加载当前屏幕上面和下面不显示的250px的内容。用于加载的时候不出现白屏。
php
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(const MaterialApp(home: HomePage()));
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('ListView 全方位知识点')),
body: ListView.builder(
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
print(index);
return Container(
margin: EdgeInsets.only(top: 10),
color: Colors.amber,
alignment: Alignment.center,
child: Text("这是第$index个组件", style: TextStyle(color: Colors.white)),
);
},
),
);
}
}
ListView.Seprate
多了分割线的能力,也是懒加载
php
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(const MaterialApp(home: HomePage()));
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('ListView 全方位知识点')),
body: ListView.separated(
itemCount: 100,
separatorBuilder: (BuildContext context, int index){
return Container(
color: Colors.green,
height: 100,
);
},
itemBuilder: (BuildContext context, int index) {
print(index);
return Container(
margin: EdgeInsets.only(top: 10),
color: Colors.amber,
alignment: Alignment.center,
child: Text("这是第$index个组件", style: TextStyle(color: Colors.white)),
);
},
),
);
}
}
ListView进阶
less
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(const MaterialApp(home: HomePage()));
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('ListView 全方位知识点')),
body: ListView(
padding: const EdgeInsets.all(16),
children: const [
_Section1_Constructors(),
_Section2_LazyLoading(),
_Section3_ShrinkWrap(),
_Section4_ItemExtent(),
_Section5_Physics(),
_Section6_Controller(),
_Section7_Reverse(),
_Section8_ScrollDirection(),
_Section9_CacheExtent(),
_Section10_KeepAlive(),
_Section11_NestedScroll(),
_Section12_Refresh(),
_Section13_Summary(),
],
),
);
}
}
// ────────────────────────────────────────────────────────────────
// ① 四种构造函数
// ────────────────────────────────────────────────────────────────
class _Section1_Constructors extends StatelessWidget {
const _Section1_Constructors();
@override
Widget build(BuildContext context) {
return _Card(
title: '① ListView 四种构造函数',
subtitle: '不同场景选不同构造函数',
children: const [
Text(
'ListView({ children }) 一次性构建(不推荐大量数据)\n'
'ListView.builder({ itemCount, itemBuilder }) 懒加载 ✅\n'
'ListView.separated({ separatorBuilder }) 带分割线的懒加载 ✅\n'
'ListView.custom({ slivers }) 自定义 Sliver 布局',
style: TextStyle(fontSize: 11, fontFamily: 'monospace'),
),
SizedBox(height: 8),
Row(
children: [
Expanded(
child: _ListDemo(
title: 'ListView(children:)',
highlight: false,
),
),
SizedBox(width: 8),
Expanded(
child: _ListDemo(
title: 'ListView.builder',
highlight: true,
),
),
],
),
SizedBox(height: 8),
Text('✅ 99% 场景用 ListView.builder\n'
'✅ 需要分割线用 ListView.separated\n'
'❌ ListView(children:) 适合子组件 < 20 的固定列表',
style: TextStyle(fontSize: 11)),
],
);
}
}
class _ListDemo extends StatelessWidget {
final String title;
final bool highlight;
const _ListDemo({required this.title, required this.highlight});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 80,
child: highlight
? ListView.builder(
itemCount: 50,
itemBuilder: (_, i) => Container(
height: 16,
margin: const EdgeInsets.symmetric(vertical: 1),
color: Colors.blue,
alignment: Alignment.center,
child: Text('$i', style: const TextStyle(fontSize: 9, color: Colors.white)),
),
)
: ListView(
children: List.generate(50, (i) => Container(
height: 16,
margin: const EdgeInsets.symmetric(vertical: 1),
color: Colors.amber,
alignment: Alignment.center,
child: Text('$i', style: const TextStyle(fontSize: 9)),
)),
),
);
}
}
// ────────────────────────────────────────────────────────────────
// ② 懒加载原理
// ────────────────────────────────────────────────────────────────
class _Section2_LazyLoading extends StatelessWidget {
const _Section2_LazyLoading();
@override
Widget build(BuildContext context) {
return _Card(
title: '② 懒加载原理',
subtitle: 'builder vs children 的核心区别',
children: [
Container(
padding: const EdgeInsets.all(8),
color: Colors.amber.shade50,
child: const Text(
'ListView(children: [...]) ❌ 非懒加载\n'
'────────────────────────────────────────\n'
'final list = List.generate(100, (i) => Text("i"));\n'
'// ↑ List.generate 在这里就执行完了\n'
'// 100 个 Widget 全部创建好再传给 ListView\n'
'ListView(children: list);\n\n'
'ListView.builder ✅ 懒加载\n'
'────────────────────────────────────────\n'
'ListView.builder(\n'
' itemCount: 100,\n'
' itemBuilder: (ctx, i) => Text(i.toString()),\n'
');\n'
'// ↑ itemBuilder 是回调函数\n'
'// ListView 滚到第 N 个才调用 itemBuilder(ctx, N)\n'
'// 只构建屏幕可见的 item\n'
'// 不可见的会被销毁回收(超出 cacheExtent 后)',
style: TextStyle(fontSize: 11, fontFamily: 'monospace'),
),
),
const SizedBox(height: 8),
const Text('🚀 懒加载的好处:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11)),
const Text('• 首屏加载快(只构建可见的)\n'
'• 内存占用少(不可见的会被回收)\n'
'• 滑动更流畅',
style: TextStyle(fontSize: 11)),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ③ shrinkWrap
// ────────────────────────────────────────────────────────────────
class _Section3_ShrinkWrap extends StatelessWidget {
const _Section3_ShrinkWrap();
@override
Widget build(BuildContext context) {
return _Card(
title: '③ shrinkWrap',
subtitle: '让 ListView 只占内容高度,适合嵌套场景',
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
children: [
Container(
color: Colors.amber.withValues(alpha: 0.2),
height: 100,
child: ListView.builder(
itemCount: 5,
itemBuilder: (_, i) => Container(
height: 18,
margin: const EdgeInsets.all(1),
color: Colors.amber,
child: Text('item $i', style: const TextStyle(fontSize: 10)),
),
),
),
const SizedBox(height: 2),
const Text('shrinkWrap: false\n(撑满父级)', style: TextStyle(fontSize: 9)),
],
),
),
const SizedBox(width: 8),
Expanded(
child: Column(
children: [
Container(
color: Colors.blue.withValues(alpha: 0.2),
height: 100,
child: ListView.builder(
shrinkWrap: true,
itemCount: 5,
itemBuilder: (_, i) => Container(
height: 18,
margin: const EdgeInsets.all(1),
color: Colors.blue,
child: Text('item $i',
style: const TextStyle(fontSize: 10, color: Colors.white)),
),
),
),
const SizedBox(height: 2),
const Text('shrinkWrap: true\n(只占内容高度)', style: TextStyle(fontSize: 9)),
],
),
),
],
),
const SizedBox(height: 8),
const Text('⚠️ shrinkWrap: true 会禁用 ListView 的懒加载!\n'
'所有 item 会立即全部构建。只在嵌套必须用的时候才开。\n\n'
'典型场景:Column 里放 ListView\n'
'→ ListView 默认会撑开无限高,报错\n'
'→ 加 shrinkWrap: true 让它根据内容收缩',
style: TextStyle(fontSize: 11)),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ④ itemExtent ------ 性能优化
// ────────────────────────────────────────────────────────────────
class _Section4_ItemExtent extends StatelessWidget {
const _Section4_ItemExtent();
@override
Widget build(BuildContext context) {
return _Card(
title: '④ itemExtent',
subtitle: '提前告诉 ListView 每个 item 的高度 → 性能大幅提升',
children: [
SizedBox(
height: 100,
child: ListView.builder(
itemCount: 50,
itemExtent: 20, // ← 告诉 ListView 每个 item 高 20
itemBuilder: (_, i) => Container(
alignment: Alignment.center,
color: i.isEven ? Colors.amber : Colors.blue,
child: Text('$i',
style: const TextStyle(fontSize: 10, color: Colors.white)),
),
),
),
const SizedBox(height: 8),
const Text(
'// 如果所有 item 高度一样,务必设 itemExtent\n'
'ListView.builder(\n'
' itemExtent: 50.0, // ← 告诉 ListView 固定高度\n'
' itemCount: 10000,\n'
' itemBuilder: ...,\n'
')\n\n'
'📈 性能提升原理:\n'
'• ListView 不需要先 build item 再量高度\n'
'• 直接算出哪些 item 在可视区域\n'
'• 滚动更流畅,内存更少\n\n'
'⚠️ item 高度不一致时不要用 itemExtent\n'
'用 prototypeItem 代替(Flutter 3.x 新增)',
style: TextStyle(fontSize: 11, fontFamily: 'monospace'),
),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑤ physics
// ────────────────────────────────────────────────────────────────
class _Section5_Physics extends StatelessWidget {
const _Section5_Physics();
@override
Widget build(BuildContext context) {
return _Card(
title: '⑤ physics',
subtitle: '滚动物理效果,四种 ScrollPhysics',
children: [
Row(
children: [
Expanded(
child: _PhysicsDemo(
label: 'Bouncing',
physics: const BouncingScrollPhysics(),
color: Colors.green,
),
),
const SizedBox(width: 6),
Expanded(
child: _PhysicsDemo(
label: 'Clamping',
physics: const ClampingScrollPhysics(),
color: Colors.blue,
),
),
const SizedBox(width: 6),
Expanded(
child: _PhysicsDemo(
label: 'NeverScrollable',
physics: const NeverScrollableScrollPhysics(),
color: Colors.grey,
),
),
],
),
const SizedBox(height: 8),
const Text('四种 ScrollPhysics:\n'
'• BouncingScrollPhysics iOS 风格,边缘回弹\n'
'• ClampingScrollPhysics Android 风格,边缘夹手\n'
'• AlwaysScrollableScrollPhysics 内容少也能拉动(配合 Bouncing/Clamping)\n'
'• NeverScrollableScrollPhysics 完全禁止滚动\n\n'
'想让 Android 也有 iOS 回弹效果:\n'
'physics: const BouncingScrollPhysics()',
style: TextStyle(fontSize: 11)),
],
);
}
}
class _PhysicsDemo extends StatelessWidget {
final String label;
final ScrollPhysics physics;
final Color color;
const _PhysicsDemo({required this.label, required this.physics, required this.color});
@override
Widget build(BuildContext context) {
return Column(
children: [
SizedBox(
height: 70,
child: ListView.builder(
physics: physics,
itemCount: 5,
itemBuilder: (_, i) => Container(
height: 20,
margin: const EdgeInsets.all(1),
color: color,
alignment: Alignment.center,
child: Text('$i', style: const TextStyle(fontSize: 9, color: Colors.white)),
),
),
),
const SizedBox(height: 2),
Text(label, style: const TextStyle(fontSize: 9)),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑥ ScrollController ------ 监听 + 跳转 + 加载更多
// ────────────────────────────────────────────────────────────────
class _Section6_Controller extends StatefulWidget {
const _Section6_Controller();
@override
State<_Section6_Controller> createState() => _Section6_ControllerState();
}
class _Section6_ControllerState extends State<_Section6_Controller> {
final _controller = ScrollController();
double _offset = 0;
bool _isTop = true;
bool _isBottom = false;
int _itemCount = 20;
@override
void initState() {
super.initState();
_controller.addListener(_onScroll);
}
void _onScroll() {
if (!mounted) return;
final offset = _controller.offset;
final max = _controller.position.maxScrollExtent;
setState(() {
_offset = offset;
_isTop = offset <= 0;
_isBottom = offset >= max - 50;
});
if (_isBottom && _itemCount < 100) {
Future.delayed(const Duration(milliseconds: 300), () {
if (mounted) setState(() => _itemCount += 10);
});
}
}
@override
void dispose() {
_controller
..removeListener(_onScroll)
..dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return _Card(
title: '⑥ ScrollController',
subtitle: '监听滚动位置 + 跳转 + 上拉加载更多',
children: [
Text(
'offset: ${_offset.toStringAsFixed(1)} | '
'top: $_isTop | bottom: $_isBottom\n'
'itemCount: $_itemCount / 100',
style: const TextStyle(fontSize: 11, fontFamily: 'monospace'),
),
const SizedBox(height: 4),
SizedBox(
height: 100,
child: ListView.builder(
controller: _controller,
itemCount: _itemCount,
itemExtent: 20,
itemBuilder: (_, i) => Container(
alignment: Alignment.center,
color: i.isEven ? Colors.amber : Colors.blue,
child: Text('$i',
style: const TextStyle(fontSize: 10, color: Colors.white)),
),
),
),
const SizedBox(height: 6),
Row(
children: [
ElevatedButton(
onPressed: () => _controller.animateTo(0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut),
child: const Text('回到顶部', style: TextStyle(fontSize: 11)),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => _controller.jumpTo(150),
child: const Text('跳到 150', style: TextStyle(fontSize: 11)),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => _controller.animateTo(
_controller.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
),
child: const Text('滚到底', style: TextStyle(fontSize: 11)),
),
],
),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.all(8),
color: Colors.amber.shade50,
child: const Text(
'// 上拉加载更多核心逻辑:\n'
'controller.addListener(() {\n'
' final max = controller.position.maxScrollExtent;\n'
' if (controller.offset >= max - 50) {\n'
' loadMore(); // 滚到底部加载下一页\n'
' }\n'
'});\n\n'
'// ⚠️ 必须 dispose:\n'
'@override\n'
'void dispose() {\n'
' controller.dispose();\n'
' super.dispose();\n'
'}',
style: TextStyle(fontSize: 10.5, fontFamily: 'monospace'),
),
),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑦ reverse ------ 反向滚动
// ────────────────────────────────────────────────────────────────
class _Section7_Reverse extends StatelessWidget {
const _Section7_Reverse();
@override
Widget build(BuildContext context) {
return _Card(
title: '⑦ reverse',
subtitle: '反向滚动,聊天列表/评论区倒序显示',
children: [
Row(
children: [
Expanded(
child: Column(
children: [
SizedBox(
height: 80,
child: ListView.builder(
reverse: false,
itemCount: 5,
itemBuilder: (_, i) => Container(
height: 18,
margin: const EdgeInsets.symmetric(vertical: 1),
color: Colors.amber,
child: Text('item ${i + 1}', style: const TextStyle(fontSize: 10)),
),
),
),
const SizedBox(height: 2),
const Text('reverse: false\n从上往下', style: TextStyle(fontSize: 9)),
],
),
),
const SizedBox(width: 8),
Expanded(
child: Column(
children: [
SizedBox(
height: 80,
child: ListView.builder(
reverse: true,
itemCount: 5,
itemBuilder: (_, i) => Container(
height: 18,
margin: const EdgeInsets.symmetric(vertical: 1),
color: Colors.blue,
child: Text('item ${i + 1}',
style: const TextStyle(fontSize: 10, color: Colors.white)),
),
),
),
const SizedBox(height: 2),
const Text('reverse: true\n从下往上', style: TextStyle(fontSize: 9)),
],
),
),
],
),
const SizedBox(height: 8),
const Text('聊天列表完整示例:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11)),
const Text(
'ListView.builder(\n'
' reverse: true,\n'
' itemCount: messages.length,\n'
' itemBuilder: (_, i) => MessageBubble(messages[i]),\n'
')\n\n'
'// 新消息来了自动显示在底部\n'
'// 不需要手动滚到底',
style: TextStyle(fontSize: 11),
),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑧ scrollDirection ------ 水平列表
// ────────────────────────────────────────────────────────────────
class _Section8_ScrollDirection extends StatelessWidget {
const _Section8_ScrollDirection();
@override
Widget build(BuildContext context) {
return _Card(
title: '⑧ scrollDirection',
subtitle: 'Axis.horizontal 水平滚动列表',
children: [
SizedBox(
height: 60,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 15,
itemExtent: 50,
itemBuilder: (_, i) => Container(
margin: const EdgeInsets.symmetric(horizontal: 2),
color: Colors.blue,
alignment: Alignment.center,
child: Text('$i',
style: const TextStyle(color: Colors.white, fontSize: 12)),
),
),
),
const SizedBox(height: 8),
const Text('水平 ListView 典型场景:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11)),
const Text('• 商品分类 Tab 横向滑动\n'
'• 图片轮播(配合 PageView 更专业)\n'
'• 时间轴选择器(日期/小时)\n'
'• 游戏道具栏',
style: TextStyle(fontSize: 11)),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑨ cacheExtent ------ 预加载区域
// ────────────────────────────────────────────────────────────────
class _Section9_CacheExtent extends StatelessWidget {
const _Section9_CacheExtent();
@override
Widget build(BuildContext context) {
return _Card(
title: '⑨ cacheExtent',
subtitle: '可视区域外提前构建多少像素的 item',
children: const [
Text(
'ListView(\n'
' cacheExtent: 1000, // 视口外各预加载 1000px\n'
' ...\n'
')\n\n'
'默认值:\n'
'• Android:0\n'
'• iOS:viewport 的 0.25 倍\n\n'
'作用:\n'
'• 滚动到边界时提前 build 下一批 item\n'
'• 避免快速滚动时出现"白屏"或"锯齿"\n\n'
'如果 item 构建成本高(如加载网络图片),\n'
'可以适当调大 cacheExtent',
style: TextStyle(fontSize: 11),
),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑩ KeepAlive ------ 保持 item 状态
// ────────────────────────────────────────────────────────────────
class _Section10_KeepAlive extends StatelessWidget {
const _Section10_KeepAlive();
@override
Widget build(BuildContext context) {
return _Card(
title: '⑩ addAutomaticKeepAlives',
subtitle: '滚出可视区域的 item 状态会被销毁!需要保持状态?',
children: [
SizedBox(
height: 80,
child: ListView.builder(
itemCount: 10,
itemExtent: 20,
addAutomaticKeepAlives: true, // 默认就是 true
itemBuilder: (_, i) => _KeepAliveItem(index: i),
),
),
const SizedBox(height: 8),
const Text('AutomaticKeepAliveClientMixin 用法:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11)),
const Text(
'class _MyItemState extends State<_MyItem>\n'
' with AutomaticKeepAliveClientMixin {\n'
'\n'
' @override\n'
' bool get wantKeepAlive => true; // ← 保持状态\n'
'\n'
' @override\n'
' Widget build(BuildContext context) {\n'
' super.build(context); // ← 必须调\n'
' return Container(...);\n'
' }\n'
'}\n\n'
'⚠️ 不写这个的话,item 滚出可视区域后\n'
'State 会被 dispose → 再滚回来状态丢失',
style: TextStyle(fontSize: 11, fontFamily: 'monospace'),
),
],
);
}
}
class _KeepAliveItem extends StatefulWidget {
final int index;
const _KeepAliveItem({required this.index});
@override
State<_KeepAliveItem> createState() => _KeepAliveItemState();
}
class _KeepAliveItemState extends State<_KeepAliveItem>
with AutomaticKeepAliveClientMixin {
int _count = 0;
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return GestureDetector(
onTap: () => setState(() => _count++),
child: Container(
alignment: Alignment.center,
color: Colors.amber,
child: Text('item ${widget.index} count=$_count',
style: const TextStyle(fontSize: 10)),
),
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑪ 嵌套滚动 ------ Column 里放 ListView
// ────────────────────────────────────────────────────────────────
class _Section11_NestedScroll extends StatelessWidget {
const _Section11_NestedScroll();
@override
Widget build(BuildContext context) {
return _Card(
title: '⑪ Column + ListView',
subtitle: '最常见的嵌套问题和 3 种解决方案',
children: [
Container(
padding: const EdgeInsets.all(8),
color: Colors.red.shade50,
child: const Text(
'❌ 错误写法:直接嵌套\n\n'
'Column(\n'
' children: [\n'
' Text("标题"),\n'
' ListView.builder(...), // ← 报错!\n'
' ], // Column 给 ListView 无限高约束\n'
') // ListView 也需要无限高\n'
' // 死锁!',
style: TextStyle(fontSize: 11, fontFamily: 'monospace'),
),
),
const SizedBox(height: 6),
const Text('✅ 三种解决方案:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11)),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.all(8),
color: Colors.amber.shade50,
child: const Text(
'方案 1:shrinkWrap(简单但非懒加载)\n'
'Column(\n'
' children: [\n'
' Text("标题"),\n'
' Expanded(\n'
' child: ListView.builder(\n'
' shrinkWrap: true, // ← 让 ListView 收缩\n'
' itemBuilder: ...,\n'
' ),\n'
' ),\n'
' ],\n'
')',
style: TextStyle(fontSize: 11, fontFamily: 'monospace'),
),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.all(8),
color: Colors.blue.shade50,
child: const Text(
'方案 2:Expanded/Flexible(推荐,保持懒加载)\n'
'Column(\n'
' children: [\n'
' Text("标题"),\n'
' Expanded( // ← 给 ListView 一个有限高度\n'
' child: ListView.builder(...),\n'
' ),\n'
' ],\n'
')',
style: TextStyle(fontSize: 11, fontFamily: 'monospace'),
),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.all(8),
color: Colors.green.shade50,
child: const Text(
'方案 3:CustomScrollView(最灵活)\n'
'CustomScrollView(\n'
' slivers: [\n'
' SliverToBoxAdapter(child: Text("标题")),\n'
' SliverList(delegate: ...),\n'
' ],\n'
')',
style: TextStyle(fontSize: 11, fontFamily: 'monospace'),
),
),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑫ 下拉刷新 RefreshIndicator
// ────────────────────────────────────────────────────────────────
class _Section12_Refresh extends StatefulWidget {
const _Section12_Refresh();
@override
State<_Section12_Refresh> createState() => _Section12_RefreshState();
}
class _Section12_RefreshState extends State<_Section12_Refresh> {
final List<int> _items = List.generate(20, (i) => i);
Future<void> _onRefresh() async {
await Future.delayed(const Duration(milliseconds: 800));
setState(() {
_items.clear();
_items.addAll(List.generate(20, (i) => DateTime.now().millisecond + i));
});
}
@override
Widget build(BuildContext context) {
return _Card(
title: '⑫ RefreshIndicator 下拉刷新',
subtitle: '下拉刷新 + 上拉加载完整示例',
children: [
SizedBox(
height: 120,
child: RefreshIndicator(
onRefresh: _onRefresh,
child: ListView.builder(
itemCount: _items.length,
itemExtent: 22,
itemBuilder: (_, i) => Container(
alignment: Alignment.center,
margin: const EdgeInsets.symmetric(horizontal: 4),
color: i.isEven ? Colors.amber : Colors.blue,
child: Text('#${_items[i]}',
style: const TextStyle(fontSize: 10, color: Colors.white)),
),
),
),
),
const SizedBox(height: 6),
const Text('试着在上面的列表下拉 → 触发刷新',
style: TextStyle(fontSize: 11, color: Colors.blue)),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.all(8),
color: Colors.amber.shade50,
child: const Text(
'// 下拉刷新完整代码:\n'
'RefreshIndicator(\n'
' onRefresh: () async {\n'
' await fetchFromNetwork();\n'
' setState(() { items = newData; });\n'
' },\n'
' child: ListView.builder(...),\n'
')\n\n'
'// 上拉加载更多(配合 ScrollController):\n'
'controller.addListener(() {\n'
' if (controller.offset >= controller.position.maxScrollExtent) {\n'
' loadMore();\n'
' }\n'
'});\n\n'
'// 推荐用 easy_refresh / pull_to_refresh 第三方库\n'
'// 比 RefreshIndicator 功能强太多(支持头图、上拉、指示器自定义)',
style: TextStyle(fontSize: 10.5, fontFamily: 'monospace'),
),
),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑬ 完整总结
// ────────────────────────────────────────────────────────────────
class _Section13_Summary extends StatelessWidget {
const _Section13_Summary();
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.brown.shade50,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'📌 ListView 完整总结\n\n'
'1. 四种构造:\n'
' children(非懒加载)/ builder / separated / custom\n'
' ✅ 99% 场景用 builder\n\n'
'2. 懒加载核心:\n'
' itemBuilder 是回调 → 滚到哪构建哪\n'
' 超出 cacheExtent 的被销毁回收\n\n'
'3. 性能优化三件套:\n'
' itemExtent(固定高度)\n'
' addAutomaticKeepAlives(保持状态)\n'
' cacheExtent(预加载)\n\n'
'4. shrinkWrap: true 会禁用懒加载!\n'
' 只在嵌套必须用的时候开\n\n'
'5. 嵌套 Column:Expanded > shrinkWrap > CustomScrollView\n\n'
'6. KeepAlive 必须 AutomaticKeepAliveClientMixin\n'
' wantKeepAlive = true + super.build\n\n'
'7. 控制器必记:controller.dispose()\n\n'
'8. 下拉刷新 RefreshIndicator\n'
' 上拉加载 ScrollController 监听 maxScrollExtent',
style: TextStyle(fontSize: 12),
),
);
}
}
// ────────────────────────────────────────────────────────────────
// 通用卡片
// ────────────────────────────────────────────────────────────────
class _Card extends StatelessWidget {
final String title;
final String subtitle;
final List<Widget> children;
const _Card({required this.title, this.subtitle = '', required this.children});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey.shade300),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
if (subtitle.isNotEmpty) ...[
const SizedBox(height: 2),
Text(subtitle,
style: TextStyle(fontSize: 11, color: Colors.grey.shade600, fontStyle: FontStyle.italic)),
],
const SizedBox(height: 10),
...children,
],
),
);
}
}
GridView
GridView.count是非懒加载的模式
php
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(const MaterialApp(home: HomePage()));
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('GridView 知识点')),
body: GridView.count(
scrollDirection: Axis.horizontal,
crossAxisSpacing: 10,
mainAxisSpacing: 20,
crossAxisCount: 2,
padding: const EdgeInsets.all(16),
children: List.generate(100, (index){
return Container(
color: Colors.green,
alignment: Alignment.center,
child: Text("第$index个"),
);
}),
),
);
}
}
GridView进阶
less
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(const MaterialApp(home: HomePage()));
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('GridView 全方位知识点')),
body: ListView(
padding: const EdgeInsets.all(16),
children: const [
_Section1_Constructors(),
_Section2_GridDelegate(),
_Section3_CrossAxisCount(),
_Section4_MaxCrossAxisExtent(),
_Section5_ChildAspectRatio(),
_Section6_Spacing(),
_Section7_Builder(),
_Section8_ScrollDirection(),
_Section9_CommonParams(),
_Section10_WhenToUse(),
_Section11_Summary(),
],
),
);
}
}
// ────────────────────────────────────────────────────────────────
// ① 四种构造函数
// ────────────────────────────────────────────────────────────────
class _Section1_Constructors extends StatelessWidget {
const _Section1_Constructors();
@override
Widget build(BuildContext context) {
return _Card(
title: '① GridView 四种构造函数',
subtitle: '和 ListView 类似,但多了 gridDelegate 参数',
children: const [
Text(
'GridView({ children }) 一次性构建(不推荐)\n'
'GridView.count({ crossAxisCount }) 固定列数,最常用 ✅\n'
'GridView.extent({ maxCrossAxisExtent }) 固定 item 宽度,自动算列数 ✅\n'
'GridView.builder({ gridDelegate, itemBuilder }) 完全自定义\n'
'GridView.custom({ gridDelegate, childrenDelegate }) 最底层',
style: TextStyle(fontSize: 11, fontFamily: 'monospace'),
),
SizedBox(height: 8),
Text('✅ 90% 场景用 GridView.count\n'
'✅ 固定 item 宽度用 GridView.extent\n'
'❌ GridView(children:) 只适合 < 20 个 item',
style: TextStyle(fontSize: 11)),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ② gridDelegate ------ 核心控制
// ────────────────────────────────────────────────────────────────
class _Section2_GridDelegate extends StatelessWidget {
const _Section2_GridDelegate();
@override
Widget build(BuildContext context) {
return _Card(
title: '② gridDelegate 是核心',
subtitle: 'count / extent 都是 GridView.builder 的语法糖',
children: const [
Text(
'// GridView.count 本质上是:\n'
'GridView(\n'
' gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(\n'
' crossAxisCount: 3,\n'
' mainAxisSpacing: 10,\n'
' crossAxisSpacing: 10,\n'
' childAspectRatio: 1.0,\n'
' ),\n'
' children: [...],\n'
')\n\n'
'// GridView.extent 本质上是:\n'
'GridView(\n'
' gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(\n'
' maxCrossAxisExtent: 120,\n'
' mainAxisSpacing: 10,\n'
' crossAxisSpacing: 10,\n'
' childAspectRatio: 1.0,\n'
' ),\n'
' children: [...],\n'
')\n\n'
'// ✅ 99% 场景用 SliverGridDelegateWithFixedCrossAxisCount\n'
'// ✅ 需要自适应列宽用 SliverGridDelegateWithMaxCrossAxisExtent',
style: TextStyle(fontSize: 11, fontFamily: 'monospace'),
),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ③ crossAxisCount ------ 固定列数
// ────────────────────────────────────────────────────────────────
class _Section3_CrossAxisCount extends StatelessWidget {
const _Section3_CrossAxisCount();
@override
Widget build(BuildContext context) {
return _Card(
title: '③ crossAxisCount ------ 固定列数',
subtitle: '不管屏幕多宽,始终显示 N 列',
children: [
Row(
children: [
Expanded(child: _GridCountDemo(count: 2, label: '2 列')),
SizedBox(width: 6),
Expanded(child: _GridCountDemo(count: 3, label: '3 列')),
SizedBox(width: 6),
Expanded(child: _GridCountDemo(count: 4, label: '4 列')),
],
),
SizedBox(height: 8),
Text('注意:列数固定,item 宽度 = 屏幕宽度 / crossAxisCount',
style: TextStyle(fontSize: 11)),
],
);
}
}
class _GridCountDemo extends StatelessWidget {
final int count;
final String label;
const _GridCountDemo({required this.count, required this.label});
@override
Widget build(BuildContext context) {
return Column(
children: [
SizedBox(
height: 100,
child: GridView.count(
crossAxisCount: count,
mainAxisSpacing: 2,
crossAxisSpacing: 2,
children: List.generate(12, (i) => Container(
color: Colors.blue,
alignment: Alignment.center,
child: Text('$i', style: TextStyle(fontSize: 9, color: Colors.white)),
)),
),
),
SizedBox(height: 2),
Text(label, style: TextStyle(fontSize: 9)),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ④ maxCrossAxisExtent ------ 固定 item 宽度
// ────────────────────────────────────────────────────────────────
class _Section4_MaxCrossAxisExtent extends StatelessWidget {
const _Section4_MaxCrossAxisExtent();
@override
Widget build(BuildContext context) {
return _Card(
title: '④ maxCrossAxisExtent ------ 自适应列数',
subtitle: '每个 item 最多 N 宽,剩下的空间再排一列',
children: [
SizedBox(
height: 100,
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 80,
mainAxisSpacing: 4,
crossAxisSpacing: 4,
childAspectRatio: 1,
),
itemCount: 20,
itemBuilder: (_, i) => Container(
color: Colors.amber,
alignment: Alignment.center,
child: Text('$i', style: const TextStyle(fontSize: 10)),
),
),
),
SizedBox(height: 8),
Text('上面 maxCrossAxisExtent: 80\n'
'→ item 最多宽 80px,屏幕宽度足够就多排\n'
'→ 旋转屏幕,列数会自动变',
style: TextStyle(fontSize: 11)),
SizedBox(height: 6),
Container(
padding: EdgeInsets.all(8),
color: Colors.amber.shade50,
child: Text(
'// 对比:\n'
'crossAxisCount: 3 → 永远 3 列\n'
'maxCrossAxisExtent: 80 → item 固定宽 80,列数 = 屏幕宽 / 80\n\n'
'商品列表推荐用 maxCrossAxisExtent\n'
'设置页图标推荐用 crossAxisCount',
style: TextStyle(fontSize: 11, fontFamily: 'monospace'),
),
),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑤ childAspectRatio ------ 宽高比
// ────────────────────────────────────────────────────────────────
class _Section5_ChildAspectRatio extends StatelessWidget {
const _Section5_ChildAspectRatio();
@override
Widget build(BuildContext context) {
return _Card(
title: '⑤ childAspectRatio ------ item 宽高比',
subtitle: 'childAspectRatio = width / height',
children: [
Row(
children: [
Expanded(child: _RatioDemo(ratio: 1, label: '1:1 正方形')),
SizedBox(width: 6),
Expanded(child: _RatioDemo(ratio: 2, label: '2:1 横扁')),
SizedBox(width: 6),
Expanded(child: _RatioDemo(ratio: 0.5, label: '1:2 竖长')),
],
),
SizedBox(height: 8),
Text('item 宽度由 crossAxisCount 决定,高度由 childAspectRatio 反推:\n'
'height = width / childAspectRatio\n\n'
'商品图片常用 1:1(正方形)\n'
'视频封面常用 16:9(宽屏)',
style: TextStyle(fontSize: 11)),
],
);
}
}
class _RatioDemo extends StatelessWidget {
final double ratio;
final String label;
const _RatioDemo({required this.ratio, required this.label});
@override
Widget build(BuildContext context) {
return Column(
children: [
SizedBox(
height: 80,
child: GridView.count(
crossAxisCount: 2,
mainAxisSpacing: 2,
crossAxisSpacing: 2,
childAspectRatio: ratio,
children: List.generate(6, (i) => Container(
color: Colors.green,
alignment: Alignment.center,
child: Text('$i', style: const TextStyle(fontSize: 9, color: Colors.white)),
)),
),
),
const SizedBox(height: 2),
Text(label, style: const TextStyle(fontSize: 9)),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑥ spacing ------ 间距
// ────────────────────────────────────────────────────────────────
class _Section6_Spacing extends StatelessWidget {
const _Section6_Spacing();
@override
Widget build(BuildContext context) {
return _Card(
title: '⑥ mainAxisSpacing / crossAxisSpacing',
subtitle: '主轴间距 & 交叉轴间距',
children: [
SizedBox(
height: 100,
child: GridView.count(
crossAxisCount: 3,
mainAxisSpacing: 10,
crossAxisSpacing: 20,
children: List.generate(9, (i) => Container(
color: Colors.blue,
alignment: Alignment.center,
child: Text('$i', style: const TextStyle(fontSize: 10, color: Colors.white)),
)),
),
),
SizedBox(height: 8),
Text('上面:\n'
'• mainAxisSpacing: 10 → 行与行之间 10px\n'
'• crossAxisSpacing: 20 → 列与列之间 20px\n\n'
'// 方向记忆:\n'
'// 垂直滚动时,主轴 = 垂直方向,交叉轴 = 水平方向\n'
'// 水平滚动时反过来',
style: TextStyle(fontSize: 11)),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑦ GridView.builder ------ 懒加载
// ────────────────────────────────────────────────────────────────
class _Section7_Builder extends StatelessWidget {
const _Section7_Builder();
@override
Widget build(BuildContext context) {
return _Card(
title: '⑦ GridView.builder ------ 懒加载',
subtitle: '和 ListView.builder 一样,按需构建',
children: const [
Text(
'GridView.builder(\n'
' gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(\n'
' crossAxisCount: 3,\n'
' ),\n'
' itemCount: 10000,\n'
' itemBuilder: (context, index) {\n'
' return Container(child: Text(index.toString()));\n'
' },\n'
')\n\n'
'✅ 懒加载,只构建可见的 item\n'
'✅ itemCount 可以很大(几万条没问题)\n\n'
'// 注意:GridView.count 和 GridView.extent\n'
'// 内部也是调用 builder,已经自带懒加载\n'
'// 所以 count / extent 也可以放心用',
style: TextStyle(fontSize: 11, fontFamily: 'monospace'),
),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑧ scrollDirection ------ 水平网格
// ────────────────────────────────────────────────────────────────
class _Section8_ScrollDirection extends StatelessWidget {
const _Section8_ScrollDirection();
@override
Widget build(BuildContext context) {
return _Card(
title: '⑧ scrollDirection',
subtitle: 'Axis.horizontal 水平滚动的网格',
children: [
SizedBox(
height: 80,
child: GridView.count(
scrollDirection: Axis.horizontal,
crossAxisCount: 2,
mainAxisSpacing: 4,
crossAxisSpacing: 4,
childAspectRatio: 1.5,
children: List.generate(20, (i) => Container(
color: Colors.amber,
alignment: Alignment.center,
child: Text('$i', style: const TextStyle(fontSize: 11)),
)),
),
),
SizedBox(height: 8),
Text('水平网格典型场景:图标选择器、颜色选择器\n'
'⚠️ 水平滚动时,crossAxisCount 控制的是垂直方向的列数',
style: TextStyle(fontSize: 11)),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑨ 通用属性 ------ 和 ListView 一样
// ────────────────────────────────────────────────────────────────
class _Section9_CommonParams extends StatelessWidget {
const _Section9_CommonParams();
@override
Widget build(BuildContext context) {
return _Card(
title: '⑨ 通用属性(和 ListView 完全一样)',
subtitle: '所有 ScrollView 共享的属性',
children: const [
Text(
'// GridView 同样支持:\n\n'
'ScrollController controller 监听/跳转/加载更多\n'
'ScrollPhysics physics Bouncing / Clamping / NeverScrollable\n'
'bool reverse 反向滚动\n'
'bool shrinkWrap 嵌套 Column 时收缩(⚠️ 禁用懒加载)\n'
'double cacheExtent 预加载区域\n'
'EdgeInsetsGeometry padding 内边距\n'
'bool addAutomaticKeepAlives 保持 item 状态\n\n'
'// 用法和 ListView 完全一样:\n'
'final controller = ScrollController();\n'
'controller.addListener(() {\n'
' if (controller.offset >= controller.position.maxScrollExtent - 50) {\n'
' loadMore();\n'
' }\n'
'});\n'
'// ⚠️ dispose 必须释放\n'
'@override\n'
'void dispose() {\n'
' controller.dispose();\n'
' super.dispose();\n'
'}',
style: TextStyle(fontSize: 11, fontFamily: 'monospace'),
),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑩ 什么时候用 GridView
// ────────────────────────────────────────────────────────────────
class _Section10_WhenToUse extends StatelessWidget {
const _Section10_WhenToUse();
@override
Widget build(BuildContext context) {
return _Card(
title: '⑩ 什么时候用 GridView',
subtitle: '和 Wrap 的区别 & 典型场景',
children: const [
Text('✅ GridView 典型场景:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11)),
Text('• 商品瀑布流(电商 App)\n'
'• 照片墙 / 相册\n'
'• 应用图标网格(iOS 桌面)\n'
'• 颜色选择器 / 图标选择器\n'
'• 棋盘 / 井字棋',
style: TextStyle(fontSize: 11)),
SizedBox(height: 6),
Text('❌ GridView vs Wrap:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11, color: Colors.red)),
Text('Wrap:非滚动,子组件尺寸可以不一样\n'
'→ 标签云、聊天表情面板、流式布局\n\n'
'GridView:可滚动,所有 item 尺寸统一\n'
'→ 需要滚动的网格、固定列数的布局',
style: TextStyle(fontSize: 11)),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ⑪ 总结
// ────────────────────────────────────────────────────────────────
class _Section11_Summary extends StatelessWidget {
const _Section11_Summary();
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.brown.shade50,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'📌 GridView 完整总结\n\n'
'1. 四种构造:\n'
' children(非懒加载)/ count(固定列数 ✅)\n'
' extent(固定 item 宽)/ builder / custom\n\n'
'2. 两个核心 Delegate:\n'
' SliverGridDelegateWithFixedCrossAxisCount 固定列数\n'
' SliverGridDelegateWithMaxCrossAxisExtent 自适应列数\n\n'
'3. 必记四件套:\n'
' crossAxisCount / mainAxisSpacing\n'
' crossAxisSpacing / childAspectRatio\n\n'
'4. GridView.count / extent 已自带懒加载 ✅\n'
' 不需要特意用 builder\n\n'
'5. 通用属性和 ListView 完全一致\n'
' controller.dispose() 必须调\n\n'
'6. 选型:\n'
' 固定列数 → count\n'
' 自适应列数 → extent\n'
' 非滚动不等高 → Wrap',
style: TextStyle(fontSize: 12),
),
);
}
}
// ────────────────────────────────────────────────────────────────
class _Card extends StatelessWidget {
final String title;
final String subtitle;
final List<Widget> children;
const _Card({required this.title, this.subtitle = '', required this.children});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey.shade300),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
if (subtitle.isNotEmpty) ...[
const SizedBox(height: 2),
Text(subtitle,
style: TextStyle(fontSize: 11, color: Colors.grey.shade600, fontStyle: FontStyle.italic)),
],
const SizedBox(height: 10),
...children,
],
),
);
}
}