2

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;
}

1 Answer 1

1

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;
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.