I'm trying to display data in a ListView with a FutureBuilder. In debug mode, when I launch the app, no data is displayed, but, if I reload the app (hot Reload or hot Restart), the ListView displays all the data. I already tried several approaches to solve this - even without a FutureBuilder, I still haven't succeeded. If I create a button to populate the ListView, with the same method "_getregistos()", the ListView returns the data correctly.
This is the code I'm using:
import 'package:flutter/material.dart';
import 'package:xxxxx/models/task_model.dart';
import 'package:xxxxx/shared/loading.dart';
class AddTask extends StatefulWidget {
static const id = 'add_task';
#override
_AddTaskState createState() => _AddTaskState();
}
class _AddTaskState extends State<AddTask> {
dynamic tasks;
final textController = TextEditingController();
_getRegistos() async {
List<TaskModel> taskList = await _todoHelper.getAllTask();
// print('DADOS DA tasklist: ${taskList.length}');
return taskList;
}
TaskModel currentTask;
final TodoHelper _todoHelper = TodoHelper();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
padding: EdgeInsets.all(32),
child: Column(
children: <Widget>[
TextField(
controller: textController,
),
FlatButton(
child: Text('Insert'),
onPressed: () {
currentTask = TaskModel(name: textController.text);
_todoHelper.insertTask(currentTask);
},
color: Colors.blue,
textColor: Colors.white,
),
//
FutureBuilder(
future: _getRegistos(),
builder: (context, snapshot) {
if (snapshot.hasData) {
tasks = snapshot.data;
return ListView.builder(
shrinkWrap: true,
itemCount: tasks == null ? 0 : tasks.length,
itemBuilder: (BuildContext context, int index) {
TaskModel t = tasks[index];
return Card(
child: Row(
children: <Widget>[
Text('id: ${t.id}'),
Text('name: ${t.name}'),
IconButton(
icon: Icon(Icons.delete), onPressed: () {})
],
),
);
},
);
}
return Loading();
}),
],
),
),
);
}
}
Thank you.
You need to use ConnectionState inside your builder. Look at this code template: (Currently your builder returns ListView widget without waiting for the future to complete)
return FutureBuilder(
future: yourFuture(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// future complete
// if error or data is false return error widget
if (snapshot.hasError || !snapshot.hasData) {
return _buildErrorWidget();
}
// return data widget
return _buildDataWidget();
// return loading widget while connection state is active
} else
return _buildLoadingWidget();
},
);
Thanks for your help.
I already implemented ConnectionState in the FutureBuilder and the issue persists.
When I launch the app, I get error "ERROR or No-Data" (is the message I defined in case of error of no-data.
If I click on the FlatButton to call the method "_getTasks()", the same method used in FutureBuilder, everything is ok. The method return data correctly.
This is the code refactored:
import 'package:flutter/material.dart';
import 'package:xxxx/models/task_model.dart';
import 'package:xxxx/shared/loading.dart';
class AddTask extends StatefulWidget {
static const id = 'add_task';
#override
_AddTaskState createState() => _AddTaskState();
}
class _AddTaskState extends State<AddTask> {
final textController = TextEditingController();
Future<List<TaskModel>> _getTasks() async {
List<TaskModel> tasks = await _todoHelper.getAllTask();
print('Tasks data: ${tasks.length}');
return tasks;
}
TaskModel currentTask;
//list to test with the FlatButton List all tasks
List<TaskModel> tasksList = [];
final TodoHelper _todoHelper = TodoHelper();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
padding: EdgeInsets.all(32),
child: Column(
children: <Widget>[
TextField(
controller: textController,
),
FlatButton(
child: Text('Insert'),
onPressed: () {
currentTask = TaskModel(name: textController.text);
_todoHelper.insertTask(currentTask);
},
color: Colors.blue,
textColor: Colors.white,
),
//when clicking on this flatButton, I can populate the taskList
FlatButton(
child: Text('Show all Tasks'),
onPressed: () async {
List<TaskModel> list = await _getTasks();
setState(() {
tasksList = list;
print(
'TaskList loaded by "flatButton" has ${tasksList.length} rows');
});
},
color: Colors.red,
textColor: Colors.white,
),
//
FutureBuilder(
future: _getTasks(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// future complete
// if error or data is false return error widget
if (snapshot.hasError || !snapshot.hasData) {
return Text('ERROR or NO-DATA');
}
// return data widget
return ListItems(context, snapshot.data);
// return loading widget while connection state is active
} else
return Loading();
},
),
],
),
),
);
}
}
//*****************************************
class ListItems extends StatelessWidget {
final List<TaskModel> snapshot;
final BuildContext context;
ListItems(this.context, this.snapshot);
#override
Widget build(BuildContext context) {
return Expanded(
child: ListView.builder(
itemCount: snapshot == null ? 0 : snapshot.length,
itemBuilder: (context, index) {
TaskModel t = snapshot[index];
return Text(' ${t.id} - ${t.name}');
}),
);
}
}
Related
As the following animation displays, when I tap one of the list items that StreamBuilder() is querying, it shows the items data on the right darker container (it's always Instance of '_JsonQueryDocumentSnapshot'). But at the same time in each tap, the whole list is refreshing itself, which is not very cost-effective I believe.
How can I avoid this unwanted refresh?
Answers with GetX state management dependency are also welcome.
class Schedule extends StatefulWidget {
#override
_ScheduleState createState() => _ScheduleState();
}
class _ScheduleState extends State<Schedule> {
final FirebaseFirestore _db = FirebaseFirestore.instance;
final DateTime _yesterday = DateTime.now().subtract(Duration(days: 1));
var _chosenData;
#override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: StreamBuilder<QuerySnapshot>(
stream: _db.collection('Schedule').where('date', isGreaterThan: _yesterday).limit(10).orderBy('date').snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
return ListView.builder(
itemCount: snapshot.data!.docs.length,
itemBuilder: (context, index) {
var data = snapshot.data!.docs[index];
return ListTile(
leading: Icon(Icons.person),
title: Text(data['project'], style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(data['parkour']),
onTap: () {
setState(() {_chosenData = data;});
},
);
},
);
} else {
return Center(child: CupertinoActivityIndicator());
}
},
),
),
VerticalDivider(),
Expanded(
child: Container(
alignment: Alignment.center,
color: Colors.black26,
child: Text('$_chosenData'),
),
),
],
);
}
}
To me the easiest solution would be just make it stateless and use a Getx class.
class ScheduleController extends GetxController {
var chosenData;
void updateChosenData(var data) {
chosenData = data;
update();
}
}
And your Schedule.dart would look like this:
class Schedule extends StatelessWidget {
final FirebaseFirestore _db = FirebaseFirestore.instance;
final DateTime _yesterday = DateTime.now().subtract(Duration(days: 1));
#override
Widget build(BuildContext context) {
final controller = Get.put(ScheduleController());
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: StreamBuilder<QuerySnapshot>(
stream: _db
.collection('Schedule')
.where('date', isGreaterThan: _yesterday)
.limit(10)
.orderBy('date')
.snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
return ListView.builder(
itemCount: snapshot.data!.docs.length,
itemBuilder: (context, index) {
var data = snapshot.data!.docs[index];
return ListTile(
leading: Icon(Icons.person),
title: Text(data['project'],
style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(data['parkour']),
onTap: () => controller.updateChosenData(data), // calls method from GetX class
);
},
);
} else {
return Center(child: CupertinoActivityIndicator());
}
},
),
),
VerticalDivider(),
Expanded(
child: Container(
alignment: Alignment.center,
color: Colors.black26,
child: GetBuilder<ScheduleController>(
builder: (controller) => Text('${controller.chosenData}'), // only this rebuilds
),
),
),
],
);
}
}
This way the listview.builder never rebuilds, only the Text widget directly inside the GetBuilder gets rebuilt when you selected a different ListTile.
Calling setState() notifies the framework that the state of Schedule has changed, which causes a rebuild of the widget and so your StreamBuilder.
You could move your stream logic to an upper level of the widget tree. So, setState() will not trigger a rebuild of StreamBuilder.
class ParentWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('Schedule')
.where(
'date',
isGreaterThan: DateTime.now().subtract(Duration(days: 1)),
)
.limit(10)
.orderBy('date')
.snapshots(),
builder: (context, snapshot) {
return Schedule(snapshot: snapshot); // Pass snapshot to Schedule
},
);
}
}
Another approach would be using Stream.listen in initState() which is called once. This way your stream won't be subscribed for each time setState() is called.
...
late StreamSubscription<QuerySnapshot> _subscription;
#override
void initState() {
_subscription = _db
.collection('Schedule')
.where('date', isGreaterThan: _yesterday)
.limit(10)
.orderBy('date')
.snapshots()
.listen((QuerySnapshot querySnapshot) {
setState(() {
_querySnapshot = querySnapshot;
});
});
super.didChangeDependencies();
}
#override
void dispose() {
_subscription.cancel(); // Cancel the subscription
super.dispose();
}
...
I started to use providers but I have a problem. I want to get the index of items that are in an other list in an other screen. How can i get them ? I have two screens: a home screen and a favorite screen and I have a listView in each. I want to get the index of the item in the home screen when it is remove from the favorite screen. This is the link of my code on GitHub : https://github.com/Rianou20/my_app_from_scratch/tree/master/my_app_from_scratch. And some relevant parts of my code :
favModel.dart
class FavModel extends ChangeNotifier {
List<Item> favList = [];
List<bool> isInFav = [];
addInFavorite(title, description, index){
Item item = Item(title: title, description: description, );
favList.add(item);
isInFav[index] = true;
notifyListeners();
}
removeOfFavorite(int index, int index2){
favList.removeAt(index);
isInFav[index2] = false;
notifyListeners();
}
implement(){
isInFav.add(false);
}
}
favorite_screen.dart
class Favorite extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Favorite'),
),
body: Consumer<FavModel>(
builder: (context, favModel, child) {
return ListView.builder(
itemCount: favModel.favList.length,
itemBuilder: (context, index) {
return TextObject(favModel.favList[index].title,
favModel.favList[index].description),
Padding(
padding: const EdgeInsets.all(7.0),
child: GestureDetector(
child: Icon(
Icons.favorite,
color: Colors.red,
size: 32,
),
onTap: () {
favModel.removeOfFavorite(index, index);
}),
),
});
},
),
);
}
}
home_screen.dart
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
actions: [
IconButton(
icon: Icon(Icons.favorite_border),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
fullscreenDialog: true,
builder: (context) {
return Favorite();
},
),
),
),
],
),
body: Consumer<FavModel>(builder: (context, favModel, child) {
return ListView.builder(
shrinkWrap: false,
itemCount: itemData.length,
itemBuilder: (context, index) {
favModel.implement();
return TextObject(
itemData[index].title, itemData[index].description),
Padding(
padding: const EdgeInsets.all(7.0),
child: GestureDetector(
child: Icon(
favModel.isInFav.elementAt(index)
? Icons.favorite
: Icons.favorite_border,
color:
favModel.isInFav[index] ? Colors.red : null,
size: 32,
),
onTap: () {
favModel.isInFav[index]
? null
: Provider.of<FavModel>(context,
listen: false)
.addInFavorite(
itemData[index].title,
itemData[index].description,
index,
);
}),
);
});
}),
);
}
}
Where I want to get the index is in the favorite_screen.dart at this line favModel.removeOfFavorite(index, index);
Without knowing the exact use case, you can potentially store the removed values in a list and use them on your home screen.
class FavModel extends ChangeNotifier {
List<Item> favList = [];
List<bool> isInFav = [];
List<int> _removedItemIndexList = []
get removedItemIndexList => _removedItemIndexList;
addInFavorite(title, description, countdown, imageURL, index){
Item item = Item(title: title, description: description, countdown:countdown, imageURL: imageURL);
favList.add(item);
isInFav[index] = true;
notifyListeners();
}
removeOfFavorite(int index, int index2){
favList.removeAt(index);
isInFav[index2] = false;
_addToRemovedIndexList(index);
notifyListeners();
}
void _addToRemovedIndexList(int index) {
_removedItemIndexList.add(index);
}
implement(){
isInFav.add(false);
}
}
And then use on home_sreen.dart as
...
body: Consumer<FavModel>(builder: (context, favModel, child) {
List<int> removedIndexes = favModel.removedItemIndexList;
return ListView.builder( ... ) };
Note that the FavModel provider class must be lifted above then home_screen.dart on the widget tree in order to be able to access its values. i.e. you would want to do something like this in your main.dart
...
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(
value: FavModel(),
),
],
child: MaterialApp(...
I have a Listview.builder() inside a FutureBuilder() that displays data fetched from API. I can retrieve the data successfully. But when I call the refreshData() function, previous data gets appended in the list.. How do I properly 'refresh' the widgets inside a FutureBuilder()?
Note: I'm only using get request here, so it's impossible that the data gets duplicated in the back-end. So the problem actually lies in displaying the data.
Here is my code:
class _MyHomePageState extends State<MyHomePage> {
List<Giver> _givers = [];
Future giversList;
getData() async {
_givers.addAll(await NetworkHelper().fetchGivers());
return _givers;
}
refreshData() {
giversList = getData();
}
#override
void initState() {
super.initState();
giversList = getData();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: <Widget>[
RaisedButton(
onPressed: (){
setState(() {
refreshData();
});
},
child: Text('Refresh'),
),
FutureBuilder(
future: giversList,
builder: (context, snapShot){
switch(snapShot.connectionState) {
case ConnectionState.none:
return Center(child: Text('none'));
case ConnectionState.active:
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
//this is where the listview is created
case ConnectionState.done:
return ListView.builder(
shrinkWrap: true,
itemCount: _givers.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(snapShot.data[index].name),
subtitle: Text(snapShot.data[index].address),
);
});
default:
return Center(child: Text('Default!'));
}
},
)
],
),
),
);
}
}
As #pskink mentioned in the comment above, I just replaced _givers.addAll(await NetworkHelper().fetchGivers()); with _givers = await NetworkHelper().fetchGivers();
Thanks for the help!
I have a StreamBuilder inside my Widget build of UserListDart:
StreamBuilder(
stream: stream.asStream(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if(snapshot.hasData) {
return Expanded(
child: ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
snapshot.data[index].firstname + " " +
snapshot.data[index].lastname
),
onTap: () {
Navigator.of(context).push(DetailScreenDart(snapshot.data[index]));
},
);
}
)
);
}
}
...
)
The Stream is defined in the initState:
Future<List> stream;
#override
void initState() {
super.initState();
stream = fetchPost();
}
The fetchPost() is an api call:
Future<List<User>> fetchPost() async {
final response = await http.get('url');
final jsonResponse = json.decode(response.body);
List<User> users = [];
for(var u in jsonResponse){
User user = User(
firstname: u["firstname"],
lastname: u["lastname"],
);
users.add(user);
}
return users;
}
I Navigate to another Page to change for example the firstname (api get updated) and I Navigate back to the UserList:
Navigator.pushReplacement(
context,
new MaterialPageRoute(builder: (context) => new UserListDart())
).then((onValue) {
fetchPost();
});
But the StreamBuilder won't get updated and I don't know why.
Note:
I think the StreamBuilder don't realise that a change has happend when I navigate back. It only applies the changes if I reopen the Page..
You should be using setState and updating your stream variable with the result of the fetchList() call:
Navigator.pushReplacement(
context,
new MaterialPageRoute(builder: (context) => new UserListDart())
).then((onValue) {
setState((){
stream = fetchPost();
});
});
Here's a working example of what you want to achieve:
class StreamBuilderIssue extends StatefulWidget {
#override
_StreamBuilderIssueState createState() => _StreamBuilderIssueState();
}
class _StreamBuilderIssueState extends State<StreamBuilderIssue> {
Future<List<String>> futureList;
List<String> itemList = [
'item 1',
'item 1',
'item 1',
'item 1',
'item 1',
];
#override
void initState() {
futureList = fetchList();
super.initState();
}
#override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: Center(
child: StreamBuilder(
stream: futureList.asStream(),
builder: (context, snapshot){
if(snapshot.hasData){
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index){
return Text(snapshot.data[index]);
},
);
}else{
return CircularProgressIndicator();
}
},
),
),
),
RaisedButton(
onPressed: goToAnotherView,
child: Text('Next View'),
),
RaisedButton(
onPressed: addItem,
child: Text('AddItem'),
)
],
),
);
}
Future<List<String>> fetchList(){
return Future.delayed(Duration(seconds: 2), (){
return itemList;
});
}
void goToAnotherView(){
Navigator.push(context, MaterialPageRoute(
builder: (context){
return StreamBuilderIssueNewView(addItem);
})
).then((res){
setState(() {
futureList = fetchList();
});
});
}
void addItem(){
itemList.add('anotherItem');
}
}
class StreamBuilderIssueNewView extends StatelessWidget {
final Function buttonAction;
StreamBuilderIssueNewView(this.buttonAction);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
children: <Widget>[
Text('New view'),
RaisedButton(
onPressed: buttonAction,
child: Text('AddItem'),
)
],
),
),
);
}
}
By the way, you could also just use a FutureBuilder as your are not using a real Stream here, just an api fetch and you have to update with setState anyway.
My dashboard code looks like this,
Here I am doing get req in getReport method, I have added the RefreshIndicator in the code which when pulled down inside container should do the refresh, there I am calling my getData(), But I am not getting the refreshed content, I am adding my code below, let me know if anywhere I made a mistake.
below my dashboard.dart
class Window extends StatefulWidget {
#override
_WindowState createState() => _WindowState();
}
class _WindowState extends State<Window> {
Future reportList;
#override
void initState() {
super.initState();
reportList = getReport();
}
Future<void> getReport() async {
http.Response response =
await http.get(reportsListURL, headers: {"token": "$token"});
switch (response.statusCode) {
case 200:
String reportList = response.body;
var collection = json.decode(reportList);
return collection;
case 403:
break;
case 401:
return null;
default:
return 1;
}
}
getRefreshScaffold() {
return Center(
child: RaisedButton(
onPressed: () {
setState(() {
reportList = getReport();
});
},
child: Text('Refresh, Network issues.'),
),
);
}
getDashBody(var data) {
double maxHeight = MediaQuery.of(context).size.height;
return Column(
children: <Widget>[
Container(
height: maxHeight - 800,
),
Container(
margin: new EdgeInsets.all(0.0),
height: maxHeight - 188,
child: new Center(
child: new RefreshIndicator( //here I am adding the RefreshIndicator
onRefresh:getReport, //and calling the getReport() which hits the get api
child: createList(context, data),
),),
),
],
);
}
Widget createList(BuildContext context, var data) {
Widget _listView = ListView.builder(
itemCount: data.length,
itemBuilder: (context, count) {
return createData(context, count, data);
},
);
return _listView;
}
createData(BuildContext context, int count, var data) {
var metrics = data["statistic_cards"].map<Widget>((cardInfo) {
var cardColor = getColorFromHexString(cardInfo["color"]);
if (cardInfo["progress_bar"] != null && cardInfo["progress_bar"]) {
return buildRadialProgressBar(
context: context,
progressPercent: cardInfo["percentage"],
color: cardColor,
count: cardInfo["value"],
title: cardInfo["title"],
);
} else {
return buildSubscriberTile(context, cardInfo, cardColor);
}
}).toList();
var rowMetrics = new List<Widget>();
for (int i = 0; i < metrics.length; i += 2) {
if (i + 2 < metrics.length)
rowMetrics.add(Row(children: metrics.sublist(i, i + 2)));
else
rowMetrics.add(Row(children: [metrics[metrics.length - 1], Spacer()]));
}
return SingleChildScrollView(
child: LimitedBox(
// maxHeight: MediaQuery.of(context).size.height / 1.30,
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: rowMetrics,
),
),
);
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: reportList,
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
return Center(
child: CircularProgressIndicator(),
);
case ConnectionState.done:
var data = snapshot.data;
if (snapshot.hasData && !snapshot.hasError) {
return getDashBody(data);
} else if (data == null) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text("Timeout! Log back in to continue"),
Padding(
padding: EdgeInsets.all(25.0),
),
RaisedButton(
onPressed: () {
setState(() {
token = null;
});
Navigator.of(context).pushReplacement(
CupertinoPageRoute(
builder: (BuildContext context) => LoginPage()),
);
},
child: Text('Login Again!'),
),
],
),
);
} else {
getRefreshScaffold();
}
}
},
);
}
}
Basic Example
Below is a State class of a StatefulWidget, where:
a ListView is wrapped in a RefreshIndicator
numbersList state variable is its data source
onRefresh calls _pullRefresh function to update data & ListView
_pullRefresh is an async function, returning nothing (a Future<void>)
when _pullRefresh's long running data request completes, numbersList member/state variable is updated in a setState() call to rebuild ListView to display new data
import 'package:flutter/material.dart';
import 'dart:math';
class PullRefreshPage extends StatefulWidget {
const PullRefreshPage();
#override
State<PullRefreshPage> createState() => _PullRefreshPageState();
}
class _PullRefreshPageState extends State<PullRefreshPage> {
List<String> numbersList = NumberGenerator().numbers;
#override
Widget build(BuildContext context) {
return Scaffold(
body: RefreshIndicator(
onRefresh: _pullRefresh,
child: ListView.builder(
itemCount: numbersList.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(numbersList[index]),
);
},),
),
);
}
Future<void> _pullRefresh() async {
List<String> freshNumbers = await NumberGenerator().slowNumbers();
setState(() {
numbersList = freshNumbers;
});
// why use freshNumbers var? https://stackoverflow.com/a/52992836/2301224
}
}
class NumberGenerator {
Future<List<String>> slowNumbers() async {
return Future.delayed(const Duration(milliseconds: 1000), () => numbers,);
}
List<String> get numbers => List.generate(5, (index) => number);
String get number => Random().nextInt(99999).toString();
}
Notes
If your async onRefresh function completes very quickly, you may want to add an await Future.delayed(Duration(seconds: 2)); after it, just so the UX is more pleasant.
This gives time for the user to complete a swipe / pull down gesture & for the refresh indicator to render / animate / spin indicating data has been fetched.
FutureBuilder Example
Here's another version of the above State<PullRefreshPage> class using a FutureBuilder, which is common when fetching data from a Database or HTTP source:
class _PullRefreshPageState extends State<PullRefreshPage> {
late Future<List<String>> futureNumbersList;
#override
void initState() {
super.initState();
futureNumbersList = NumberGenerator().slowNumbers();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<List<String>>(
future: futureNumbersList,
builder: (context, snapshot) {
return RefreshIndicator(
child: _listView(snapshot),
onRefresh: _pullRefresh,
);
},
),
);
}
Widget _listView(AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(snapshot.data[index]),
);
},);
}
else {
return Center(
child: Text('Loading data...'),
);
}
}
Future<void> _pullRefresh() async {
List<String> freshNumbers = await NumberGenerator().slowNumbers();
setState(() {
futureNumbersList = Future.value(freshNumbers);
});
}
}
Notes
slowNumbers() function is the same as in the Basic Example above, but the data is wrapped in a Future.value() since FutureBuilder expects a Future, but setState() should not await async data
according to Rémi, Collin & other Dart/Flutter demigods it's good practice to update Stateful Widget member variables inside setState() (futureNumbersList in FutureBuilder example & numbersList in Basic example), after its long running async data fetch functions have completed.
see https://stackoverflow.com/a/52992836/2301224
if you try to make setState async, you'll get an exception
updating member variables outside of setState and having an empty setState closure, may result in hand-slapping / code analysis warnings in the future
Not sure about futures, but for refresh indicator you must return a void so
Use something like
RefreshIndicator(
onRefresh: () async {
await getData().then((lA) {
if (lA is Future) {
setState(() {
reportList = lA;
});
return;
} else {
setState(() {
//error
});
return;
}
});
return;
},
Try this and let me know!
EDIT:
Well, then just try this inside you refresh method
setState(() {
reportList = getReport();
});
return reportList;
Try this:
onRefresh: () {
setState(() {});
}}
instead of onRefresh:getReport
reportList field is Future which returns its value once. So, when you call getReport again it changes nothing. Actually, more correctly it'll be with Stream and StreamBuilder instead of Future and FutureBuilder. But for this code it can be shortest solution
Easy method: you can just use Pull Down to Refresh Package - https://pub.dev/packages/pull_to_refresh
In Non-scrollable list view, RefreshIndicator does not work, so you have to wrap your widget with Stack for implementing pull down to refresh.
RefreshIndicator(
onRefresh: () {
// Refresh Functionality
},
child: Stack(
children: [
ListView(
padding: EdgeInsets.zero,
shrinkWrap: true,
children: [
SizedBox(
height: MediaQuery.of(context).size.height,
)
],
),
// Your Widget
],
);
),
I am working on a huge project which contains CustomScrollView, NestedScrollView, ListView, etc I tried every answer above and all of the answers use RefreshIndicator from flutter SDK. It doesn't work entirely with my app because I also have horizontal scroll views. So in order to implement it I had to use NestedScrollView on almost every screen. Then I came to know about liquid_pull_to_refresh, applied it to the top widget, and WOLAAH! If you need a separate logic for each screen then use it at the top of each screen but in my case, I'm refreshing the whole project's data.