Flutter - How to make a widget be created when it's scrolled onto the screen

Viewed 779

I created a custom widget that adds a footer as last item to a received scrollable widget. Everything works fine, but I want to change the time the footer is being created.

Like ListView.builder creates items as they're scrolled onto the screen, I want that happen with the footer too. If this custom widget receives a ListView.builder or GridView.builder, their items are being created fine as they're scrolled onto the screen, but the footer is created the first time even if it's not on the screen.

Here is the custom widget, where the received widget is widget.child (ScrollView) and the footer is widget.footer (Widget).

@override
Widget build(BuildContext context) {
  return Scrollable(
    controller: widget.child.controller,
    axisDirection: widget.child.getDirection(context),
    viewportBuilder: (context, offset) {
      return ShrinkWrappingViewport(
        axisDirection: widget.child.getDirection(context),
        offset: offset,
        slivers: widget.child.buildSlivers(context)
          ..add(
              SliverList(
                  delegate: SliverChildBuilderDelegate((context, index) {
            print('footer');
            return widget.footer;
          }, childCount: 1))),
      );
    },
  );
}

This is how I'm using it:

ScrollableWithFooter(
  child: ListView.builder(
    itemCount: 50,
    itemBuilder: (context, index) {
      print(index);
      return SizedBox(height: 400, child: Text('Item $index'));
    },
  ),
  footer: Center(child: Text('Footer')),
)

With a list of 50, I'm seeing the first items on the screen and this is the output of the prints:

I/flutter (28472): 0
I/flutter (28472): 1
I/flutter (28472): 2
I/flutter (28472): footer

And when I start scrolling, the other items start printing their indexes as they're created:

I/flutter (28472): 2
I/flutter (28472): footer
I/flutter (28472): 3
I/flutter (28472): 4
I/flutter (28472): 5

This is correct for the items but not for the footer. I want the footer to be created when I reached the end of the list, like the other items. How can achieve that?

Note that on the screen everything works fine, the footer appears at the end of the list. It's only its creation that happens before it should.


This is something I tried, but it doesn't work:

@override
Widget build(BuildContext context) {
  return Scrollable(
    controller: widget.child.controller,
    axisDirection: widget.child.getDirection(context),
    viewportBuilder: (context, offset) {
      List<Widget> slivers = widget.child.buildSlivers(context);
      return ShrinkWrappingViewport(
        axisDirection: widget.child.getDirection(context),
        offset: offset,
        slivers: [
          SliverList(
              delegate: SliverChildBuilderDelegate((context, index) {
            if (index < slivers.length) {
              return slivers[index]; // <- Here seems to be the problem
            }
            print('footer');
            return widget.footer;
          }, childCount: slivers.length + 1))
        ],
      );
    },
  );
}

I get this error:

The following assertion was thrown building NotificationListener<KeepAliveNotification>:
I/flutter (28472): A RenderRepaintBoundary expected a child of type RenderBox but received a child of type
I/flutter (28472): RenderSliverPadding.
2 Answers

you can add a listener to the controller of the listview , it will listen to the scrolling and when you hit the bottom you shall active some flag called footer ,, and then show the footer

I found a way to do this. I was doing it well, the thing is for some reason the SliverChildBuilderDelegate always creates the first 2 elements even if they're not scrolled onto the screen. So I had to create 2 dummy invisible footers and then the real footer, this way the real footer will be created when is scrolled onto the screen:

@override
Widget build(BuildContext context) {
return Scrollable(
  controller: widget.child.controller,
  axisDirection: widget.child.getDirection(context),
  viewportBuilder: (context, offset) {
    return ShrinkWrappingViewport(
      axisDirection: widget.child.getDirection(context),
      offset: offset,
      slivers: widget.child.buildSlivers(context)
        ..add(SliverList(
            delegate: SliverChildBuilderDelegate((context, index) {
          if (index < 2) {
            print('dummy footer');
            return SizedBox.shrink();
          }
          print('footer');
          return widget.footer;
        }, childCount: 3))),
    );
  },
);

And this would be the final solution without the prints:

@override
Widget build(BuildContext context) {
  return Scrollable(
    controller: widget.child.controller,
    axisDirection: widget.child.getDirection(context),
    viewportBuilder: (context, offset) {
      return ShrinkWrappingViewport(
        axisDirection: widget.child.getDirection(context),
        offset: offset,
        slivers: widget.child.buildSlivers(context)
          ..add(SliverList(
              delegate: SliverChildBuilderDelegate(
                  (_, index) => index < 2 ? SizedBox.shrink() : widget.footer,
                  childCount: 3))),
      );
    },
  );
}

Now the custom widget adds a footer at the bottom of any scroll view efficiently, being created both the items of the scroll view and the footer only when they're scrolled onto the screen.

If someone finds a better way to do this let me know. For now, I'm okay with this solution.

Related