Как реализовать listview lazyload внутри вложенного ScrollView?

#flutter

#flutter

Вопрос:

У меня есть приложение, у него есть страница, которая действует как точка входа и показывает TabView , содержащую 3 или более страниц на нем. Он использует NestedScrollView и SliverAppBar для придания некоторой анимации, когда пользователь прокручивает представление.

Я хочу реализовать отложенную загрузку разбитого на страницы списка, но поскольку это не позволяет мне использовать контроллер внутри, CustomScrollView как указано в документах в этой строке:

 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>[
  

Я не могу использовать ScrollController на дочерней странице, чтобы получить значение прокрутки для запуска loadMore функции. К счастью, есть аналогичный виджет для прослушивания вызываемого события прокрутки ScrollNotification . Но я не знаю, какое свойство содержит значение максимального предела прокрутки.

Попытался сравнить доступные свойства с помощью этого:

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

Но похоже, что они не содержат никакого фиксированного значения, как мне нужно. Он всегда менял свое значение независимо.

на родительской странице ScrollController я также не могу использовать ( tabview_holder ), поскольку каждая страница на каждой вкладке имеет независимый bloc , events , data алгоритм выборки. Имея это в виду, как я могу выполнить это требование?

Пожалуйста, взгляните на мой скрипт:

tabview_holder.dart (не настоящее имя файла, просто для иллюстрации)

 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:

Наконец-то нашел ответ самостоятельно после проведения некоторых исследований. Не идеальное решение, но оно работает.

 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 amp;amp; notification.metrics.axis == Axis.vertical) {
    ScrollDirection direction = notification.direction;
    if (direction == ScrollDirection.reverse amp;amp; !_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;
}