RefreshIndicator with NestedScrollview

Viewed 4787

I want 2 tab pages with a ListView each to share a single RefreshIndicator. However, a RefreshIndicator must have Scrollable as a child (which a TabBarView isn't) so instead I tried making 2 RefreshIndicators per tab as shown in the code below.

But this brings a different problem, I also wanted a floating AppBar which meant I had to use a NestedScrollView. So as a result I end up triggering both RefreshIndicators' onRefresh method whenever I scroll down. Whereas I only need one to refresh.

import 'package:flutter/material.dart';

main() {
    runApp(
        MaterialApp(
            home: DefaultTabController(
                length: 2,
                child: Scaffold(
                    body: NestedScrollView(
                        headerSliverBuilder: (context, innerBoxIsScrolled) {
                            return [
                                SliverAppBar(
                                    floating: true,
                                    snap: true,
                                    bottom: TabBar(
                                        tabs: [
                                            Tab(text: 'Page1'),
                                            Tab(text: 'Page2'),
                                        ],
                                    ),
                                ),
                            ];
                        },
                        body: TabBarView(
                            children: [
                                Page(1),
                                Page(2),
                            ],
                        ),
                    ),
                ),
            ),
        ),
    );
}

class Page extends StatefulWidget {
    final pageNumber;
    Page(this.pageNumber);
    createState() => PageState();
}

class PageState extends State<Page> with AutomaticKeepAliveClientMixin {
    get wantKeepAlive => true;

    build(context){
        super.build(context);
        return RefreshIndicator(
            onRefresh: () => Future(() async {
                print('Refreshing page no. ${widget.pageNumber}');  // This prints twice once both tabs have been opened
                await Future.delayed(Duration(seconds: 5));
            }),
            child: ListView.builder(
                itemBuilder: ((context, index){
                    return ListTile(
                        title: Text('Item $index')
                    );
                }),
            )
        );
    }
}

The AutomaticKeepAliveClientMixin is there to prevent the pages rebuilding every time I switch tabs as this would be an expensive process in my actual app.

A solution that uses a single RefreshIndicator for both tabs would be most ideal, but any help is appreciated.

3 Answers

This code is working for me, it's mostly built on top of @Nuts answer, but it needed some tweaking and my edit was rejected

DefaultTabController(
      length: tabs.length,
      child: RefreshIndicator(
        notificationPredicate: (notification) {
          // with NestedScrollView local(depth == 2) OverscrollNotification are not sent
          return notification.depth == 2;
        },
        onRefresh: () => Future.value(null),
        child: NestedScrollView(
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
            return [
              SliverAppBar(...)
            ];
          },
          body: TabBarView(
            children: tabs,
          ),
        ),
      ),
    )

Could wrap whole NestedScrollView with RefreshIndicator and update notificationPredicate:

       DefaultTabController(
          length: tabs.length,
          child: RefreshIndicator(
            notificationPredicate: (notification) {
              // with NestedScrollView local(depth == 2) OverscrollNotification are not sent
              if (notification is OverscrollNotification || Platform.isIOS) {
                return notification.depth == 2;
              }
              return notification.depth == 0;
            },
            onRefresh: () => Future.value(null),
            child: NestedScrollView(
              headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
                return [
                  SliverAppBar(...)
                ];
              },
              body: TabBarView(
                children: tabs,
              ),
            ),
          ),
        )

If you want floating app bar then you have to use nested scroll view and sliver app bar . When you try to use refresh indicator in a list which a child of tab bar view , refresh indicator doesn't work. This is just because of the nested scroll view .

If you have suppose two lists as child of tab bar view, you want to refresh only one or both at a time then follow the below code.

Wrap the nested scroll view with refresh indicator then on refresh part ,

RefreshIndicator(
    color: Colors.red,
    displacement: 70,


    onRefresh: _refreshGeneralList,
    key: _refreshIndicatorKey,
    child: NestedScrollView(


      headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
        return <Widget>[

          SliverAppBar(

            centerTitle: true,
            title: Text(
              "App Bar",
              style: TextStyle(
                  color: Colors.black,
                  fontSize: 14,
            ),
            leading: Padding(
              padding: const EdgeInsets.only(left: 8.0),
              child: IconButton(
                icon: Icon(Icons.profile),
                onPressed: () {
                  Navigator.of(context).pop();
                },
              ),
            ),
            actions: [
              InkWell(
                  onTap: () {
                    setState(() {
                      isPremium = !isPremium;
                    });
                  },
                  child: Icon(
                    Icons.monetization_on,
                    color: isPremium ? Colors.green : Colors.blueGrey,
                    size: 33,
                  )),
              SizedBox(
                width: 25,
              )
            ],

            elevation: 0,
            backgroundColor: Colors.white,
            pinned: true,
            floating: true,
            forceElevated: innerBoxIsScrolled,
            bottom: isPremium
                ? TabBar(

                labelStyle: TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.w600),
                labelColor: Colors.blueGrey,
                indicatorColor:Colors.red,
                unselectedLabelColor:
                Colors.green,
                labelPadding: EdgeInsets.symmetric(vertical: 13.5),
                controller: _tabController,
                tabs: [
                  Text(
                    "General",
                  ),
                  Text(
                    "Visitors",
                  ),
                ])
                : null,
          )
        ];
      },

      body: isPremium
          ? TabBarView(

          controller: _tabController,
          children: [
        generalNotificationsList(context),
        visitorsNotificationsList(context),
      ])
          : generalNotificationsList(context),
    ),
  ),

add a function which calls a future. In the future part we will write the code if one child or two child of tab bar view will be scrolled.

 Future _refreshGeneralList() async{
print('refreshing ');
GeneralNotificationBloc().add(LoadGeneralNotificationEvent(context));
PremiumNotificationBloc().add(LoadPremiumNotificationEvent(context));
return Future.delayed(Duration(seconds: 1));

}

Related