Skip to content

Instantly share code, notes, and snippets.

@jixiaoyong
Created August 13, 2023 06:40
Show Gist options
  • Save jixiaoyong/54e701411e3104c45c181b710c095a00 to your computer and use it in GitHub Desktop.
Save jixiaoyong/54e701411e3104c45c181b710c095a00 to your computer and use it in GitHub Desktop.
An infinite loading scrollable list built with riverpod which can work as expected
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'endless_list_river_pod.g.dart';
/// @author : jixiaoyong
/// @description :使用riverpod实现一个无限加载滚动的列表
/// An infinite loading scrollable list built with riverpod
///
/// _MyEndlessApp will throw issues when we scroll up back after the item has
/// been released
///
/// _MyEndlessAppSafely can work prefect with riverpod
///
/// to use this gist you will need to add 'riverpod_annotation' in your pubspec.yaml
/// test on Flutter (Channel stable, 3.10.5)
///
/// @email : jixiaoyong1995@gmail.com
/// @date : 2023/8/13
const _page_size = 20;
main() {
runApp(const ProviderScope(child: MaterialApp(home: _MyEndlessApp())));
}
/// This class will throw issues when we scroll up back after the item has
/// been released: flutter framework will earliestUsefulChild != null after we
/// change the previous loaded item to null (which because the data associate
/// with them has been released by riverpod as it thinks the widget has no longer used)
class _MyEndlessApp extends ConsumerWidget {
const _MyEndlessApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(),
body: ListView.builder(itemBuilder: (context, index) {
var page = index ~/ _page_size;
var curIndex = index % _page_size;
var data = ref.watch(fetchListDataProvider(page));
print("$index page:$page data:${data.value?.join()}");
return data.when(
data: (items) {
if (curIndex > items.length) {
return Container();
}
return Text("$index page $page content:${items[curIndex]}");
},
error: (error, stack) => const Center(child: Text("error")),
loading: () => curIndex == 0
? const Center(child: CircularProgressIndicator())
// return null after the item has already been loaded will
// case a error called "earliestUsefulChild != null"
// when we scroll up back after the item
// has already been released, follow the issues online:
// Flutter (Channel stable, 3.10.5)
// https://github.com/rrousselGit/riverpod/issues/2728
: null);
}),
floatingActionButton: FloatingActionButton(
onPressed: () async {
// here we use ref.keepAlive() to keep the loaded data alive until
// the page(_MyEndlessAppSafely) has been released, after that we
// release the data associated with it manually
var res = ref.read(keepAliveLinksProvider.notifier);
var link = res.ref.keepAlive();
await Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const _MyEndlessAppSafely()));
link.close();
},
),
);
}
}
@riverpod
Future<List<String>> fetchListData(FetchListDataRef ref, int page) async {
print("fetch page$page data start...");
await Future.delayed(Duration(seconds: 3));
print("fetch page$page data end...");
return List.generate(
_page_size, (index) => "page$page-${page * _page_size + index} ");
}
/// this page can work normally
class _MyEndlessAppSafely extends ConsumerWidget {
const _MyEndlessAppSafely({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(),
body: ListView.builder(itemBuilder: (context, index) {
var page = index ~/ _page_size;
var curIndex = index % _page_size;
var data = ref.watch(fetchListDataAndCachedProvider(page));
print("$index page:$page data:${data.value?.join()}");
return data.when(
data: (items) {
if (curIndex > items.length) {
return Container();
}
return Text("$index page $page content:${items[curIndex]}");
},
error: (error, stack) => const Center(child: Text("error")),
loading: () => curIndex == 0
? const Center(child: CircularProgressIndicator())
// as benefit of using ref.keepAlive(),the loaded data won't
// been released after we scroll down and the previous item had been
// release.
// Flutter (Channel stable, 3.10.5)
// https://github.com/rrousselGit/riverpod/issues/2728
: null);
}),
);
}
}
@riverpod
class KeepAliveLinks extends _$KeepAliveLinks {
@override
List<KeepAliveLink> build() {
print("call Keep Alive Links Build");
ref.onDispose(() {
for (var element in state) {
print("close ${state.indexOf(element)}element:${element}");
element.close();
}
});
return <KeepAliveLink>[];
}
void add(KeepAliveLink link) {
state.add(link);
print("state.length size:${state.length}");
}
List<KeepAliveLink> getAll() => state;
}
@riverpod
Future<List<String>> fetchListDataAndCached(
FetchListDataAndCachedRef ref, int page) async {
// we keep the data alive until we leave the page
// and then we will release the data manually
var link = ref.keepAlive();
ref.read(keepAliveLinksProvider.notifier).add(link);
print("fetch page$page data start...");
await Future.delayed(Duration(seconds: 3));
print("fetch page$page data end...");
return List.generate(
_page_size, (index) => "page$page-${page * _page_size + index} ");
}
@jixiaoyong
Copy link
Author

jixiaoyong commented Aug 13, 2023

the associated issues 'earliestUsefulChild != null'
flutter/flutter#130658
rrousselGit/riverpod#2728

@jixiaoyong
Copy link
Author

jixiaoyong commented Aug 13, 2023

https://github.com/bizz84/tmdb_movie_app_riverpod

https://github.com/bizz84/tmdb_movie_app_riverpod/blob/5f97913891a2aaffd3b49d637d2e49f606fc7e42/lib/src/features/movies/presentation/movies/movies_search_screen.dart#L54C5-L54C5

这里使用loading: () => const MovieListTileShimmer(),避免了上述当loading返回为空时Flutter3.x中ListView出错的问题。

其中MovieListTileShimmer是骨架Item,使用Shimmer库实现

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment