Mobile Development 21 min read

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.

IEG Growth Platform Technology Team
IEG Growth Platform Technology Team
IEG Growth Platform Technology Team
Flutter Scrollable Widgets: ListView, GridView, Sliver, CustomScrollView, and TabBarView

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.

Fluttermobile developmentlistviewsliverGridViewScrollable Widgets
IEG Growth Platform Technology Team
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.