Flutter Scrollable Widgets: ListView, GridView, Sliver, CustomScrollView, and TabBarView
This tutorial explains how to use Flutter's scrollable components—including ListView, GridView, Sliver, CustomScrollView, and TabBarView—covers their constructors, lazy‑loading behavior, event listening with ScrollController and NotificationListener, and provides complete code examples for each widget.
Flutter provides several scrollable widgets for displaying large amounts of data on mobile devices, such as lists, grids, and tabbed pages. The most common widgets are ListView, GridView, Sliver, CustomScrollView, and TabBarView.
ListView can be created with a default constructor that takes a children list of widgets, which is suitable for a small, known number of items. Example:
class ListViewDemo01 extends StatelessWidget {
const ListViewDemo01({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView(
children:
[
Padding(padding: const EdgeInsets.all(8.0), child: Text("莫听穿林打叶声", style: TextStyle(fontSize: 20, color: Colors.redAccent))),
Padding(padding: const EdgeInsets.all(8.0), child: Text("何妨吟啸且徐行", style: TextStyle(fontSize: 20, color: Colors.redAccent))),
Padding(padding: const EdgeInsets.all(8.0), child: Text("竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生!", style: TextStyle(fontSize: 20, color: Colors.redAccent))),
],
);
}
}When a list item consists of an icon, title, subtitle, and trailing icon, ListTile simplifies the layout:
class ListViewDemo02 extends StatelessWidget {
const ListViewDemo02({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView(
children:
[
ListTile(leading: Icon(Icons.people, size: 36), title: Text("联系人"), subtitle: Text("联系人信息"), trailing: Icon(Icons.arrow_forward_ios)),
ListTile(leading: Icon(Icons.email, size: 36), title: Text("邮箱"), subtitle: Text("邮箱地址信息"), trailing: Icon(Icons.arrow_forward_ios)),
ListTile(leading: Icon(Icons.message, size: 36), title: Text("消息"), subtitle: Text("消息详情信息"), trailing: Icon(Icons.arrow_forward_ios)),
ListTile(leading: Icon(Icons.map, size: 36), title: Text("地址"), subtitle: Text("地址详情信息"), trailing: Icon(Icons.arrow_forward_ios)),
],
);
}
}For large or infinite lists, ListView.builder creates items lazily using an itemBuilder callback and an optional itemCount :
class ListViewDemo03 extends StatelessWidget {
const ListViewDemo03({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView.builder(
itemExtent: 50,
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("滑动列表演示$index"));
},
);
}
}ListView.separated adds a separatorBuilder to insert dividers between items, and ListView.custom allows a custom SliverChildDelegate for advanced scenarios.
GridView displays items in a matrix. Its default constructor requires a gridDelegate (e.g., SliverGridDelegateWithFixedCrossAxisCount or SliverGridDelegateWithMaxCrossAxisExtent ) and an optional children list.
class GridViewDemo01 extends StatelessWidget {
const GridViewDemo01({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, mainAxisSpacing: 10, crossAxisSpacing: 10),
children: List.generate(100, (index) => Container(color: Colors.blue)),
);
}
}Convenient shortcuts GridView.count and GridView.extent wrap the two common delegate types, while GridView.builder provides lazy loading similar to ListView.builder :
class GridViewDemo03 extends StatelessWidget {
const GridViewDemo03({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, mainAxisSpacing: 10, crossAxisSpacing: 10, childAspectRatio: 1.5),
itemBuilder: (context, index) => Container(color: Colors.red, alignment: Alignment.center, child: Text("GridView $index")),
);
}
}Sliver widgets are the building blocks of scrollable areas. Scrollable handles gestures, Viewport defines the visible region, and Sliver objects render the actual content. Common slivers include SliverList , SliverGrid , SliverAppBar , SliverPadding , and others.
class CustomScrollViewDemo01 extends StatelessWidget {
const CustomScrollViewDemo01({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverSafeArea(
sliver: SliverPadding(
padding: EdgeInsets.all(8),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, crossAxisSpacing: 2, mainAxisSpacing: 2, childAspectRatio: 1.5),
delegate: SliverChildBuilderDelegate((context, index) => Container(alignment: Alignment.center, color: Colors.red, child: Text("item $index")), childCount: 20),
),
),
),
],
);
}
}A more complex composition can combine SliverAppBar , SliverGrid , and SliverFixedExtentList to create a page with a collapsible header, a grid, and a fixed‑height list:
class HomeContent extends StatelessWidget {
const HomeContent({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverAppBar(expandedHeight: 250, flexibleSpace: FlexibleSpaceBar(title: Text("Sliver组合使用"), background: Image.network("https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg", fit: BoxFit.cover))),
SliverGrid(
delegate: SliverChildBuilderDelegate((context, index) => Container(alignment: Alignment.center, color: Colors.teal[100 * (index % 9)], child: Text("frid item $index")), childCount: 10),
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 200, mainAxisSpacing: 10, crossAxisSpacing: 10, childAspectRatio: 4),
),
SliverFixedExtentList(
delegate: SliverChildBuilderDelegate((context, index) => Container(alignment: Alignment.center, color: Colors.lightBlue[100 * (index % 9)], child: Text("list item $index")), childCount: 20),
itemExtent: 50,
),
],
);
}
}The lazy‑loading mechanism works because the default ListView uses SliverChildListDelegate , which holds a concrete list of widgets, while ListView.builder and GridView.builder use SliverChildBuilderDelegate that creates each child on demand via the supplied itemBuilder function.
Scroll event listening can be achieved with ScrollController (provides offset , jumpTo , animateTo ) and with NotificationListener to capture ScrollStartNotification , ScrollUpdateNotification , and ScrollEndNotification :
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State
{
ScrollController controller = ScrollController(initialScrollOffset: 300);
bool isShowFloatingButton = false;
@override
void initState() {
super.initState();
controller.addListener(() {
print("监听到滚动: ${controller.offset}");
setState(() { isShowFloatingButton = controller.offset >= 1000; });
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('ScrollController测试')),
body: ListView.builder(controller: controller, itemBuilder: (context, index) => ListTile(leading: Icon(Icons.people), title: Text('联系人$index'))),
floatingActionButton: isShowFloatingButton ? FloatingActionButton(child: Icon(Icons.arrow_upward), onPressed: () { controller.animateTo(0, duration: Duration(seconds: 1), curve: Curves.easeIn); }) : null,
);
}
} class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State
{
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('NotificationListener测试')),
body: NotificationListener
(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollStartNotification) print('开始滚动');
else if (scrollNotification is ScrollUpdateNotification) print('正在滚动:${scrollNotification.metrics.pixels}');
else if (scrollNotification is ScrollEndNotification) print('结束滚动');
return false;
},
child: ListView.builder(itemBuilder: (context, index) => ListTile(leading: Icon(Icons.people), title: Text('联系人$index'))),
),
);
}
}TabBarView works together with TabBar and a shared TabController . If no controller is supplied, the nearest DefaultTabController in the widget tree is used.
class _HomePageState extends State
{
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: ScrollableState());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('TabBarView测试'), bottom: TabBar(controller: _tabController, tabs: [Tab(text: '新闻'), Tab(text: '历史'), Tab(text: '图片')] ),
body: TabBarView(controller: _tabController, children: [
Center(child: Text('新闻', textScaleFactor: 5)),
Center(child: Text('历史', textScaleFactor: 5)),
Center(child: Text('图片', textScaleFactor: 5)),
]),
);
}
} class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(title: Text('TabBarView测试'), bottom: TabBar(tabs: [Tab(text: '新闻'), Tab(text: '历史'), Tab(text: '图片')])),
body: TabBarView(children: [
Center(child: Text('新闻', textScaleFactor: 5)),
Center(child: Text('历史', textScaleFactor: 5)),
Center(child: Text('图片', textScaleFactor: 5)),
]),
),
);
}
}These examples demonstrate how to build efficient, scrollable UIs in Flutter, choose the appropriate constructor for performance, and handle user interaction through controllers and listeners.
IEG Growth Platform Technology Team
Official account of Tencent IEG Growth Platform Technology Team, showcasing cutting‑edge achievements across front‑end, back‑end, client, algorithm, testing and other domains.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.