-
-
Save mono0926/9f920d81d80112dbdd21d88783dd96eb to your computer and use it in GitHub Desktop.
Infinite Scrolling with Riverpod
This file contains hidden or 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'; | |
void main() { | |
runApp(const ProviderScope(child: App())); | |
} | |
class App extends StatelessWidget { | |
const App({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return const MaterialApp( | |
home: HomePage(), | |
); | |
} | |
} | |
class HomePage extends StatelessWidget { | |
const HomePage({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar(), | |
body: const PopularPeopleList(), | |
); | |
} | |
} | |
// Disclaimer: This uses the "The Movie Database API (TMDB)" | |
// https://developers.themoviedb.org/3/getting-started | |
// With this endpoint: | |
// https://developers.themoviedb.org/3/people/get-popular-people | |
/// The FutureProvider that does the fetching of the paginated list of people | |
final paginatedPopularPeopleProvider = | |
FutureProvider.family<PaginatedResponse<Person>, int>( | |
(ref, int pageIndex) async { | |
final peopleRepository = ref.watch(peopleRepositoryProvider); | |
// The API request: | |
return peopleRepository.getPopularPeople(page: pageIndex + 1); | |
}, | |
); | |
/// The provider that has the value of the total count of the list items | |
/// | |
/// The [PaginatedResponse] class contains information about the total number of | |
/// pages and the total results in all pages along with a list of the provided type | |
/// | |
/// An example response from the API for any page looks like this: | |
/// { | |
/// "page": 1, | |
/// "results": [], // list of 20 items | |
/// "total_pages": 500, | |
/// "total_results": 10000 // Value taken by this provider | |
/// } | |
final popularPeopleCountProvider = Provider<AsyncValue<int>>((ref) { | |
return ref.watch(paginatedPopularPeopleProvider(0)).whenData( | |
(PaginatedResponse<Person> pageData) => pageData.totalResults, | |
); | |
}); | |
/// The provider that provides the Person data for each list item | |
/// | |
/// Initially it throws an UnimplementedError because we won't use it before overriding it | |
final currentPopularPersonProvider = Provider<AsyncValue<Person>>((ref) { | |
throw UnimplementedError(); | |
}); | |
class PopularPeopleList extends ConsumerWidget { | |
const PopularPeopleList({super.key}); | |
@override | |
Widget build(BuildContext context, WidgetRef ref) { | |
final popularPeopleCount = ref.watch(popularPeopleCountProvider); | |
// The ListView's count is from the popularPeopleCountProvider which | |
// by watching it here, causes the first fetch with a page index of 0 | |
return popularPeopleCount.when( | |
loading: () => const Center(child: CircularProgressIndicator()), | |
data: (int count) { | |
return ListView.builder( | |
itemCount: count, | |
itemBuilder: (context, index) { | |
// At this point the paginatedPopularPeopleProvider stores the values of the | |
// list items of at least the first page | |
// | |
// (index ~/ 20): Performing a truncating division of the list item index by the number of | |
// items per page gives us the value of the current page that we then access using the | |
// family modifier of the paginatedPopularPeopleProvider provider | |
// This way calling 21 ~/ 20 = 1 will fetch the second page, | |
// and 41 ~/ 20 = 2 will fetch the 3rd page, and so on. | |
final currentPopularPersonFromIndex = ref | |
.watch(paginatedPopularPeopleProvider(index ~/ 20)) | |
.whenData((pageData) => pageData.results[index % 20]); | |
return ProviderScope( | |
overrides: [ | |
// Override the Unimplemented provider | |
currentPopularPersonProvider | |
.overrideWithValue(currentPopularPersonFromIndex) | |
], | |
child: const PopularPersonListItem(), | |
); | |
}, | |
); | |
}, | |
// Handle error | |
error: (e, __) { | |
print(e); | |
return const Icon(Icons.error); | |
}, | |
); | |
} | |
} | |
class PopularPersonListItem extends ConsumerWidget { | |
const PopularPersonListItem({super.key}); | |
@override | |
Widget build(BuildContext context, WidgetRef ref) { | |
// Here we don't need to do anything but listen to the currentPopularPersonProvider's | |
// AsyncValue that was overridden in the ListView's builder | |
final personAsync = ref.watch(currentPopularPersonProvider); | |
return Container( | |
child: personAsync.when( | |
data: (Person person) => ListTile( | |
title: Text(person.name), | |
subtitle: Text('${person.id}'), | |
), // List item content | |
loading: () => | |
const Center(child: CircularProgressIndicator()), // Handle loading | |
error: (e, __) { | |
print(e); | |
return const Icon(Icons.error); | |
}, // Handle Error | |
), | |
); | |
} | |
} | |
final peopleRepositoryProvider = Provider<PeopleRepository>( | |
(ref) { | |
return HttpPeopleRepository(); | |
}, | |
); | |
abstract class PeopleRepository { | |
Future<PaginatedResponse<Person>> getPopularPeople({ | |
int page = 1, | |
}); | |
} | |
class HttpPeopleRepository implements PeopleRepository { | |
HttpPeopleRepository(); | |
@override | |
Future<PaginatedResponse<Person>> getPopularPeople({ | |
int page = 1, | |
}) async { | |
await Future<void>.delayed(const Duration(seconds: 1)); | |
return PaginatedResponse( | |
page: page, | |
totalPages: 20, | |
totalResults: 200, | |
results: List.generate( | |
20, | |
(index) => Person( | |
id: 20 * (page - 1) + index, | |
name: 'page: $page, index: $index', | |
), | |
), | |
); | |
} | |
} | |
class PaginatedResponse<T> { | |
PaginatedResponse({ | |
this.page = 1, | |
this.results = const [], | |
this.totalPages = 1, | |
this.totalResults = 1, | |
}); | |
factory PaginatedResponse.fromJson( | |
Map<String, dynamic> json, { | |
required List<T> results, | |
}) { | |
return PaginatedResponse<T>( | |
page: json['page'] as int, | |
results: results, | |
totalPages: json['total_pages'] as int, | |
totalResults: json['total_results'] as int, | |
); | |
} | |
final int page; | |
final List<T> results; | |
final int totalPages; | |
final int totalResults; | |
} | |
class Person { | |
const Person({ | |
required this.id, | |
required this.name, | |
}); | |
factory Person.fromJson(Map<String, dynamic> json) { | |
return Person( | |
id: json['id'] as int, | |
name: json['name'] as String, | |
); | |
} | |
final int id; | |
final String name; | |
Map<String, dynamic> toJson() { | |
return <String, dynamic>{ | |
'id': id, | |
'name': name, | |
}; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment