Search code examples
dartflutter

SliverAppbar remains visible when CustomScrollView with ScrollController is scrolled


Adding a ScrollController to function timelineList() causes the SliverAppBar to remain visible on scroll (when counter list is scrolled, SliverAppBar should hide). The issue goes away if the _scrollController is removed from the list (see timelineList function) but this gives rise to a new problem, I need to listen for when the scrollbar reaches bottom (to get more content).

See sample app below, copy/paste and run.

void main() => runApp(TestApp());
class TestApp extends StatelessWidget {
  final _scrollController = new ScrollController();
  TestApp(){
    _scrollController.addListener(() {
      if (_scrollController.position.pixels ==
          _scrollController.position.maxScrollExtent) {
        print('Get more data');
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    TestBloc bloc = TestBloc();
    bloc.fetchTestTimeline();
    bloc.fetchTestAppBarTxt1();
    bloc.fetchTestAppBarTxt2();
    return MaterialApp(
      home: new Scaffold(
          backgroundColor: Colors.grey[200],
          appBar: AppBar(
            backgroundColor: Colors.blueGrey,
            elevation: 0.0,
          ),
          body: NestedScrollView(
            headerSliverBuilder:
                (BuildContext contrxt, bool innerBoxIsScrolled) {
              return <Widget>[
                buildSliverAppBar(context, bloc),
              ];
            },
            body: Column(
              children: <Widget>[
                timelineList(bloc),
              ],
            )

          )),
    );
  }

  buildSliverAppBar(context, TestBloc bloc){
    return SliverAppBar(
        automaticallyImplyLeading: false,
        backgroundColor: Colors.grey[400],
        expandedHeight: 200.0,
        floating: true,
        snap: true,
        flexibleSpace: FlexibleSpaceBar(
          background: Column(
            children: <Widget>[
              Container(
                padding: EdgeInsets.only(left: 2.0),
                height: 200,
                child: Column(
                  children: <Widget>[
                    StreamBuilder(
                        stream: bloc.testAppBarTxt1,
                        initialData: null,
                        builder: (BuildContext context,
                            AsyncSnapshot<String> snapshot) {
                          if (snapshot.data == null)
                            return buildProgressIndicator(true);
                          return Expanded(
                              child: Text('${snapshot.data}'));
                        }),
                    StreamBuilder(
                        stream: bloc.testAppBarTxt2,
                        initialData: null,
                        builder: (BuildContext context,
                            AsyncSnapshot<String> snapshot) {
                          if (snapshot.data == null)
                            return buildProgressIndicator(true);
                          return Expanded(
                              child: Text('${snapshot.data}'));
                        }),
                  ],
                ),
              )
            ],
          ),
        ));
  }

  timelineList(TestBloc bloc) {
    return StreamBuilder(
        stream: bloc.getTestTimeline,
        initialData: null,
        builder: (BuildContext context, AsyncSnapshot<List<int>> snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Expanded(child: buildProgressIndicator(true));
          }

          List<int> val = snapshot.data;

          if (val.isNotEmpty) {
            addToTimelineList(val, bloc);
            return Expanded(
              child: CustomScrollView(
                controller: _scrollController,
                slivers: <Widget>[
                  SliverList(
                      delegate: SliverChildListDelegate(new List<Widget>.generate(bloc.listTest.length, (int index) {
                        if (index == bloc.listTest.length) {
                          return buildProgressIndicator(bloc.isPerformingRequest);
                        } else {
                          return bloc.listTest[index];
                        }
                      })
                      ))
                ],

              ),
            );
          }
        });
  }
  void addToTimelineList(List<int> list, TestBloc bloc) {
    for (var val in list) {
      bloc.listTest.add(Text('$val'));
    }
  }
}

Widget buildProgressIndicator(showIndicator) {
  return new Padding(
    padding: const EdgeInsets.all(8.0),
    child: new Center(
      child: new Opacity(
        opacity: showIndicator ? 1.0 : 0.0,
        child: Container(
            width: 10.0,
            height: 10.0,
            child: new CircularProgressIndicator(
            )),
      ),
    ),
  );
}
class TestBloc {
  String appbar1Val;
  String appbar2Val;
  List<Text> listTest = new List<Text>();
  bool isPerformingRequest = false;
  final _testAppBarText1 = BehaviorSubject<String>();

  Observable<String> get testAppBarTxt1 => _testAppBarText1.stream;
  final _testAppBarText2 = BehaviorSubject<String>();

  Observable<String> get testAppBarTxt2 => _testAppBarText2.stream;
  final _testTimeline = PublishSubject<List<int>>();

  Observable<List<int>> get getTestTimeline => _testTimeline.stream;

  fetchTestTimeline() async {
    List item = await Future.delayed(
        Duration(seconds: 2), () => List<int>.generate(100, (i) => i));
    _testTimeline.sink.add(item);
  }

  fetchTestAppBarTxt1() async {
    appbar1Val = await Future.delayed(Duration(seconds: 2), () => "Text One");

    _testAppBarText1.sink.add(appbar1Val);
  }

  fetchTestAppBarTxt2() async {
    appbar2Val = await Future.delayed(Duration(seconds: 2), () => "Text Two");
    _testAppBarText2.sink.add(appbar2Val);
  }
  dispose() {
    _testAppBarText1.close();
    _testAppBarText2.close();
    _testTimeline.close();
  }
}

Solution

  • It's possible to achive the same result with wrapping your list with a Notification Listener.

    NotificationListener<ScrollNotification>(
                    onNotification: (sn) {
                      if (sn.metrics.pixels ==
                          sn.metrics.maxScrollExtent) {
                        print('Get more data');
                      }
                    },
                    child: CustomScrollView(...
    

    Edit: Since my initial answer didn't cover the animateTo use case, I got it working by removing the outer NestedScrollView. Here is the modified example.

    void main() => runApp(TestApp());
    
    class TestApp extends StatelessWidget {
      final _scrollController = new ScrollController();
      TestApp() {
        _scrollController.addListener(() {
          if (_scrollController.position.pixels ==
              _scrollController.position.maxScrollExtent) {
            print('Get more data');
          }
        });
      }
    
      @override
      Widget build(BuildContext context) {
        TestBloc bloc = TestBloc();
        bloc.fetchTestTimeline();
        bloc.fetchTestAppBarTxt1();
        bloc.fetchTestAppBarTxt2();
        return MaterialApp(
          home: new Scaffold(
            backgroundColor: Colors.grey[200],
            appBar: AppBar(
              backgroundColor: Colors.blueGrey,
              elevation: 0.0,
            ),
            body: Column(
              children: <Widget>[
                timelineList(bloc),
              ],
            ),
          ),
        );
      }
    
      buildSliverAppBar(context, TestBloc bloc) {
        return SliverAppBar(
            automaticallyImplyLeading: false,
            backgroundColor: Colors.grey[400],
            expandedHeight: 200.0,
            floating: true,
            snap: true,
            flexibleSpace: FlexibleSpaceBar(
              background: Column(
                children: <Widget>[
                  Container(
                    padding: EdgeInsets.only(left: 2.0),
                    height: 200,
                    child: Column(
                      children: <Widget>[
                        StreamBuilder(
                            stream: bloc.testAppBarTxt1,
                            initialData: null,
                            builder: (BuildContext context,
                                AsyncSnapshot<String> snapshot) {
                              if (snapshot.data == null)
                                return buildProgressIndicator(true);
                              return Expanded(child: Text('${snapshot.data}'));
                            }),
                        StreamBuilder(
                            stream: bloc.testAppBarTxt2,
                            initialData: null,
                            builder: (BuildContext context,
                                AsyncSnapshot<String> snapshot) {
                              if (snapshot.data == null)
                                return buildProgressIndicator(true);
                              return Expanded(child: Text('${snapshot.data}'));
                            }),
                      ],
                    ),
                  )
                ],
              ),
            ));
      }
    
      timelineList(TestBloc bloc) {
        return StreamBuilder(
            stream: bloc.getTestTimeline,
            initialData: null,
            builder: (BuildContext context, AsyncSnapshot<List<int>> snapshot) {
              if (snapshot.connectionState == ConnectionState.waiting) {
                return Expanded(child: buildProgressIndicator(true));
              }
    
              List<int> val = snapshot.data;
    
              if (val.isNotEmpty) {
                addToTimelineList(val, bloc);
                return Expanded(
                  child: CustomScrollView(
                    controller: _scrollController,
                    slivers: <Widget>[
                      buildSliverAppBar(context, bloc),
                      SliverList(
                          delegate: SliverChildListDelegate(
                              new List<Widget>.generate(bloc.listTest.length,
                                  (int index) {
                        if (index == bloc.listTest.length) {
                          return buildProgressIndicator(bloc.isPerformingRequest);
                        } else {
                          return bloc.listTest[index];
                        }
                      })))
                    ],
                  ),
                );
              }
            });
      }
    
      void addToTimelineList(List<int> list, TestBloc bloc) {
        for (var val in list) {
          bloc.listTest.add(Text('$val'));
        }
      }
    }
    
    Widget buildProgressIndicator(showIndicator) {
      return new Padding(
        padding: const EdgeInsets.all(8.0),
        child: new Center(
          child: new Opacity(
            opacity: showIndicator ? 1.0 : 0.0,
            child: Container(
                width: 10.0, height: 10.0, child: new CircularProgressIndicator()),
          ),
        ),
      );
    }
    
    class TestBloc {
      String appbar1Val;
      String appbar2Val;
      List<Text> listTest = new List<Text>();
      bool isPerformingRequest = false;
      final _testAppBarText1 = BehaviorSubject<String>();
    
      Observable<String> get testAppBarTxt1 => _testAppBarText1.stream;
      final _testAppBarText2 = BehaviorSubject<String>();
    
      Observable<String> get testAppBarTxt2 => _testAppBarText2.stream;
      final _testTimeline = PublishSubject<List<int>>();
    
      Observable<List<int>> get getTestTimeline => _testTimeline.stream;
    
      fetchTestTimeline() async {
        List item = await Future.delayed(
            Duration(seconds: 2), () => List<int>.generate(100, (i) => i));
        _testTimeline.sink.add(item);
      }
    
      fetchTestAppBarTxt1() async {
        appbar1Val = await Future.delayed(Duration(seconds: 2), () => "Text One");
    
        _testAppBarText1.sink.add(appbar1Val);
      }
    
      fetchTestAppBarTxt2() async {
        appbar2Val = await Future.delayed(Duration(seconds: 2), () => "Text Two");
        _testAppBarText2.sink.add(appbar2Val);
      }
    
      dispose() {
        _testAppBarText1.close();
        _testAppBarText2.close();
        _testTimeline.close();
      }
    }