Skip to content

Instantly share code, notes, and snippets.

@mono0926
Forked from Roaa94/infinite_scrolling.dart
Last active August 1, 2022 10:27
Show Gist options
  • Save mono0926/9f920d81d80112dbdd21d88783dd96eb to your computer and use it in GitHub Desktop.
Save mono0926/9f920d81d80112dbdd21d88783dd96eb to your computer and use it in GitHub Desktop.
Infinite Scrolling with Riverpod
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