diff --git a/CHANGELOG.md b/CHANGELOG.md index 43e825d..0011d50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 1.2.0 + - Improve feed scrolling and reload + ## 1.0.2 - Add logout to drawer - Fix permissions diff --git a/lib/pages/articles/about.dart b/lib/pages/articles/about.dart index 14be47b..f18aebb 100644 --- a/lib/pages/articles/about.dart +++ b/lib/pages/articles/about.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:realworld_flutter/localizations/rw_localizations.dart'; +import 'package:realworld_flutter/version.dart'; import 'package:realworld_flutter/widgets/external_link.dart'; void about(BuildContext context) { @@ -20,7 +21,7 @@ void about(BuildContext context) { ), * */ - applicationVersion: 'Version: 1.0.0', + applicationVersion: 'Version: $version', applicationLegalese: '© ${DateTime.now().year} ${locale.appName}', children: [ Padding( diff --git a/lib/pages/articles/feed.dart b/lib/pages/articles/feed.dart index c2875b0..313a0e0 100644 --- a/lib/pages/articles/feed.dart +++ b/lib/pages/articles/feed.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:realworld_flutter/blocs/articles/bloc.dart'; import 'package:realworld_flutter/pages/articles/preview_post.dart'; @@ -12,12 +13,16 @@ class Feed extends StatefulWidget { final Function(ArticlesBloc bloc) onLoad; final Function(ArticlesBloc bloc) onLoadMore; final double scrollThreshold; + + /// The ScrollController itself is handled by the NestedScrollView parent. + final ScrollController scrollController; Feed({ @required this.id, @required this.label, @required this.onRefresh, @required this.onLoad, @required this.onLoadMore, + @required this.scrollController, this.scrollThreshold = 400.0, }); @override @@ -25,14 +30,13 @@ class Feed extends StatefulWidget { } class _FeedState extends State { - final _scrollController = ScrollController(); double _scrollMarker = 0; ArticlesBloc _articlesBloc; @override void initState() { super.initState(); - _scrollController.addListener(_onScroll); + widget.scrollController.addListener(_onScroll); _articlesBloc = BlocProvider.of(context); if (_articlesBloc.currentState is! ArticlesLoaded) { @@ -40,6 +44,12 @@ class _FeedState extends State { } } + @override + void dispose() { + widget.scrollController.removeListener(_onScroll); + super.dispose(); + } + @override Widget build(BuildContext context) { return BlocBuilder( @@ -60,7 +70,6 @@ class _FeedState extends State { return RefreshIndicator( onRefresh: _onRefresh, child: ListView.builder( - controller: _scrollController, itemBuilder: (BuildContext context, int index) { final article = articles[index]; @@ -94,18 +103,22 @@ class _FeedState extends State { ); } - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); + bool get _hasReachedMax { + return (_articlesBloc.currentState as ArticlesLoaded).hasReachedMax; + } + + bool get _isAtBottom { + final scrollPosition = widget.scrollController.position; + + return scrollPosition.atEdge && + scrollPosition.userScrollDirection == ScrollDirection.forward; } void _onScroll() { final maxScroll = _scrollMarker + widget.scrollThreshold; - final currentScroll = _scrollController.position.pixels; + final currentScroll = widget.scrollController.position.pixels; if (_articlesBloc.currentState is ArticlesLoaded) { - if (!(_articlesBloc.currentState as ArticlesLoaded).hasReachedMax && - currentScroll >= maxScroll) { + if (!_hasReachedMax && (currentScroll >= maxScroll || _isAtBottom)) { _scrollMarker = maxScroll; widget.onLoadMore(_articlesBloc); } diff --git a/lib/pages/articles/feeds.dart b/lib/pages/articles/feeds.dart index 2714a08..cf7caba 100644 --- a/lib/pages/articles/feeds.dart +++ b/lib/pages/articles/feeds.dart @@ -34,23 +34,28 @@ class _FeedsState extends State with SingleTickerProviderStateMixin { final Map _blocs = {}; + ScrollController _scrollController; @override void initState() { + _scrollController = ScrollController(); + final initialIndex = widget.initialFeed != null ? widget.feeds .indexWhere((FeedModel feed) => feed.id == widget.initialFeed) : 0; - super.initState(); _tabController = TabController( length: widget.feeds.length, initialIndex: initialIndex == -1 ? 0 : initialIndex, vsync: this, ); + + super.initState(); } @override void dispose() { + _scrollController.dispose(); _tabController.dispose(); for (var cachedBloc in _blocs.values) { cachedBloc.dispose(); @@ -62,67 +67,79 @@ class _FeedsState extends State with SingleTickerProviderStateMixin { @override Widget build(BuildContext context) { return NestedScrollView( - headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { - return [ - SliverPersistentHeader( - pinned: widget.pinned, - delegate: HeroHeader( - minExtent: widget.minExtentHeader, - maxExtent: widget.maxExtentHeader, - child: widget.header, - ), + controller: _scrollController, + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverPersistentHeader( + pinned: widget.pinned, + delegate: HeroHeader( + minExtent: widget.minExtentHeader, + maxExtent: widget.maxExtentHeader, + child: widget.header, ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Container( - child: TabBar( - controller: _tabController, - tabs: widget.feeds - .map((feed) => Tab(text: feed.label)) - .toList(), - ), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Container( + child: TabBar( + controller: _tabController, + tabs: widget.feeds + .map((feed) => Tab(text: feed.label)) + .toList(), ), ), ), - ]; - }, - body: SizedBox( - height: 300, - child: AnimatedBuilder( - animation: _tabController.animation, - builder: (BuildContext context, snapshot) { - return TabBarView( - controller: _tabController, - children: mapWithIndex( - widget.feeds, - (FeedModel feed, int index) { - if (feed.child != null) { - return feed.child; - } else { - return BlocProvider.value( - value: getArticlesBloc(feed), - child: Opacity( - opacity: index % 2 == 0 - ? 1 - _tabController.animation.value - : _tabController.animation.value, - child: Feed( - id: feed.id, - scrollThreshold: feed.scrollThreshold, - label: feed.label, - onRefresh: feed.onRefresh, - onLoadMore: feed.onLoadMore, - onLoad: feed.onLoad, - ), - ), - ); - } - }, - ), - ); - }, ), - )); + ]; + }, + body: Builder( + builder: (BuildContext context) { + final innerScrollController = + context.ancestorWidgetOfExactType(PrimaryScrollController) + as PrimaryScrollController; + + return SizedBox( + height: 300, + child: AnimatedBuilder( + animation: _tabController.animation, + builder: (BuildContext context, snapshot) { + return TabBarView( + controller: _tabController, + children: mapWithIndex( + widget.feeds, + (FeedModel feed, int index) { + if (feed.child != null) { + return feed.child; + } else { + return BlocProvider.value( + value: getArticlesBloc(feed), + child: Opacity( + opacity: index % 2 == 0 + ? 1 - _tabController.animation.value + : _tabController.animation.value, + child: Feed( + id: feed.id, + scrollThreshold: feed.scrollThreshold, + label: feed.label, + onRefresh: feed.onRefresh, + onLoadMore: feed.onLoadMore, + onLoad: feed.onLoad, + scrollController: + innerScrollController.controller, + ), + ), + ); + } + }, + ), + ); + }, + ), + ); + }, + ), + ); } ArticlesBloc getArticlesBloc(FeedModel feed) { diff --git a/lib/version.dart b/lib/version.dart new file mode 100644 index 0000000..d1f7048 --- /dev/null +++ b/lib/version.dart @@ -0,0 +1 @@ +final version = '1.1.0'; diff --git a/pubspec.yaml b/pubspec.yaml index ce759b4..fae58f4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: realworld_flutter description: A Realworld Flutter Application -version: 1.0.2 +version: 1.1.0 environment: sdk: ">=2.2.2 <3.0.0"