Reputation: 31
Flutter newbie, learned a lot from here.
In a common case, people will request resource from web servers and server return an array of objects:
[ { "key": "value"}, {"key": "value}...]
We can easily handle this use FutureBuilder.
But I have a server with massive data which I have to get resource in this way:
So I have a list with fix count some kind, but the resource have to dynamic loaded from server. how to do that?
Some code.
@override
Widget build(BuildContext context) {
// _max == -1, request in progress.
return _max == -1
? new CircularProgressIndicator()
: ListView.builder(
padding: const EdgeInsets.all(16.0),
itemBuilder: (context, i) {
return new FutureBuilder(
future: _getFollowingContracts(),
builder: (context, snapshot) {
if (i.isOdd) return new Divider();
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
return new Text('loading...');
case ConnectionState.done:
if (i.isOdd)
return new Divider(
height: 2.0,
);
final int index = i ~/ 2;
// when user scroll down here we got exception
// because only 20 records is available.
// how to get another 20 records?
return _buildRow(_contracts[index]);
}
},
);
},
itemCount: _max * 2,
);
}
Upvotes: 3
Views: 4825
Reputation: 1081
What you're looking for is "infinite-scrolling". There are Flutter tutorials out there which explain this in detail, and here a common pattern that I've found works well.
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
class Feed extends StatefulWidget {
@override
State<StatefulWidget> createState() => new _FeedState();
}
class _FeedState extends State<Feed> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
new GlobalKey<RefreshIndicatorState>();
bool _initialized;
bool _readyToFetchNextPage;
ScrollController _scrollController;
Repository _repository;
int _page;
Map<int, Widget> _cache;
@override
void initState() {
_initialized = false;
_repository = Repository();
_readyToFetchNextPage = true;
_initialize();
super.initState();
}
Future<Null> _initialize() async {
_initialized = false;
_readyToFetchNextPage = false;
final Completer<Null> completer = new Completer<Null>();
// Reset cache
_cache = new Map();
// Reset page
_page = 1;
// Fetch initial data
List results = repository.getPage(_page);
// Add results to the cache
_update(_cache.length, results);
completer.complete();
// Let the refresh indicator know the method has completed
_readyToFetchNextPage = true;
// SetState must be called to dismiss the CircularProgressIndicator
setState(() { _initialized = true; });
return completer.future;
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
body: new RefreshIndicator(
key: _refreshIndicatorKey,
onRefresh: _initialize,
child: CustomScrollView(
slivers: <Widget>[
AppBar(),
(_initialized) ? new SliverList(
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return _getItem(index);
}),
) : new SliverFillRemaining(
child: new Center(
child: new CircularProgressIndicator(),
),
),
],
),
),
);
}
Widget _getItem(int index) {
if (_cache.containsKey(index)) {
return _cache[index];
} else {
if (_readyToFetchNextPage) {
_readyToFetchNextPage = false;
_page += 1;
_repository.getPage(_page).then((results) {
setState(() {
_update(_cache.length, results);
});
});
}
return null;
}
}
void _update(int offset, List results) {
for (int i = 0; i < results.length; i += 1) {
int index = i + offset;
_cache.putIfAbsent(
index,
() => new ListTile(
key: new Key('post-$index'),
title: Text(results[i]),
));
}
_readyToFetchNextPage = true;
}
}
This works as follows.
Repository
which makes your API calls given a page number
Ex: repository.getPage(1) => GET http://api.com?page=1 => List
_update
_update
adds all of these posts to a cache (a Map).ScrollView
takes a function called getItem(int index)
which must return a Widget
for the given index.getItem
function looks for the item in the cache, and returns it if it's found.Repository
to get the next page and update the cache and so on...You should certainly consider using this pattern as it will allow to you to fetch new pages on demand.
IMPORTANT This is not a direct implementation. The Repository
that I'm "using" in this code sample is meant to show you where you would make your API calls. It is not an existing class, rather an abstract representation of a repository that you would implement. Furthermore, you will specify the Widget
you would like to appear in the list when you call _cache.putIfAbsent(index, () => MyWidget())
Upvotes: 1
Reputation: 5632
To implement this, you can attach a ScrollController
to your ListView
and listen its changes. In this listener when you are in the end of the scroll you fetch more data and update your app state.
Example:
The following code is extracted from this well explained blog post: https://marcinszalek.pl/flutter/infinite-dynamic-listview/.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
theme: new ThemeData(primarySwatch: Colors.blue),
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<int> items = List.generate(10, (i) => i);
ScrollController _scrollController = new ScrollController();
bool isPerformingRequest = false;
@override
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
_getMoreData();
}
});
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
_getMoreData() async {
if (!isPerformingRequest) {
setState(() => isPerformingRequest = true);
List<int> newEntries = await fakeRequest(
items.length, items.length + 10); //returns empty list
if (newEntries.isEmpty) {
double edge = 50.0;
double offsetFromBottom = _scrollController.position.maxScrollExtent -
_scrollController.position.pixels;
if (offsetFromBottom < edge) {
_scrollController.animateTo(
_scrollController.offset - (edge - offsetFromBottom),
duration: new Duration(milliseconds: 500),
curve: Curves.easeOut);
}
}
setState(() {
items.addAll(newEntries);
isPerformingRequest = false;
});
}
}
Widget _buildProgressIndicator() {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: new Opacity(
opacity: isPerformingRequest ? 1.0 : 0.0,
child: new CircularProgressIndicator(),
),
),
);
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
title: Text("Infinite ListView"),
),
body: ListView.builder(
itemCount: items.length + 1,
itemBuilder: (context, index) {
if (index == items.length) {
return _buildProgressIndicator();
} else {
return ListTile(title: new Text("Number $index"));
}
},
controller: _scrollController,
),
);
}
}
/// from - inclusive, to - exclusive
Future<List<int>> fakeRequest(int from, int to) async {
return Future.delayed(Duration(seconds: 2), () {
return List.generate(to - from, (i) => i + from);
});
}
You will have to adapt this code to your scenario (mainly by changing the fakeRequest
method and how the tiles are rendered), but I think it will give you the main idea.
Upvotes: 3