From 818097f9626d55c4fa2791af1c0d24cfa1401a2a Mon Sep 17 00:00:00 2001 From: shilangyu Date: Thu, 17 Sep 2020 19:43:26 +0200 Subject: [PATCH 1/8] created infinite scroll component --- lib/widgets/infinite_scroll.dart | 54 ++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 lib/widgets/infinite_scroll.dart diff --git a/lib/widgets/infinite_scroll.dart b/lib/widgets/infinite_scroll.dart new file mode 100644 index 0000000..3fca3f8 --- /dev/null +++ b/lib/widgets/infinite_scroll.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +class InfiniteScroll extends HookWidget { + final int batchSize; + final Widget loadingWidget; + final Widget Function(T data) builder; + final Future> Function(int page, int batchSize) fetchMore; + + InfiniteScroll({ + this.batchSize = 10, + this.loadingWidget = + const ListTile(title: Center(child: CircularProgressIndicator())), + this.builder, + this.fetchMore, + }) : assert(builder != null), + assert(fetchMore != null); + + @override + Widget build(BuildContext context) { + final page = useState(1); + final hasMore = useState(true); + final data = useState>([]); + + return ListView.builder( + // +1 for the loading widget + itemCount: data.value.length + 1, + itemBuilder: (_, i) { + // reached the bottom, fetch more + if (i == data.value.length) { + // if there are no more, skip + if (!hasMore.value) { + return Container(); + } + + fetchMore(page.value, batchSize).then((value) { + // if got less than the batchSize, mark the list as done + if (value.length < batchSize) { + hasMore.value = false; + } + // append new data and increment page count + data.value.addAll(value); + page.value++; + }); + + return loadingWidget; + } + + // not last element, render list item + return builder(data.value[i]); + }, + ); + } +} From 60dcf0700183c767d08f25b0569e6e3bfdc3e931 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Thu, 17 Sep 2020 19:49:42 +0200 Subject: [PATCH 2/8] convert hasMore into a ref + prevent double fetching --- lib/widgets/infinite_scroll.dart | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/widgets/infinite_scroll.dart b/lib/widgets/infinite_scroll.dart index 3fca3f8..debb16f 100644 --- a/lib/widgets/infinite_scroll.dart +++ b/lib/widgets/infinite_scroll.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import '../hooks/ref.dart'; + class InfiniteScroll extends HookWidget { final int batchSize; final Widget loadingWidget; @@ -19,8 +21,9 @@ class InfiniteScroll extends HookWidget { @override Widget build(BuildContext context) { final page = useState(1); - final hasMore = useState(true); final data = useState>([]); + final hasMore = useRef(true); + final isFetching = useRef(false); return ListView.builder( // +1 for the loading widget @@ -29,19 +32,23 @@ class InfiniteScroll extends HookWidget { // reached the bottom, fetch more if (i == data.value.length) { // if there are no more, skip - if (!hasMore.value) { + if (!hasMore.current) { return Container(); } - fetchMore(page.value, batchSize).then((value) { - // if got less than the batchSize, mark the list as done - if (value.length < batchSize) { - hasMore.value = false; - } - // append new data and increment page count - data.value.addAll(value); - page.value++; - }); + // if it's already fetching more, skip + if (!isFetching.current) { + isFetching.current = true; + fetchMore(page.value, batchSize).then((value) { + // if got less than the batchSize, mark the list as done + if (value.length < batchSize) { + hasMore.current = false; + } + // append new data and increment page count + data.value.addAll(value); + page.value++; + }).whenComplete(() => isFetching.current = false); + } return loadingWidget; } From 0fb53e331478d133e3e15ce5fdc37eabd7d9d155 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Fri, 18 Sep 2020 23:24:58 +0200 Subject: [PATCH 3/8] added a controller and more robust behavior --- lib/hooks/infinite_scroll.dart | 6 +++++ lib/widgets/infinite_scroll.dart | 39 +++++++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 lib/hooks/infinite_scroll.dart diff --git a/lib/hooks/infinite_scroll.dart b/lib/hooks/infinite_scroll.dart new file mode 100644 index 0000000..4e03cb8 --- /dev/null +++ b/lib/hooks/infinite_scroll.dart @@ -0,0 +1,6 @@ +import 'package:flutter_hooks/flutter_hooks.dart'; + +import '../widgets/infinite_scroll.dart'; + +InfiniteScrollController useInfiniteScrollController() => + useMemoized(() => InfiniteScrollController()); diff --git a/lib/widgets/infinite_scroll.dart b/lib/widgets/infinite_scroll.dart index debb16f..9d8773e 100644 --- a/lib/widgets/infinite_scroll.dart +++ b/lib/widgets/infinite_scroll.dart @@ -3,11 +3,27 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import '../hooks/ref.dart'; +class InfiniteScrollController { + Function() clear; + + InfiniteScrollController() { + usedBeforeCreation() => throw Exception( + 'Tried to use $runtimeType before it being initialized'); + + clear = usedBeforeCreation; + } + + void dispose() { + clear = null; + } +} + class InfiniteScroll extends HookWidget { final int batchSize; final Widget loadingWidget; final Widget Function(T data) builder; final Future> Function(int page, int batchSize) fetchMore; + final InfiniteScrollController controller; InfiniteScroll({ this.batchSize = 10, @@ -15,16 +31,28 @@ class InfiniteScroll extends HookWidget { const ListTile(title: Center(child: CircularProgressIndicator())), this.builder, this.fetchMore, + this.controller, }) : assert(builder != null), - assert(fetchMore != null); + assert(fetchMore != null), + assert(batchSize > 0); @override Widget build(BuildContext context) { - final page = useState(1); final data = useState>([]); final hasMore = useRef(true); final isFetching = useRef(false); + useEffect(() { + if (controller != null) { + controller.clear = () => data.value = []; + return controller.dispose; + } + + return null; + }, []); + + final page = data.value.length ~/ batchSize + 1; + return ListView.builder( // +1 for the loading widget itemCount: data.value.length + 1, @@ -39,14 +67,13 @@ class InfiniteScroll extends HookWidget { // if it's already fetching more, skip if (!isFetching.current) { isFetching.current = true; - fetchMore(page.value, batchSize).then((value) { + fetchMore(page, batchSize).then((newData) { // if got less than the batchSize, mark the list as done - if (value.length < batchSize) { + if (newData.length < batchSize) { hasMore.current = false; } // append new data and increment page count - data.value.addAll(value); - page.value++; + data.value = [...data.value, ...newData]; }).whenComplete(() => isFetching.current = false); } From ea923b032c4b441d0e2fd4965781a89b6a839178 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Fri, 18 Sep 2020 23:29:38 +0200 Subject: [PATCH 4/8] added @required decorator --- lib/widgets/infinite_scroll.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/widgets/infinite_scroll.dart b/lib/widgets/infinite_scroll.dart index 9d8773e..8bbb3aa 100644 --- a/lib/widgets/infinite_scroll.dart +++ b/lib/widgets/infinite_scroll.dart @@ -29,8 +29,8 @@ class InfiniteScroll extends HookWidget { this.batchSize = 10, this.loadingWidget = const ListTile(title: Center(child: CircularProgressIndicator())), - this.builder, - this.fetchMore, + @required this.builder, + @required this.fetchMore, this.controller, }) : assert(builder != null), assert(fetchMore != null), From 34f8f5b15562b639cdf818bdd613151f09380d38 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Fri, 18 Sep 2020 23:31:54 +0200 Subject: [PATCH 5/8] removed outdated comment --- lib/widgets/infinite_scroll.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/infinite_scroll.dart b/lib/widgets/infinite_scroll.dart index 8bbb3aa..cc7a5bb 100644 --- a/lib/widgets/infinite_scroll.dart +++ b/lib/widgets/infinite_scroll.dart @@ -72,7 +72,7 @@ class InfiniteScroll extends HookWidget { if (newData.length < batchSize) { hasMore.current = false; } - // append new data and increment page count + // append new data data.value = [...data.value, ...newData]; }).whenComplete(() => isFetching.current = false); } From 97d946c8a256c7dff6b380256075602450281ead Mon Sep 17 00:00:00 2001 From: shilangyu Date: Sat, 19 Sep 2020 00:06:12 +0200 Subject: [PATCH 6/8] hotfix: check for formatting in CI --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2704ef..8904ad9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,9 @@ jobs: - name: Run lints run: flutter analyze + - name: Run dartfmt + run: dartfmt --dry-run --set-exit-if-changed . + - name: Run tests run: flutter test From dea66fe8bf0fb88f170864d3062a5667f9da4f43 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Sat, 19 Sep 2020 00:40:47 +0200 Subject: [PATCH 7/8] hotfix: move api_extensions to dedicated folder --- lib/pages/communities_list.dart | 2 +- lib/pages/community.dart | 2 +- lib/pages/full_post.dart | 2 +- lib/pages/instance.dart | 2 +- lib/pages/profile_tab.dart | 2 +- lib/pages/users_list.dart | 2 +- lib/util/{api_extensions.dart => extensions/api.dart} | 0 lib/widgets/comment.dart | 2 +- lib/widgets/post.dart | 2 +- lib/widgets/user_profile.dart | 2 +- 10 files changed, 9 insertions(+), 9 deletions(-) rename lib/util/{api_extensions.dart => extensions/api.dart} (100%) diff --git a/lib/pages/communities_list.dart b/lib/pages/communities_list.dart index e4dc958..3e25a3c 100644 --- a/lib/pages/communities_list.dart +++ b/lib/pages/communities_list.dart @@ -2,7 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:lemmy_api_client/lemmy_api_client.dart'; -import '../util/api_extensions.dart'; +import '../util/extensions/api.dart'; import '../util/goto.dart'; import '../widgets/markdown_text.dart'; diff --git a/lib/pages/community.dart b/lib/pages/community.dart index 2838253..b54174a 100644 --- a/lib/pages/community.dart +++ b/lib/pages/community.dart @@ -10,7 +10,7 @@ import 'package:url_launcher/url_launcher.dart' as ul; import '../hooks/delayed_loading.dart'; import '../hooks/memo_future.dart'; import '../hooks/stores.dart'; -import '../util/api_extensions.dart'; +import '../util/extensions/api.dart'; import '../util/goto.dart'; import '../util/intl.dart'; import '../util/text_color.dart'; diff --git a/lib/pages/full_post.dart b/lib/pages/full_post.dart index 8c29d2f..b9ab64a 100644 --- a/lib/pages/full_post.dart +++ b/lib/pages/full_post.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:lemmy_api_client/lemmy_api_client.dart'; -import '../util/api_extensions.dart'; +import '../util/extensions/api.dart'; import '../widgets/comment_section.dart'; import '../widgets/post.dart'; diff --git a/lib/pages/instance.dart b/lib/pages/instance.dart index 0877956..0e294b5 100644 --- a/lib/pages/instance.dart +++ b/lib/pages/instance.dart @@ -7,7 +7,7 @@ import 'package:intl/intl.dart'; import 'package:lemmy_api_client/lemmy_api_client.dart'; import 'package:url_launcher/url_launcher.dart' as ul; -import '../util/api_extensions.dart'; +import '../util/extensions/api.dart'; import '../util/goto.dart'; import '../util/text_color.dart'; import '../widgets/badge.dart'; diff --git a/lib/pages/profile_tab.dart b/lib/pages/profile_tab.dart index 3142b42..c315d29 100644 --- a/lib/pages/profile_tab.dart +++ b/lib/pages/profile_tab.dart @@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import '../hooks/stores.dart'; -import '../util/api_extensions.dart'; +import '../util/extensions/api.dart'; import '../util/goto.dart'; import '../widgets/bottom_modal.dart'; import '../widgets/user_profile.dart'; diff --git a/lib/pages/users_list.dart b/lib/pages/users_list.dart index 332b43c..7f3654d 100644 --- a/lib/pages/users_list.dart +++ b/lib/pages/users_list.dart @@ -2,7 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:lemmy_api_client/lemmy_api_client.dart'; -import '../util/api_extensions.dart'; +import '../util/extensions/api.dart'; import '../widgets/markdown_text.dart'; class UsersListPage extends StatelessWidget { diff --git a/lib/util/api_extensions.dart b/lib/util/extensions/api.dart similarity index 100% rename from lib/util/api_extensions.dart rename to lib/util/extensions/api.dart diff --git a/lib/widgets/comment.dart b/lib/widgets/comment.dart index 559be00..b25a673 100644 --- a/lib/widgets/comment.dart +++ b/lib/widgets/comment.dart @@ -9,7 +9,7 @@ import 'package:timeago/timeago.dart' as timeago; import 'package:url_launcher/url_launcher.dart' as ul; import '../comment_tree.dart'; -import '../util/api_extensions.dart'; +import '../util/extensions/api.dart'; import '../util/goto.dart'; import '../util/text_color.dart'; import 'bottom_modal.dart'; diff --git a/lib/widgets/post.dart b/lib/widgets/post.dart index 1fb4c75..9e1de99 100644 --- a/lib/widgets/post.dart +++ b/lib/widgets/post.dart @@ -10,7 +10,7 @@ import 'package:url_launcher/url_launcher.dart' as ul; import '../pages/full_post.dart'; import '../url_launcher.dart'; -import '../util/api_extensions.dart'; +import '../util/extensions/api.dart'; import '../util/goto.dart'; import 'bottom_modal.dart'; import 'fullscreenable_image.dart'; diff --git a/lib/widgets/user_profile.dart b/lib/widgets/user_profile.dart index d5ebef2..60d9740 100644 --- a/lib/widgets/user_profile.dart +++ b/lib/widgets/user_profile.dart @@ -5,7 +5,7 @@ import 'package:intl/intl.dart'; import 'package:lemmy_api_client/lemmy_api_client.dart'; import 'package:timeago/timeago.dart' as timeago; -import '../util/api_extensions.dart'; +import '../util/extensions/api.dart'; import '../util/goto.dart'; import '../util/intl.dart'; import '../util/text_color.dart'; From 94ac08825e17c0f888c313feddd8b115a37a3a62 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Sat, 19 Sep 2020 01:06:35 +0200 Subject: [PATCH 8/8] fix rebase files --- lib/widgets/save_post_button.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/save_post_button.dart b/lib/widgets/save_post_button.dart index b782a6c..3d458ba 100644 --- a/lib/widgets/save_post_button.dart +++ b/lib/widgets/save_post_button.dart @@ -4,7 +4,7 @@ import 'package:lemmy_api_client/lemmy_api_client.dart'; import '../hooks/delayed_loading.dart'; import '../hooks/logged_in_action.dart'; -import '../util/api_extensions.dart'; +import '../util/extensions/api.dart'; // TODO: sync this button between post and fullpost. the same with voting