Last active
March 28, 2024 11:24
-
-
Save epatel/c74f136c5c9e09df917ba53064024b2b to your computer and use it in GitHub Desktop.
ListView.builder with delayed loading
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 'dart:math' as math; | |
void main() { | |
runApp(const MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
debugShowCheckedModeBanner: false, | |
home: Scaffold( | |
body: FutureBuilder( | |
future: Api.fetchPage(), // Load first page | |
builder: (context, snapshot) { | |
if (!snapshot.hasData) { | |
return Center( | |
child: CircularProgressIndicator(), | |
); | |
} | |
// Track pageloading with this list | |
List<PageLoader?> pageLoaders = [ | |
// Pre-build loader for first page just loaded | |
PageLoader( | |
pageIndex: 1, | |
pageFuture: Future(() => snapshot.data!), | |
), | |
// Prepare rest with empty pageloader slots | |
...List.generate( | |
snapshot.data!.totalPages - 1, | |
(_) => null, | |
), | |
]; | |
return ListView.builder( | |
itemCount: snapshot.data!.totalItems, | |
prototypeItem: ListTile( | |
title: Text('Abc'), | |
), | |
itemBuilder: (context, index) { | |
int pageIndex = index ~/ Api.numItemsPerPage; | |
PageLoader? pageLoader = pageLoaders[pageIndex]; | |
if (pageLoader == null || | |
pageLoader.state == PageLoaderState.canceled) { | |
pageLoader = PageLoader(pageIndex: pageIndex + 1); | |
pageLoaders[pageIndex] = pageLoader; | |
} | |
return ListTile( | |
tileColor: Color( | |
(math.Random(pageIndex + 42).nextDouble() * 0xFFFFFF) | |
.toInt()) | |
.withOpacity(0.5), | |
title: Item( | |
pageLoader: pageLoader, | |
index: index, | |
), | |
); | |
}, | |
); | |
}, | |
), | |
), | |
); | |
} | |
} | |
class Item extends StatefulWidget { | |
final PageLoader pageLoader; | |
final int index; | |
const Item({ | |
required this.pageLoader, | |
required this.index, | |
Key? key, | |
}) : super(key: key); | |
@override | |
State<Item> createState() => _ItemState(); | |
} | |
class _ItemState extends State<Item> { | |
@override | |
void initState() { | |
super.initState(); | |
widget.pageLoader.activate(); | |
} | |
@override | |
void dispose() { | |
widget.pageLoader.cancel(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return FutureBuilder( | |
future: widget.pageLoader.pageFuture, | |
builder: (context, snapshot) { | |
if (!snapshot.hasData) { | |
return Text('Page: __, Item: ____'); | |
} | |
final page = snapshot.data!; | |
final item = | |
page.items[widget.index - (page.page - 1) * Api.numItemsPerPage]; | |
return Text(item); | |
}); | |
} | |
} | |
// -------------------- | |
enum PageLoaderState { | |
idle, | |
fetching, | |
fetched, | |
canceled, | |
} | |
class PageLoader { | |
late PageLoaderState state; | |
final int pageIndex; | |
late Future<Page?> pageFuture; | |
int tiles = 0; | |
PageLoader({ | |
required this.pageIndex, | |
Future<Page?>? pageFuture, | |
}) { | |
if (pageFuture == null) { | |
state = PageLoaderState.idle; | |
// Add 1s delay before loading, not loading pages scrolled passed | |
this.pageFuture = Future.delayed(Duration(seconds: 1)).then((_) { | |
if (state != PageLoaderState.canceled) { | |
state = PageLoaderState.fetching; | |
final pageFuture = Api.fetchPage(page: pageIndex); | |
pageFuture.then((_) { | |
state = PageLoaderState.fetched; | |
}); | |
return pageFuture; | |
} | |
return null; | |
}); | |
} else { | |
state = PageLoaderState.fetched; | |
this.pageFuture = pageFuture; | |
} | |
} | |
void activate() { | |
tiles++; | |
} | |
void cancel() { | |
if (--tiles == 0 && state == PageLoaderState.idle) { | |
state = PageLoaderState.canceled; | |
} | |
} | |
} | |
// -------------------- | |
class Page { | |
final int totalItems; | |
final int page; | |
final int totalPages; | |
final List<String> items; | |
Page({ | |
required this.totalItems, | |
required this.page, | |
required this.totalPages, | |
required this.items, | |
}); | |
String toString() { | |
return '{totalItems: $totalItems, page: $page, totalPages: $totalPages, items: $items}'; | |
} | |
} | |
// -------------------- | |
class Api { | |
static final numItems = 2000; | |
static final numItemsPerPage = 25; | |
static Future<Page> fetchPage({ | |
int page = 1, | |
}) async { | |
// Simulate delay from API call... | |
await Future.delayed(Duration(seconds: 1)); | |
print('fetchPage: $page'); | |
return Page( | |
totalItems: numItems, | |
page: page, | |
totalPages: numItems ~/ numItemsPerPage + | |
((numItems % numItemsPerPage > 0) ? 1 : 0), | |
items: List.generate( | |
numItemsPerPage, | |
(index) => | |
'Page: $page, Item: ${numItemsPerPage * (page - 1) + index + 1}', | |
), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment