Flutter components–basic use of Slivers

We consider a layout like this: a sliding view includes a header view (HeaderView), a list view (ListView), and a grid view (GridView).

How can we make them achieve a unified sliding effect? It’s hard to do using the front scroll.

There is a Widget in Flutter that can accomplish such a scrolling effect: CustomScrollView, which can manage multiple scrolling views in a unified manner.

In CustomScrollView, each independent, scrollable Widget is called Sliver.

1. Basic use of Slivers

Because we need to put a lot of slivers in a CustomScrollView, so CustomScrollView has a slivers property, which allows us to put some corresponding slivers:

  • SliverList: similar to the ListView we have used before;

  • SliverFixedExtentList: Similar to SliverList, only the height of scrolling can be set;

  • SliverGrid: similar to the GridView we have used before;

  • SliverPadding: Set the inner margin of Sliver, because it may be necessary to set the inner margin for Sliver separately;

  • SliverAppBar: Add an AppBar, usually used as the HeaderView of CustomScrollView;

  • SliverSafeArea: Set the content to be displayed in a safe area (such as not letting Qi Liuhai block our content)

1.1. Combination of SliverGrid + SliverPadding + SliverSafeArea

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: <Widget>[
        SliverSafeArea(
          sliver: SliverPadding(
            padding: EdgeInsets. all(8),
            sliver: SliverGrid(
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2,
                crossAxisSpacing: 8,
                mainAxisSpacing: 8,
              ),
              delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
                  return Container(
                    alignment: Alignment(0, 0),
                    color: Colors.orange,
                    child: Text("item$index"),
                  );
                },
                childCount: 20
              ),
            ),
          ),
        )
      ],
    );
  }
}

1.2. Combination of Slivers

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return showCustomScrollView();
  }

  Widget showCustomScrollView() {
    return new CustomScrollView(
      slivers: <Widget>[
        const SliverAppBar(
          expandedHeight: 250.0,
          flexibleSpace: FlexibleSpaceBar(
            title: Text('Coderwhy Demo'),
            background: Image(
              image: NetworkImage(
                "https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg",
              ),
              fit: BoxFit. cover,
            ),
          ),
        ),
        new SliverGrid(
          gridDelegate: new SliverGridDelegateWithMaxCrossAxisExtent(
            maxCrossAxisExtent: 200.0,
            mainAxisSpacing: 10.0,
            crossAxisSpacing: 10.0,
            childAspectRatio: 4.0,
          ),
          delegate: new SliverChildBuilderDelegate(
                (BuildContext context, int index) {
              return new Container(
                alignment: Alignment. center,
                color: Colors.teal[100 * (index % 9)],
                child: new Text('grid item $index'),
              );
            },
            childCount: 10,
          ),
        ),
        SliverFixedExtentList(
          itemExtent: 50.0,
          delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
              return new Container(
                alignment: Alignment. center,
                color: Colors.lightBlue[100 * (index % 9)],
                child: new Text('list item $index'),
              );
            },
            childCount: 20
          ),
        ),
      ],
    );
  }
}

2. Listen to scrolling events

For scrolling views, we often need to listen to some of its scrolling events, and do some corresponding things when listening.

For example, when the view scrolls to the bottom, we may want to do a pull-up to load more;

For example, when scrolling to a certain position, a button to return to the top is displayed, click the button to return to the top, and return to the top;

For example, monitor when scrolling starts and ends;

Monitoring scrolling-related content in Flutter consists of two parts: ScrollController and ScrollNotification.

2.1.ScrollController monitoring

The component controller of ListView and GridView is ScrollController, through which we can obtain the scrolling information of the view, and call the methods inside to update the scrolling position of the view.

In addition, under normal circumstances, we will change some Widget state information according to the scrolling position, so ScrollController is usually used together with StatefulWidget, and will control its initialization, monitoring, destruction and other events in it.

Let’s make a case, when scrolling to the 1000 position, display a back to top button:

  • jumpTo(double offset), animateTo(double offset,...): These two methods are used to jump to the specified position, the difference is that the latter The former will perform an animation when jumping, while the former will not.

  • ScrollController indirectly inherits from Listenable, and we can listen to scrolling events according to ScrollController.

class MyHomePage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => MyHomePageState();
}

class MyHomePageState extends State<MyHomePage> {
  ScrollController_controller;
  bool _isShowTop = false;
  
  @override
  void initState() {
    // Initialize ScrollController
    _controller = ScrollController();
    
    // listen for scrolling
    _controller. addListener(() {
      var tempSsShowTop = _controller.offset >= 1000;
      if (tempSsShowTop != _isShowTop) {
        setState(() {
          _isShowTop = tempSsShowTop;
        });
      }
    });
    
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("ListView display"),
      ),
      body: ListView.builder(
        itemCount: 100,
        itemExtent: 60,
        controller: _controller,
        itemBuilder: (BuildContext context, int index) {
          return ListTile(title: Text("item$index"));
        }
      ),
      floatingActionButton: !_isShowTop ? null : FloatingActionButton(
        child: Icon(Icons. arrow_upward),
        onPressed: () {
          _controller.animateTo(0, duration: Duration(milliseconds: 1000), curve: Curves.ease);
        },
      ),
    );
  }
}

2.2. NotificationListener listening

If we want to monitor when scrolling starts and when scrolling ends, we can use NotificationListener at this time.

  • NotificationListener is a Widget. The template parameter T is the notification type to be monitored. If omitted, all types of notifications will be monitored. If a specific type is specified, only notifications of this type will be monitored.

  • NotificationListener needs an onNotification callback function to implement the listening processing logic.

  • The callback can return a Boolean value, representing whether to prevent the event from bubbling up. If it is true, the bubbling is terminated and the event stops propagating upward. If it does not return or the return value is When false, bubbling continues.

class MyHomeNotificationDemo extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => MyHomeNotificationDemoState();
}

class MyHomeNotificationDemoState extends State<MyHomeNotificationDemo> {
  int_progress = 0;

  @override
  Widget build(BuildContext context) {
    return NotificationListener(
      onNotification: (ScrollNotification notification) {
        // 1. Determine the type of listening event
        if (notification is ScrollStartNotification) {
          print("Start scrolling...");
        } else if (notification is ScrollUpdateNotification) {
          // The current scroll position and total length
          final currentPixel = notification.metrics.pixels;
          final totalPixel = notification.metrics.maxScrollExtent;
          double progress = currentPixel / totalPixel;
          setState(() {
            _progress = (progress * 100).toInt();
          });
          print("Scrolling: ${notification.metrics.pixels} - ${notification.metrics.maxScrollExtent}");
        } else if (notification is ScrollEndNotification) {
          print("End scrolling...");
        }
        return false;
      },
      child: Stack(
        alignment: Alignment(.9, .9),
        children: <Widget>[
          ListView.builder(
            itemCount: 100,
            itemExtent: 60,
            itemBuilder: (BuildContext context, int index) {
              return ListTile(title: Text("item$index"));
            }
          ),
          CircleAvatar(
            radius: 30,
            child: Text("$_progress%"),
            backgroundColor: Colors. black54,
          )
        ],
      ),
    );
  }
}