Merge pull request #45 from krawieck/infinite-scroll
This commit is contained in:
commit
33f57b7e54
|
@ -29,6 +29,9 @@ jobs:
|
||||||
- name: Run lints
|
- name: Run lints
|
||||||
run: flutter analyze
|
run: flutter analyze
|
||||||
|
|
||||||
|
- name: Run dartfmt
|
||||||
|
run: dartfmt --dry-run --set-exit-if-changed .
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: flutter test
|
run: flutter test
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
|
||||||
|
import '../widgets/infinite_scroll.dart';
|
||||||
|
|
||||||
|
InfiniteScrollController useInfiniteScrollController() =>
|
||||||
|
useMemoized(() => InfiniteScrollController());
|
|
@ -2,7 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lemmy_api_client/lemmy_api_client.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 '../util/goto.dart';
|
||||||
import '../widgets/markdown_text.dart';
|
import '../widgets/markdown_text.dart';
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import '../hooks/delayed_loading.dart';
|
||||||
import '../hooks/logged_in_action.dart';
|
import '../hooks/logged_in_action.dart';
|
||||||
import '../hooks/memo_future.dart';
|
import '../hooks/memo_future.dart';
|
||||||
import '../hooks/stores.dart';
|
import '../hooks/stores.dart';
|
||||||
import '../util/api_extensions.dart';
|
import '../util/extensions/api.dart';
|
||||||
import '../util/goto.dart';
|
import '../util/goto.dart';
|
||||||
import '../util/intl.dart';
|
import '../util/intl.dart';
|
||||||
import '../util/text_color.dart';
|
import '../util/text_color.dart';
|
||||||
|
|
|
@ -5,7 +5,7 @@ import 'package:lemmy_api_client/lemmy_api_client.dart';
|
||||||
|
|
||||||
import '../hooks/memo_future.dart';
|
import '../hooks/memo_future.dart';
|
||||||
import '../hooks/stores.dart';
|
import '../hooks/stores.dart';
|
||||||
import '../util/api_extensions.dart';
|
import '../util/extensions/api.dart';
|
||||||
import '../widgets/comment_section.dart';
|
import '../widgets/comment_section.dart';
|
||||||
import '../widgets/post.dart';
|
import '../widgets/post.dart';
|
||||||
import '../widgets/save_post_button.dart';
|
import '../widgets/save_post_button.dart';
|
||||||
|
|
|
@ -7,7 +7,7 @@ import 'package:intl/intl.dart';
|
||||||
import 'package:lemmy_api_client/lemmy_api_client.dart';
|
import 'package:lemmy_api_client/lemmy_api_client.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart' as ul;
|
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/goto.dart';
|
||||||
import '../util/text_color.dart';
|
import '../util/text_color.dart';
|
||||||
import '../widgets/badge.dart';
|
import '../widgets/badge.dart';
|
||||||
|
|
|
@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||||
|
|
||||||
import '../hooks/stores.dart';
|
import '../hooks/stores.dart';
|
||||||
import '../util/api_extensions.dart';
|
import '../util/extensions/api.dart';
|
||||||
import '../util/goto.dart';
|
import '../util/goto.dart';
|
||||||
import '../widgets/bottom_modal.dart';
|
import '../widgets/bottom_modal.dart';
|
||||||
import '../widgets/user_profile.dart';
|
import '../widgets/user_profile.dart';
|
||||||
|
|
|
@ -2,7 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lemmy_api_client/lemmy_api_client.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';
|
import '../widgets/markdown_text.dart';
|
||||||
|
|
||||||
class UsersListPage extends StatelessWidget {
|
class UsersListPage extends StatelessWidget {
|
||||||
|
|
|
@ -9,7 +9,7 @@ import 'package:timeago/timeago.dart' as timeago;
|
||||||
import 'package:url_launcher/url_launcher.dart' as ul;
|
import 'package:url_launcher/url_launcher.dart' as ul;
|
||||||
|
|
||||||
import '../comment_tree.dart';
|
import '../comment_tree.dart';
|
||||||
import '../util/api_extensions.dart';
|
import '../util/extensions/api.dart';
|
||||||
import '../util/goto.dart';
|
import '../util/goto.dart';
|
||||||
import '../util/text_color.dart';
|
import '../util/text_color.dart';
|
||||||
import 'bottom_modal.dart';
|
import 'bottom_modal.dart';
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
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<T> extends HookWidget {
|
||||||
|
final int batchSize;
|
||||||
|
final Widget loadingWidget;
|
||||||
|
final Widget Function(T data) builder;
|
||||||
|
final Future<List<T>> Function(int page, int batchSize) fetchMore;
|
||||||
|
final InfiniteScrollController controller;
|
||||||
|
|
||||||
|
InfiniteScroll({
|
||||||
|
this.batchSize = 10,
|
||||||
|
this.loadingWidget =
|
||||||
|
const ListTile(title: Center(child: CircularProgressIndicator())),
|
||||||
|
@required this.builder,
|
||||||
|
@required this.fetchMore,
|
||||||
|
this.controller,
|
||||||
|
}) : assert(builder != null),
|
||||||
|
assert(fetchMore != null),
|
||||||
|
assert(batchSize > 0);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final data = useState<List<T>>([]);
|
||||||
|
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,
|
||||||
|
itemBuilder: (_, i) {
|
||||||
|
// reached the bottom, fetch more
|
||||||
|
if (i == data.value.length) {
|
||||||
|
// if there are no more, skip
|
||||||
|
if (!hasMore.current) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it's already fetching more, skip
|
||||||
|
if (!isFetching.current) {
|
||||||
|
isFetching.current = true;
|
||||||
|
fetchMore(page, batchSize).then((newData) {
|
||||||
|
// if got less than the batchSize, mark the list as done
|
||||||
|
if (newData.length < batchSize) {
|
||||||
|
hasMore.current = false;
|
||||||
|
}
|
||||||
|
// append new data
|
||||||
|
data.value = [...data.value, ...newData];
|
||||||
|
}).whenComplete(() => isFetching.current = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadingWidget;
|
||||||
|
}
|
||||||
|
|
||||||
|
// not last element, render list item
|
||||||
|
return builder(data.value[i]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ import '../hooks/delayed_loading.dart';
|
||||||
import '../hooks/logged_in_action.dart';
|
import '../hooks/logged_in_action.dart';
|
||||||
import '../pages/full_post.dart';
|
import '../pages/full_post.dart';
|
||||||
import '../url_launcher.dart';
|
import '../url_launcher.dart';
|
||||||
import '../util/api_extensions.dart';
|
import '../util/extensions/api.dart';
|
||||||
import '../util/goto.dart';
|
import '../util/goto.dart';
|
||||||
import 'bottom_modal.dart';
|
import 'bottom_modal.dart';
|
||||||
import 'fullscreenable_image.dart';
|
import 'fullscreenable_image.dart';
|
||||||
|
|
|
@ -4,7 +4,7 @@ import 'package:lemmy_api_client/lemmy_api_client.dart';
|
||||||
|
|
||||||
import '../hooks/delayed_loading.dart';
|
import '../hooks/delayed_loading.dart';
|
||||||
import '../hooks/logged_in_action.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
|
// TODO: sync this button between post and fullpost. the same with voting
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import 'package:intl/intl.dart';
|
||||||
import 'package:lemmy_api_client/lemmy_api_client.dart';
|
import 'package:lemmy_api_client/lemmy_api_client.dart';
|
||||||
import 'package:timeago/timeago.dart' as timeago;
|
import 'package:timeago/timeago.dart' as timeago;
|
||||||
|
|
||||||
import '../util/api_extensions.dart';
|
import '../util/extensions/api.dart';
|
||||||
import '../util/goto.dart';
|
import '../util/goto.dart';
|
||||||
import '../util/intl.dart';
|
import '../util/intl.dart';
|
||||||
import '../util/text_color.dart';
|
import '../util/text_color.dart';
|
||||||
|
|
Loading…
Reference in New Issue