From bf2818ef5a7b09018d361aea629e796848c4141f Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Wed, 29 Jun 2022 01:06:33 -0400 Subject: [PATCH] Use bottom bar to scroll to top on home (#90) --- lib/screens/home/home.dart | 10 ++- lib/screens/home/stores/categories_store.dart | 29 ++----- .../home/stores/categories_store.g.dart | 27 ++++-- lib/screens/home/stores/home_store.dart | 53 +++++++++-- lib/screens/home/stores/list_store.dart | 26 +++--- lib/screens/home/top/categories.dart | 57 +++++------- lib/screens/home/top/category_streams.dart | 50 +++++++---- lib/screens/home/top/top_section.dart | 87 ++++++++++++------- lib/screens/home/widgets/category_card.dart | 11 +-- lib/screens/home/widgets/stream_card.dart | 13 +-- lib/screens/home/widgets/streams_list.dart | 60 +++++-------- 11 files changed, 231 insertions(+), 192 deletions(-) diff --git a/lib/screens/home/home.dart b/lib/screens/home/home.dart index c181e689..fdb9d79a 100644 --- a/lib/screens/home/home.dart +++ b/lib/screens/home/home.dart @@ -114,8 +114,14 @@ class _HomeState extends State { builder: (_) => IndexedStack( index: _homeStore.selectedIndex, children: [ - if (_authStore.isLoggedIn) const StreamsList(listType: ListType.followed), - const TopSection(), + if (_authStore.isLoggedIn) + StreamsList( + listType: ListType.followed, + scrollController: _homeStore.followedScrollController, + ), + TopSection( + homeStore: _homeStore, + ), const Search(), ], ), diff --git a/lib/screens/home/stores/categories_store.dart b/lib/screens/home/stores/categories_store.dart index f5c3d50d..820c1c57 100644 --- a/lib/screens/home/stores/categories_store.dart +++ b/lib/screens/home/stores/categories_store.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:flutter/material.dart'; import 'package:frosty/api/twitch_api.dart'; import 'package:frosty/core/auth/auth_store.dart'; import 'package:frosty/models/category.dart'; @@ -21,18 +20,8 @@ abstract class CategoriesStoreBase with Store { String? _categoriesCursor; /// The loading status for pagination. + @readonly bool _isLoading = false; - bool get isLoading => _isLoading; - - /// Returns whether or not there are more streams and loading status for pagination. - bool get hasMore => _isLoading == false && _categoriesCursor != null; - - /// The scroll controller used for handling scroll to top. - final scrollController = ScrollController(); - - /// Whether or not the scroll to top button is visible. - @observable - var showJumpButton = false; /// The current visible categories, sorted by total viewers. @readonly @@ -42,15 +31,11 @@ abstract class CategoriesStoreBase with Store { @readonly String? _error; - CategoriesStoreBase({required this.authStore, required this.twitchApi}) { - scrollController.addListener(() { - if (scrollController.position.atEdge || scrollController.position.outOfRange) { - showJumpButton = false; - } else { - showJumpButton = true; - } - }); + /// Returns whether or not there are more streams and loading status for pagination. + @computed + bool get hasMore => _isLoading == false && _categoriesCursor != null; + CategoriesStoreBase({required this.authStore, required this.twitchApi}) { getCategories(); } @@ -88,8 +73,4 @@ abstract class CategoriesStoreBase with Store { return getCategories(); } - - void dispose() { - scrollController.dispose(); - } } diff --git a/lib/screens/home/stores/categories_store.g.dart b/lib/screens/home/stores/categories_store.g.dart index ef0c9bd6..30e0a904 100644 --- a/lib/screens/home/stores/categories_store.g.dart +++ b/lib/screens/home/stores/categories_store.g.dart @@ -9,19 +9,28 @@ part of 'categories_store.dart'; // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers mixin _$CategoriesStore on CategoriesStoreBase, Store { - late final _$showJumpButtonAtom = - Atom(name: 'CategoriesStoreBase.showJumpButton', context: context); + Computed? _$hasMoreComputed; @override - bool get showJumpButton { - _$showJumpButtonAtom.reportRead(); - return super.showJumpButton; + bool get hasMore => (_$hasMoreComputed ??= Computed(() => super.hasMore, + name: 'CategoriesStoreBase.hasMore')) + .value; + + late final _$_isLoadingAtom = + Atom(name: 'CategoriesStoreBase._isLoading', context: context); + + bool get isLoading { + _$_isLoadingAtom.reportRead(); + return super._isLoading; } @override - set showJumpButton(bool value) { - _$showJumpButtonAtom.reportWrite(value, super.showJumpButton, () { - super.showJumpButton = value; + bool get _isLoading => isLoading; + + @override + set _isLoading(bool value) { + _$_isLoadingAtom.reportWrite(value, super._isLoading, () { + super._isLoading = value; }); } @@ -86,7 +95,7 @@ mixin _$CategoriesStore on CategoriesStoreBase, Store { @override String toString() { return ''' -showJumpButton: ${showJumpButton} +hasMore: ${hasMore} '''; } } diff --git a/lib/screens/home/stores/home_store.dart b/lib/screens/home/stores/home_store.dart index d3054a6b..4d7f77f5 100644 --- a/lib/screens/home/stores/home_store.dart +++ b/lib/screens/home/stores/home_store.dart @@ -1,31 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:frosty/core/auth/auth_store.dart'; import 'package:mobx/mobx.dart'; part 'home_store.g.dart'; +/// The home store that handles scrolling and navigating between tabs on the home screen. class HomeStore = HomeStoreBase with _$HomeStore; abstract class HomeStoreBase with Store { final AuthStore authStore; - late final ReactionDisposer _disposeReaction; + /// The scroll controller for controlling the scroll to top on the followed section. + final followedScrollController = ScrollController(); + /// The scroll controllers for controlling the scroll to top on the top section. + /// One controller is for the top streams tab and the other is for the top categories tab. + final topSectionScrollControllers = [ + ScrollController(), + ScrollController(), + ]; + + /// The current index of the top section tab. Changes when switching between the streams and categories tabs. + var topSectionCurrentIndex = 0; + + /// The current selected index/tab of the bottom navigation bar. + @readonly + var _selectedIndex = 0; + + late final ReactionDisposer _disposeReaction; HomeStoreBase({required this.authStore}) { + // Setup a reaction that will return to the first tab when logging in/out. + // This will prevent an out of range index error when last visiting the search screen and logging out. _disposeReaction = reaction( (_) => authStore.isLoggedIn, (_) => _selectedIndex = 0, ); } - @readonly - var _selectedIndex = 0; - @action void handleTap(int index) { + // If tapping a different tab, set the new index to show. if (index != _selectedIndex) { _selectedIndex = index; + } else { + if (index == 0) { + // If on the followed tab and tapping the followed button, scroll to the top. + followedScrollController.animateTo( + 0.0, + duration: const Duration(milliseconds: 200), + curve: Curves.easeOutCubic, + ); + } else { + // If on the top section, scroll to the top of the tab based on the current top tab. + topSectionScrollControllers[topSectionCurrentIndex].animateTo( + 0.0, + duration: const Duration(milliseconds: 200), + curve: Curves.easeOutCubic, + ); + } } } - void dispose() => _disposeReaction(); + void dispose() { + _disposeReaction(); + + followedScrollController.dispose(); + + for (final controller in topSectionScrollControllers) { + controller.dispose(); + } + } } diff --git a/lib/screens/home/stores/list_store.dart b/lib/screens/home/stores/list_store.dart index 82301263..451892fe 100644 --- a/lib/screens/home/stores/list_store.dart +++ b/lib/screens/home/stores/list_store.dart @@ -20,7 +20,7 @@ abstract class ListStoreBase with Store { /// The type of list that this store is handling. final ListType listType; - /// The category id to use if the [listType] is [ListType.category]. + /// The category id to use when fetching streams if the [listType] is [ListType.category]. final String? categoryId; /// The pagination cursor for the streams. @@ -33,8 +33,9 @@ abstract class ListStoreBase with Store { bool _isLoading = false; bool get isLoading => _isLoading; - /// The scroll controller used for handling scroll to top. - final scrollController = ScrollController(); + /// The scroll controller used for handling scroll to top (if provided). + /// If provided, will use the [ScrollToTop] widget to scroll to the top of the list instead of the bottom tab bar. + final ScrollController? scrollController; /// Whether or not the scroll to top button is visible. @observable @@ -60,14 +61,17 @@ abstract class ListStoreBase with Store { required this.twitchApi, required this.listType, this.categoryId, + this.scrollController, }) { - scrollController.addListener(() { - if (scrollController.position.atEdge || scrollController.position.outOfRange) { - showJumpButton = false; - } else { - showJumpButton = true; - } - }); + if (scrollController != null) { + scrollController!.addListener(() { + if (scrollController!.position.atEdge || scrollController!.position.outOfRange) { + showJumpButton = false; + } else { + showJumpButton = true; + } + }); + } switch (listType) { case ListType.followed: @@ -135,7 +139,7 @@ abstract class ListStoreBase with Store { return getStreams(); } - void dispose() => scrollController.dispose(); + void dispose() => scrollController?.dispose(); } /// The possible types of lists that can be displayed. diff --git a/lib/screens/home/top/categories.dart b/lib/screens/home/top/categories.dart index 74676c79..c8a707b3 100644 --- a/lib/screens/home/top/categories.dart +++ b/lib/screens/home/top/categories.dart @@ -7,11 +7,15 @@ import 'package:frosty/screens/home/stores/categories_store.dart'; import 'package:frosty/screens/home/widgets/category_card.dart'; import 'package:frosty/widgets/alert_message.dart'; import 'package:frosty/widgets/loading_indicator.dart'; -import 'package:frosty/widgets/scroll_to_top_button.dart'; import 'package:provider/provider.dart'; class Categories extends StatefulWidget { - const Categories({Key? key}) : super(key: key); + final ScrollController scrollController; + + const Categories({ + Key? key, + required this.scrollController, + }) : super(key: key); @override State createState() => _CategoriesState(); @@ -50,34 +54,21 @@ class _CategoriesState extends State with AutomaticKeepAliveClientMi if (_categoriesStore.categories.isEmpty && _categoriesStore.isLoading && _categoriesStore.error == null) { return const LoadingIndicator(subtitle: 'Loading categories...'); } - return Stack( - alignment: AlignmentDirectional.bottomCenter, - children: [ - GridView.builder( - physics: const AlwaysScrollableScrollPhysics(), - controller: _categoriesStore.scrollController, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - ), - itemCount: _categoriesStore.categories.length, - itemBuilder: (context, index) { - if (index > _categoriesStore.categories.length - 8 && _categoriesStore.hasMore) { - _categoriesStore.getCategories(); - } - return CategoryCard( - category: _categoriesStore.categories[index], - ); - }, - ), - Observer( - builder: (context) => AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - switchInCurve: Curves.easeOutCubic, - switchOutCurve: Curves.easeInCubic, - child: _categoriesStore.showJumpButton ? ScrollToTopButton(scrollController: _categoriesStore.scrollController) : null, - ), - ), - ], + return GridView.builder( + physics: const AlwaysScrollableScrollPhysics(), + controller: widget.scrollController, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + ), + itemCount: _categoriesStore.categories.length, + itemBuilder: (context, index) { + if (index > _categoriesStore.categories.length - 8 && _categoriesStore.hasMore) { + _categoriesStore.getCategories(); + } + return CategoryCard( + category: _categoriesStore.categories[index], + ); + }, ); }, ), @@ -86,10 +77,4 @@ class _CategoriesState extends State with AutomaticKeepAliveClientMi @override bool get wantKeepAlive => true; - - @override - void dispose() { - _categoriesStore.dispose(); - super.dispose(); - } } diff --git a/lib/screens/home/top/category_streams.dart b/lib/screens/home/top/category_streams.dart index 6b739572..4e51e9f1 100644 --- a/lib/screens/home/top/category_streams.dart +++ b/lib/screens/home/top/category_streams.dart @@ -13,16 +13,19 @@ import 'package:frosty/widgets/loading_indicator.dart'; import 'package:frosty/widgets/scroll_to_top_button.dart'; import 'package:provider/provider.dart'; -/// A widget that displays a list of streams depending on the provided [listType]. -/// This differs from the normal [StreamsList] in that it uses Slivers to display the box art on top. +/// A widget that displays a list of streams under the provided [categoryId]. +/// This differs from the normal [StreamsList] in that it uses slivers to display the box art on top. class CategoryStreams extends StatefulWidget { - final ListStore listStore; + /// The category name, used for the header on the box art sliver. final String categoryName; + /// The category id, used for fetching the relevant streams in the [ListStore]. + final String categoryId; + const CategoryStreams({ Key? key, - required this.listStore, required this.categoryName, + required this.categoryId, }) : super(key: key); @override @@ -30,9 +33,17 @@ class CategoryStreams extends StatefulWidget { } class _CategoryStreamsState extends State { + late final _listStore = ListStore( + authStore: context.read(), + twitchApi: context.read(), + listType: ListType.category, + categoryId: widget.categoryId, + scrollController: ScrollController(), + ); + @override Widget build(BuildContext context) { - // Calculate the dimmensions of the box art based on the screen width. + // Calculate the dimensions of the box art based on the screen width. final size = MediaQuery.of(context).size; final pixelRatio = MediaQuery.of(context).devicePixelRatio; final artWidth = (size.width * pixelRatio).toInt(); @@ -42,12 +53,12 @@ class _CategoryStreamsState extends State { body: RefreshIndicator( onRefresh: () async { HapticFeedback.lightImpact(); - await widget.listStore.refreshStreams(); + await _listStore.refreshStreams(); - if (widget.listStore.error != null) { + if (_listStore.error != null) { final snackBar = SnackBar( content: AlertMessage( - message: widget.listStore.error!, + message: _listStore.error!, icon: Icons.error, ), behavior: SnackBarBehavior.floating, @@ -64,7 +75,7 @@ class _CategoryStreamsState extends State { children: [ CustomScrollView( physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), - controller: widget.listStore.scrollController, + controller: _listStore.scrollController, slivers: [ SliverAppBar( stretch: true, @@ -85,7 +96,7 @@ class _CategoryStreamsState extends State { background: FutureBuilder( future: context.read().getCategory( headers: context.read().headersTwitch, - gameId: widget.listStore.categoryId!, + gameId: _listStore.categoryId!, ), builder: (context, AsyncSnapshot snapshot) { return snapshot.hasData @@ -105,7 +116,7 @@ class _CategoryStreamsState extends State { ), ), ), - if (widget.listStore.streams.isEmpty && widget.listStore.isLoading && widget.listStore.error == null) + if (_listStore.streams.isEmpty && _listStore.isLoading && _listStore.error == null) const SliverFillRemaining( child: LoadingIndicator( subtitle: 'Loading streams...', @@ -117,13 +128,12 @@ class _CategoryStreamsState extends State { sliver: SliverList( delegate: SliverChildBuilderDelegate( (context, index) { - if (index > widget.listStore.streams.length - 8 && widget.listStore.hasMore) { - widget.listStore.getStreams(); + if (index > _listStore.streams.length - 8 && _listStore.hasMore) { + _listStore.getStreams(); } return Observer( builder: (context) => StreamCard( - listStore: widget.listStore, - streamInfo: widget.listStore.streams[index], + streamInfo: _listStore.streams[index], showUptime: context.read().showThumbnailUptime, showThumbnail: context.read().showThumbnails, large: context.read().largeStreamCard, @@ -131,7 +141,7 @@ class _CategoryStreamsState extends State { ), ); }, - childCount: widget.listStore.streams.length, + childCount: _listStore.streams.length, ), ), ), @@ -143,7 +153,7 @@ class _CategoryStreamsState extends State { duration: const Duration(milliseconds: 200), switchInCurve: Curves.easeOutCubic, switchOutCurve: Curves.easeInCubic, - child: widget.listStore.showJumpButton ? ScrollToTopButton(scrollController: widget.listStore.scrollController) : null, + child: _listStore.showJumpButton ? ScrollToTopButton(scrollController: _listStore.scrollController!) : null, ), ), ), @@ -154,4 +164,10 @@ class _CategoryStreamsState extends State { ), ); } + + @override + void dispose() { + _listStore.dispose(); + super.dispose(); + } } diff --git a/lib/screens/home/top/top_section.dart b/lib/screens/home/top/top_section.dart index 058dbd4a..08b2f0e8 100644 --- a/lib/screens/home/top/top_section.dart +++ b/lib/screens/home/top/top_section.dart @@ -1,44 +1,73 @@ import 'package:flutter/material.dart'; +import 'package:frosty/screens/home/stores/home_store.dart'; import 'package:frosty/screens/home/stores/list_store.dart'; import 'package:frosty/screens/home/top/categories.dart'; import 'package:frosty/screens/home/widgets/streams_list.dart'; -class TopSection extends StatelessWidget { - const TopSection({Key? key}) : super(key: key); +class TopSection extends StatefulWidget { + final HomeStore homeStore; + + const TopSection({ + Key? key, + required this.homeStore, + }) : super(key: key); + + @override + State createState() => _TopSectionState(); +} + +class _TopSectionState extends State with SingleTickerProviderStateMixin { + late final _tabBarController = TabController(length: 2, vsync: this); + + @override + void initState() { + super.initState(); + _tabBarController.addListener(() => widget.homeStore.topSectionCurrentIndex = _tabBarController.index); + } @override Widget build(BuildContext context) { - return DefaultTabController( - length: 2, - child: Column( - children: const [ - TabBar( - indicatorColor: Color(0xff9146ff), - tabs: [ - Tab( - child: Text( - 'Streams', - style: TextStyle(fontWeight: FontWeight.w600), - ), + return Column( + children: [ + TabBar( + controller: _tabBarController, + indicatorColor: const Color(0xff9146ff), + tabs: const [ + Tab( + child: Text( + 'Streams', + style: TextStyle(fontWeight: FontWeight.w600), ), - Tab( - child: Text( - 'Categories', - style: TextStyle(fontWeight: FontWeight.w600), - ), + ), + Tab( + child: Text( + 'Categories', + style: TextStyle(fontWeight: FontWeight.w600), + ), + ), + ], + ), + Expanded( + child: TabBarView( + controller: _tabBarController, + children: [ + StreamsList( + listType: ListType.top, + scrollController: widget.homeStore.topSectionScrollControllers[0], + ), + Categories( + scrollController: widget.homeStore.topSectionScrollControllers[1], ), ], ), - Expanded( - child: TabBarView( - children: [ - StreamsList(listType: ListType.top), - Categories(), - ], - ), - ), - ], - ), + ), + ], ); } + + @override + void dispose() { + _tabBarController.dispose(); + super.dispose(); + } } diff --git a/lib/screens/home/widgets/category_card.dart b/lib/screens/home/widgets/category_card.dart index 02c60e8e..c5bce054 100644 --- a/lib/screens/home/widgets/category_card.dart +++ b/lib/screens/home/widgets/category_card.dart @@ -1,13 +1,9 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:frosty/api/twitch_api.dart'; -import 'package:frosty/core/auth/auth_store.dart'; import 'package:frosty/models/category.dart'; -import 'package:frosty/screens/home/stores/list_store.dart'; import 'package:frosty/screens/home/top/category_streams.dart'; import 'package:frosty/widgets/animate_scale.dart'; import 'package:frosty/widgets/loading_indicator.dart'; -import 'package:provider/provider.dart'; /// A tappable card widget that displays a category's box art and name under. class CategoryCard extends StatelessWidget { @@ -32,12 +28,7 @@ class CategoryCard extends StatelessWidget { MaterialPageRoute( builder: (context) => CategoryStreams( categoryName: category.name, - listStore: ListStore( - twitchApi: context.read(), - authStore: context.read(), - listType: ListType.category, - categoryId: category.id, - ), + categoryId: category.id, ), ), ), diff --git a/lib/screens/home/widgets/stream_card.dart b/lib/screens/home/widgets/stream_card.dart index 8ed1ac27..9111fc99 100644 --- a/lib/screens/home/widgets/stream_card.dart +++ b/lib/screens/home/widgets/stream_card.dart @@ -3,12 +3,10 @@ import 'dart:math'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:frosty/api/twitch_api.dart'; import 'package:frosty/constants/constants.dart'; import 'package:frosty/core/auth/auth_store.dart'; import 'package:frosty/models/stream.dart'; import 'package:frosty/screens/channel/video_chat.dart'; -import 'package:frosty/screens/home/stores/list_store.dart'; import 'package:frosty/screens/home/top/category_streams.dart'; import 'package:frosty/widgets/animate_scale.dart'; import 'package:frosty/widgets/block_report_modal.dart'; @@ -19,7 +17,6 @@ import 'package:provider/provider.dart'; /// A tappable card widget that displays a stream's thumbnail and details. class StreamCard extends StatelessWidget { - final ListStore listStore; final StreamTwitch streamInfo; final bool showUptime; final bool showThumbnail; @@ -28,7 +25,6 @@ class StreamCard extends StatelessWidget { const StreamCard({ Key? key, - required this.listStore, required this.streamInfo, required this.showUptime, required this.showThumbnail, @@ -177,12 +173,7 @@ class StreamCard extends StatelessWidget { MaterialPageRoute( builder: (context) => CategoryStreams( categoryName: streamInfo.gameName, - listStore: ListStore( - twitchApi: context.read(), - authStore: context.read(), - listType: ListType.category, - categoryId: streamInfo.gameId, - ), + categoryId: streamInfo.gameId, ), ), ), @@ -218,7 +209,7 @@ class StreamCard extends StatelessWidget { backgroundColor: Colors.transparent, context: context, builder: (context) => BlockReportModal( - authStore: listStore.authStore, + authStore: context.read(), name: streamerName, userLogin: streamInfo.userLogin, userId: streamInfo.userId, diff --git a/lib/screens/home/widgets/streams_list.dart b/lib/screens/home/widgets/streams_list.dart index f0c1786b..5c4b508c 100644 --- a/lib/screens/home/widgets/streams_list.dart +++ b/lib/screens/home/widgets/streams_list.dart @@ -8,22 +8,21 @@ import 'package:frosty/screens/home/widgets/stream_card.dart'; import 'package:frosty/screens/settings/stores/settings_store.dart'; import 'package:frosty/widgets/alert_message.dart'; import 'package:frosty/widgets/loading_indicator.dart'; -import 'package:frosty/widgets/scroll_to_top_button.dart'; import 'package:provider/provider.dart'; -/// A widget that displays a list of streams depending on the provided [listType]. -/// If the [listType] is [ListType.category], [categoryId] must be provided. +/// A widget that displays a list of followed or top streams based on the provided [listType]. +/// For a widget that displays the top streams under a category, refer to [CategoryStreams]. class StreamsList extends StatefulWidget { /// The type of list to display. final ListType listType; - /// The category to display streams from if the [listType] is [ListType.category]. - final String? categoryId; + /// The scroll controller to use for scroll to top functionality. + final ScrollController scrollController; const StreamsList({ Key? key, required this.listType, - this.categoryId, + required this.scrollController, }) : super(key: key); @override @@ -35,7 +34,6 @@ class _StreamsListState extends State with AutomaticKeepAliveClient authStore: context.read(), twitchApi: context.read(), listType: widget.listType, - categoryId: widget.categoryId, ); @override @@ -75,39 +73,25 @@ class _StreamsListState extends State with AutomaticKeepAliveClient ); } - return Stack( - alignment: AlignmentDirectional.bottomCenter, - children: [ - ListView.builder( - physics: const AlwaysScrollableScrollPhysics(), - controller: _listStore.scrollController, - itemCount: _listStore.streams.length, - itemBuilder: (context, index) { - if (index > _listStore.streams.length - 8 && _listStore.hasMore) { - debugPrint('$index ${_listStore.streams.length}'); + return ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + controller: widget.scrollController, + itemCount: _listStore.streams.length, + itemBuilder: (context, index) { + if (index > _listStore.streams.length - 8 && _listStore.hasMore) { + debugPrint('$index ${_listStore.streams.length}'); - _listStore.getStreams(); - } - return Observer( - builder: (context) => StreamCard( - listStore: _listStore, - streamInfo: _listStore.streams[index], - showThumbnail: context.read().showThumbnails, - large: context.read().largeStreamCard, - showUptime: context.read().showThumbnailUptime, - ), - ); - }, - ), - Observer( - builder: (context) => AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - switchInCurve: Curves.easeOutCubic, - switchOutCurve: Curves.easeInCubic, - child: _listStore.showJumpButton ? ScrollToTopButton(scrollController: _listStore.scrollController) : null, + _listStore.getStreams(); + } + return Observer( + builder: (context) => StreamCard( + streamInfo: _listStore.streams[index], + showThumbnail: context.read().showThumbnails, + large: context.read().largeStreamCard, + showUptime: context.read().showThumbnailUptime, ), - ), - ], + ); + }, ); }, ),