Created
August 13, 2023 06:40
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} "); | |
} |
https://github.com/bizz84/tmdb_movie_app_riverpod
这里使用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
the associated issues 'earliestUsefulChild != null'
flutter/flutter#130658
rrousselGit/riverpod#2728