Yura
Yura

Reputation: 2248

How to implement listview lazyload inside a NestedScrollView?

I have an app, it has a page that act as an entry point and showing a TabView containing 3 or more pages on it. It uses NestedScrollView and SliverAppBar to give some animation when user scroll the view.

I want to implement lazy load of a paginated list but since it does not allows me to use a controller inside the CustomScrollView as mentioned in the docs in this line:

builder: (BuildContext context) {
  return CustomScrollView(
    // The "controller" and "primary" members should be left
    // unset, so that the NestedScrollView can control this
    // inner scroll view.
    // If the "controller" property is set, then this scroll
    // view will not be associated with the NestedScrollView.
    // The PageStorageKey should be unique to this ScrollView;
    // it allows the list to remember its scroll position when
    // the tab view is not on the screen.
    key: PageStorageKey<String>(name),
    slivers: <Widget>[

I cannot make use of ScrollController in the child page to get the scroll value to trigger the loadMore function. Fortunately, there is a similar widget to listen the scroll event called ScrollNotification. But I don't know which property is holding the value of the maximum scroll limit.

Tried to compare the available properties by this:

bool _onScrollNotification(ScrollNotification notification) {
  if (notification is! ScrollEndNotification) return false;

  print('extentBefore: ${notification.metrics.extentBefore}');
  print('extentAfter: ${notification.metrics.extentAfter}');
  print('maxScrollExtent: ${notification.metrics.maxScrollExtent}');
  return true;
}

But its seems like they doesn't hold any fixed value as I need. It always changed its value independently.

I also cannot use the ScrollController on the parent page (the tabview_holder) since each page in each tabs has independent bloc, events, data & fetching algorithm. With that in mind, how can I achieve this requirement?

Please have a look at my script:

tabview_holder.dart (not a real file name, just to illustrate it)

class EventPage extends StatefulWidget {
  EventPage({Key key}) : super(key: key);

  @override
  _EventPageState createState() => _EventPageState();
}

class _EventPageState extends State<EventPage>
    with SingleTickerProviderStateMixin {
  final ScrollController _scrollController = ScrollController();
  final List<Widget> _tabs = [
    Tab(text: 'My Events'),
    Tab(text: "Private Events"),
    Tab(text: "Division Events"),
    Tab(text: "Department Events"),
    Tab(text: "Public Events"),
  ];

  double _bottomNavigatorPosition = 0.0;
  double _gradientStop = 0.2;
  TabController _tabController;

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_scrollListener);
    _tabController = TabController(
      initialIndex: 0,
      length: _tabs.length,
      vsync: this,
    );
  }

  @override
  void dispose() {
    _scrollController.dispose();
    _tabController.dispose();
    super.dispose();
  }

  void _scrollListener() {
    ScrollDirection direction = _scrollController.position.userScrollDirection;
    switch (direction) {
      case ScrollDirection.reverse:
        setState(() {
          _gradientStop = 0.0;
          _bottomNavigatorPosition = -100.0;
        });
        return;
        break;

      case ScrollDirection.forward:
      case ScrollDirection.idle:
        setState(() {
          _gradientStop = 0.2;
          _bottomNavigatorPosition = 0.0;
        });
        break;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Stack(
          children: [
            NestedScrollView(
              controller: _scrollController,
              headerSliverBuilder:
                  (BuildContext context, bool innerBoxIsScrolled) {
                return <Widget>[
                  SliverOverlapAbsorber(
                    handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
                        context),
                    sliver: SliverAppBar(
                      backgroundColor:
                          Theme.of(context).scaffoldBackgroundColor,
                      automaticallyImplyLeading: false,
                      floating: true,
                      expandedHeight: 100,
                      flexibleSpace: FlexibleSpaceBar(
                        background: Container(
                          child: Stack(
                            children: [
                              Positioned(
                                left: 30.0,
                                bottom: 10,
                                child: PageHeader(title: 'Events'),
                              ),
                            ],
                          ),
                        ),
                      ),
                    ),
                  ),
                  SliverPersistentHeader(
                    pinned: true,
                    delegate: _SliverAppBarDelegate(
                      TabBar(
                        controller: _tabController,
                        isScrollable: true,
                        indicator: BubbleTabIndicator(
                          indicatorHeight: 35.0,
                          indicatorColor: Theme.of(context).primaryColor,
                          tabBarIndicatorSize: TabBarIndicatorSize.tab,
                        ),
                        tabs: _tabs,
                      ),
                    ),
                  ),
                ];
              },
              body: TabBarView(
                controller: _tabController,
                children: [
                  MyEventsPage(),
                  PrivateEventsPage(),
                  MyEventsPage(),
                  MyEventsPage(),
                  MyEventsPage(),
                ],
              ),
            ),
            _buildBottomGradient(),
            _buildBottomNavigator(),
          ],
        ),
      ),
    );
  }

  Widget _buildBottomGradient() {
    return IgnorePointer(
      child: AnimatedContainer(
        duration: Duration(milliseconds: 200),
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.bottomCenter,
            end: Alignment.topCenter,
            stops: [_gradientStop / 2, _gradientStop],
            colors: [
              Color(0xFF121212),
              Colors.transparent,
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildBottomNavigator() {
    return AnimatedPositioned(
      duration: Duration(milliseconds: 200),
      left: 0.0,
      right: 0.0,
      bottom: _bottomNavigatorPosition,
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 20.0),
        child: PageNavigator(
          primaryButtonText: 'Create new event',
          onPressedPrimaryButton: () {
            Navigator.of(context).pushNamed(Routes.EVENT_CREATE);
          },
        ),
      ),
    );
  }
}

tabview_item.dart

class MyEventsPage extends StatefulWidget {
  MyEventsPage({Key key}) : super(key: key);

  @override
  _MyEventsPageState createState() => _MyEventsPageState();
}

class _MyEventsPageState extends State<MyEventsPage>
    with AutomaticKeepAliveClientMixin<MyEventsPage> {
  Completer<void> _refreshCompleter;
  PaginatedEvent _paginated;
  MyEventsBloc _myEventsBloc;
  bool _isFetchingMoreInBackground;

  @override
  void initState() {
    super.initState();
    _myEventsBloc = BlocProvider.of<MyEventsBloc>(context);
    _myEventsBloc.add(MyEventsPageInitialized());
    _refreshCompleter = Completer<void>();
    _isFetchingMoreInBackground = false;
  }

  void _set(PaginatedEvent paginated) {
    setState(() {
      _paginated = paginated;
    });
    _refreshCompleter?.complete();
    _refreshCompleter = Completer();
  }

  void _add(Event data) {
    setState(() {
      _paginated.data.add(data);
    });
  }

  void _update(Event data) {
    final int index = _paginated.data.indexWhere((leave) {
      return leave.id == data.id;
    });

    setState(() {
      _paginated.data[index] = data;
    });
  }

  void _destroy(Event data) {
    final int index = _paginated.data.indexWhere((leave) {
      return leave.id == data.id;
    });

    setState(() {
      _paginated.data.removeAt(index);
    });
  }

  void _append(PaginatedEvent paginated) {
    setState(() {
      _paginated.currentPage = paginated.currentPage;
      _paginated.data.addAll(paginated.data);
    });
  }

  bool _onScrollNotification(ScrollNotification notification) {
    if (notification is! ScrollEndNotification) return false;

    print('extentBefore: ${notification.metrics.extentBefore}');
    print('extentAfter: ${notification.metrics.extentAfter}');
    print('maxScrollExtent: ${notification.metrics.maxScrollExtent}');
    return true;
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return RefreshIndicator(
      onRefresh: () {
        _myEventsBloc.add(MyEventsRefreshRequested());
        return _refreshCompleter.future;
      },
      child: NotificationListener<ScrollNotification>(
        onNotification: _onScrollNotification,
        child: CustomScrollView(
          slivers: [
            SliverOverlapInjector(
              handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
            ),
            SliverToBoxAdapter(
              child: BlocConsumer<MyEventsBloc, MyEventsState>(
                listener: (context, state) {
                  if (state is MyEventsLoadSuccess) {
                    _set(state.data);
                  }

                  if (state is MyEventsCreateSuccess) {
                    _add(state.data);
                  }

                  if (state is MyEventsUpdateSuccess) {
                    _update(state.data);
                  }

                  if (state is MyEventsDestroySuccess) {
                    _destroy(state.data);
                  }

                  if (state is MyEventsLoadMoreSuccess) {
                    _append(state.data);
                  }
                },
                builder: (context, state) {
                  if (state is MyEventsLoadSuccess) {
                    return EventList(data: _paginated.data);
                  }

                  return ListLoading();
                },
              ),
            ),
          ],
        ),
      ),
    );
  }

  @override
  bool get wantKeepAlive => true;
}

Upvotes: 2

Views: 1664

Answers (1)

Yura
Yura

Reputation: 2248

Finally found the answer by my own after doing some research. Not a perfect solution but it works.

bool _onScrollNotification(UserScrollNotification notification) {
  /// Make sure it listening to the nearest depth of scrollable views
  /// and ignore notifications if scroll axis is not vertical.
  if (notification.depth == 0 && notification.metrics.axis == Axis.vertical) {
    ScrollDirection direction = notification.direction;
    if (direction == ScrollDirection.reverse && !_isFetchingMoreData) {
      /// Check if the user is scrolling the list downward to prevent
      /// function call on upward. Also check if there is any fetch
      /// queues, if it still fetching, skip this step and do nothing.
      /// It was necessary to prevent the notification to bubble up
      /// the widget with `_loadMoreData()` call.
      if (_paginated.currentPage < _paginated.lastPage)
        /// If the conditions above are passed, we are safe to load more.
        return _loadMoreData();
    }
  }
  return true;
}

Upvotes: 1

Related Questions