FutureBuilder gives an unexpected result and slowing UI down - flutter

It is a first statefull widget
bool _isPressed = false;
...
ElevatedButton(
child: const Text('Run long calculations'),
onPressed: () {
setState(() {
_isPressed = !_isPressed;
});
},
),
_isPressed ? const Result() : Container(),
...
and Result widget with its builds function returns
FutureBuilder<String>(
future: _process(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: LinearProgressIndicator(),
);
} else {
if (snapshot.error != null) {
return const Center(
child: Text('An error occurred'),
);
} else {
return Text('${snapshot.data}');
}
}
},
);
Future<String> _process() async {
await argon2.hashPasswordString('dummy text', salt: Salt.newSalt()); // long calculations
return 'dummy result';
}
Why the FutureBuilder does not render LinearProgressIndicator before it render final text? Actualy, the LinearProgressIndicator is rendered for a very small amount of time before final text rendered, but there is something wrong with it, because the circular indicator should spin much longer.
_process() seems to slow down the application and that's why the progress indicator does not spin. But how can it be if the result of the computation is Future and the code awaits for it...

I think its better to change your conditions like below .
based on flutter Doc
if (snapshot.hasData) {
// data
return Text('${snapshot.data}');
} else if (snapshot.hasError) {
// error
} else {
// CircularProgressIndicator
return SizedBox(
child: CircularProgressIndicator(),
width: 60,
height: 60,
);
}

If this answer does not help you and you think have a UI freeze because of heavy task in _process() method you should do the process task in separate Isolate.

Your code is fine, if you replace the _getHash body with just a Future.delayed() the progress indicator shows fine. Hence the problem is in hashPasswordString. If you look at the implementation of this function you'll notice that in fact it is synchronous.
So the quick fix would be - create a static function like that:
static String _calculateHash(String input) {
final result = argon2.hashPasswordStringSync(input,
salt: Salt.newSalt(), iterations: 256, parallelism: 8);
return result.hexString;
}
and use it with the compute function:
Future<String> _hash() {
return compute(_calculateHash, 'input text');
// this is not needed anymore
// DArgon2Result result = await argon2.hashPasswordString('input text',
// salt: Salt.newSalt(), iterations: 256, parallelism: 8);
// return result.hexString;
}
static String _calculateHash(String input) {
final result = argon2.hashPasswordStringSync(input,
salt: Salt.newSalt(), iterations: 256, parallelism: 8);
return result.hexString;
}
The long and proper fix - create a PR for the dargon2_flutter package.

The problem is with this line:
if (snapshot.connectionState == ConnectionState.waiting)
You see, ConnectionState.waiting is used when there is no connection yet, for example when a stream has no value.
Here is what each connection state is:
Active
after an asyncronous computation started, but before it ends.
None
When there is no asyncronous computation at all (for example, the future is None on a future builder)
Done
After the asyncronous computation has ended
Waiting
Before the asynchronous computation begins
So when you check if the connection state is waiting, the value is true for a split second and then the connection state switches to active, here is what your if statement should look like:
if (snapshot.connectionState == ConnectionState.active)

MohandeRr has suggested the impmentation flutter docs has used, but i usually do it like this
if (snapshot.connectionState != ConnectionState.done) {
return const Center(
child: LinearProgressIndicator(),
);
}
if (snapshot.hasError) {
return const Center(
child: Text('An error occurred'),
);
}
return Text('${snapshot.data}');

import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Home(),
);
}
}
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
bool _isPressed = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Demo"),
actions: [
TextButton(
child: const Text(
'Press',
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {
setState(() {
_isPressed = !_isPressed;
});
},
)
],
),
body: _isPressed
? FutureBuilder<String>(
future: process(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Center(
child: Text(snapshot.data ?? ""),
);
} else if (snapshot.hasError) {
return const Center(
child: Text('An error occurred'),
);
} else {
return const Center(
child: LinearProgressIndicator(),
);
}
},
)
: const Center(
child: Text("Hidden"),
),
);
}
Future<String> process() async {
await Future.delayed(const Duration(seconds: 3));
return "Hello World";
}
}

There are 2 problems at play here:
You are creating a new Future (_process()) for every build loop. You need to put that in your state and reuse/clear it appropriately.
You are using the wrong ConnectionState check - snapshot.connectionState != ConnectionState.done is probably what you need

Related

Future builder runs forever, if memoizer used doesnt notify to the listerners

I am trying to switch the drawer tab, according to the value stored in shared preferences using the following code.
code works fine when memoizer is not used but future builder runs forever.
If I use memorizer future builder still runs at least two times (not forever), but get and set functions doesn't work and new values are not updated and are not notified to the widgets.
I need some way to stop running future builder forever and notify users as well accordingly by triggering get and set functions present in it
Notifier class
class SwitchAppProvider extends ChangeNotifier {
switchApp(value) async {
// initialize instance of sharedpreference
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool('key', value);
notifyListeners();
}
Future<bool?> getValue() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final value = prefs.getBool('key');
return value;
}
}
Drawer
Widget _buildDrawer() {
return ChangeNotifierProvider<SwitchAppProvider>(
create: (context) => SwitchAppProvider(),
child: Consumer<SwitchAppProvider>(
builder: (context, provider, _) {
return Container(
width: 260,
child: Drawer(
child: Material(
color: Color.fromRGBO(62, 180, 137, 1),
child: ListView(
children: <Widget>[
Container(
padding: AppLandingView.padding,
child: Column(
children: [
const SizedBox(height: 10),
FutureBuilder(
future: provider.getValue(),
builder: (BuildContext context,
AsyncSnapshot<dynamic> snapshot) {
if (snapshot.data == true) {
return _buildMenuItem(
text: 'widget1',
icon: Icons.add_business,
onTap: () {
provider.switchApp(false);
},
);
} else {
return _buildMenuItem(
text: 'widget2',
icon: Icons.add_business,
onTap: () {
provider.switchApp(true);
},
);
}
},
),
],
),
),
],
),
),
),
);
},
),
);
}
Scaffold
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: _buildDrawer(),
);
}
Update
I analysed further, problem lies in provider.getValue(), if i use notifyListeners() before returning the value future builder runs forever
I removed it and the future builder doesn't run forever, but it doesn't update other widgets.
Scenario is
widget 1
contains a drawer
has a button to switch app
on tap value is set using shared preferences (setValue() function) and listeners are notified
in widget 1 notifier is working well and changing the drawer button option when setValue() is called on tap.
everything resolved in widget 1, as its calling setValue() hence notifyListeners() is triggered and widget1 is rerendered
widget 2
only gets value from shared preferences(getValue() function). getValue function cant use notifyListeners(), if used futurebuilder is running forever
widget 2 don't set any value so it doesn't use setValue() hence it's not getting notified
how I can notify widget 2, when on tap setValue() is triggered in widget 1
i.e widget1 sets the app using setValue() function
widget2 gets value from getValue() function and get notified
Update 2
class SwitchAppProvider with ChangeNotifier {
dynamic _myValue;
dynamic get myValue => _myValue;
set myValue(dynamic newValue) {
_myValue = newValue;
notifyListeners();
}
setValue(value) async {
// initialize instance of sharedpreference
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setBool('key', value);
notifyListeners();
}
SwitchAppProvider(){
getValue();
}
Future<void> getValue() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
myValue = prefs.getBool('key');
}
}
widget 2
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: SwitchAppProvider(),
child: Consumer<SwitchAppProvider>(
builder: (BuildContext context, SwitchAppProvider provider, _) {
if (provider.myValue == true) {
return Center(
child: CircularProgressIndicator(),
);
} else {
return Container(
child: Text('provider.myValue'));
}
})
);
}
}
_buildMenuItem
// helper widget to build item of drawer
Widget _buildMenuItem({
required String text,
required IconData icon,
required GestureTapCallback onTap,
}) {
final color = Colors.white;
final hoverColor = Colors.white;
return ListTile(
leading: Icon(icon, color: color),
title: Text(text, style: TextStyle(color: color, fontSize: 18)),
hoverColor: hoverColor,
onTap: onTap,
);
}
"If I use memorizer future builder still runs at least two times (not forever), but get and set functions doesn't work and new values are not updated and are not notified to the widgets."
That is the expected behaviour:
An AsyncMemoizer is used when some function may be run multiple times in order to get its result, but it only actually needs to be run once for its effect.
so prefs.setBool('key', value); is executed only the first time.
You definitely do not want to use it.
If you edit your code to remove the AsyncMemoizer, we can try to help you further.
Edit after Update
You are right, the getValue() function should not notify listeners, if it does that, then the listeners will rebuild and ask for the value again, which will notify listeners, which will rebuild and ask for the value again, which... (you get the point).
There is something wrong in your reasoning. widget1 and widget2 are not notified, the Consumer is notified. Which will rebuild everything. The code is quite complicated and it could be simplified a lot by removing unneeded widgets.
I will suggest you to
await prefs.setBool('isWhatsappBusiness', value); before notifying listeners.
have a look at this answer for a similar problem.
Edit 3
I do not know what you are doing wrong, but this works:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
runApp(const MyApp());
}
class SwitchAppProvider extends ChangeNotifier {
switchApp(value) async {
// initialize instance of sharedpreference
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setBool('key', value);
notifyListeners();
}
Future<bool?> getValue() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final value = prefs.getBool('key');
return value;
}
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(),
drawer: _buildDrawer(),
),
);
}
Widget _buildDrawer() {
return ChangeNotifierProvider<SwitchAppProvider>(
create: (context) => SwitchAppProvider(),
child: Consumer<SwitchAppProvider>(
builder: (context, provider, _) {
return SizedBox(
width: 260,
child: Drawer(
child: Material(
color: const Color.fromRGBO(62, 180, 137, 1),
child: ListView(
children: <Widget>[
Column(
children: [
const SizedBox(height: 10),
FutureBuilder(
future: provider.getValue(),
builder: (BuildContext context,
AsyncSnapshot<dynamic> snapshot) {
print('Am I building?');
if (snapshot.data == true) {
return ListTile(
tileColor: Colors.red[200],
title: const Text('widget1'),
leading: const Icon(Icons.flutter_dash),
onTap: () {
provider.switchApp(false);
},
);
} else {
return ListTile(
tileColor: Colors.green[200],
title: const Text('widget2'),
leading: const Icon(Icons.ac_unit),
onTap: () {
provider.switchApp(true);
},
);
}
},
),
],
),
],
),
),
),
);
},
),
);
}
}
If you still cannot get it working, then the problem is somewhere else.
Edit 4
First, I suggest you to be more clear in future questions. Write all the code that is needed immediately and remove widgets that are not needed. Avoid confusion given by naming different things in the same way.
The second widget does not update because it is listening to a different notifier.
When you do
return ChangeNotifierProvider.value(
value: SwitchAppProvider(),
in Widget2 you are creating a new provider object, you are not listening to changes in the provider you created in the Drawer.
You need to move the ChangeNotifierProvider.value widget higher in the widget tree, and use the same one:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
runApp(const MyApp());
}
class SwitchAppProvider extends ChangeNotifier {
switchApp(value) async {
// initialize instance of sharedpreference
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setBool('key', value);
notifyListeners();
}
Future<bool?> getValue() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final value = prefs.getBool('key');
return value;
}
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: ChangeNotifierProvider<SwitchAppProvider>(
create: (context) => SwitchAppProvider(),
child: Scaffold(
appBar: AppBar(),
drawer: _buildDrawer(),
body: const Widget2(),
),
),
);
}
Widget _buildDrawer() {
return Consumer<SwitchAppProvider>(builder: (context, provider, _) {
return SizedBox(
width: 260,
child: Drawer(
child: Material(
color: const Color.fromRGBO(62, 180, 137, 1),
child: ListView(
children: <Widget>[
Column(
children: [
const SizedBox(height: 10),
FutureBuilder(
future: provider.getValue(),
builder: (BuildContext context,
AsyncSnapshot<dynamic> snapshot) {
print('Am I building?');
if (snapshot.data == true) {
return ListTile(
tileColor: Colors.red[200],
title: const Text('widget1'),
leading: const Icon(Icons.flutter_dash),
onTap: () {
provider.switchApp(false);
},
);
} else {
return ListTile(
tileColor: Colors.green[200],
title: const Text('widget2'),
leading: const Icon(Icons.ac_unit),
onTap: () {
provider.switchApp(true);
},
);
}
},
),
],
),
],
),
),
),
);
});
}
}
class Widget2 extends StatelessWidget {
const Widget2({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<SwitchAppProvider>(
builder: (BuildContext context, SwitchAppProvider provider, _) {
return FutureBuilder(
future: provider.getValue(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
print('Am I building even more ?');
if (snapshot.data == true) {
return const Center(
child: CircularProgressIndicator(),
);
} else {
return const Text('provider.myValue');
}
},
);
},
);
}
}

Flutter - how to update screen with latest api response

I want to update the screen whenever I call the API. Right now I have the following
Future<String> getData() async {
var response = await http.get(
Uri.parse('https://www.api_endpoint.com'),
headers: {
'Accept':'application/json'
}
);
Timer.periodic(Duration(microseconds: 1000), (_) {
this.setState(() {
data = json.decode(response.body);
print(data); //I can see this in the console/logcat
});
});
}
#override
void initState() {
this.getData();
}
from the line above print(data); I can see the latest api responses in console/logcat but the screen doesn't update with the new values. I can't get my head around why the latest responses aren't shown on screen when this.setState() is called every second with the Timer... all feedback is welcome. Thanks
Future executes once and returns just one result. initState() executed when creating a widget, this is also usually once. For your tasks it is better to use Streams, my solution is not the best in terms of architecture, but as an example it works.
//We create a stream that will constantly read api data
Stream<String> remoteApi = (() async* {
const url = "http://jsonplaceholder.typicode.com/todos/1";
//Infinite loop is not good, but I have a simple example
while (true) {
try {
var response = await Dio().get(url);
if (response.statusCode == 200) {
//remote api data does not change, so i will add a timestamp
yield response.data.toString() +
DateTime.now().millisecondsSinceEpoch.toString();
}
//Pause of 1 second after each request
await Future.delayed(const Duration(seconds: 1));
} catch (e) {
print(e);
}
}
})();
//On the screen we are waiting for data and display it on the screen
// A new piece of data will refresh the screen
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: StreamBuilder<String>(
stream: remoteApi,
builder: (
BuildContext context,
AsyncSnapshot<String> snapshot,
) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.connectionState == ConnectionState.active ||
snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return const Text('Error');
} else if (snapshot.hasData) {
return Center(
child: Padding(
padding: const EdgeInsets.all(15.0),
child: Text(
snapshot.data.toString(),
textAlign: TextAlign.center,
),
),
);
} else {
return const Center(child: Text('Empty data'));
}
} else {
return Center(child: Text('State: ${snapshot.connectionState}'));
}
},
),
);
}
Or simplest solution
Future<String> remoteApi() async {
try {
const url = "http://jsonplaceholder.typicode.com/todos/1";
var response = await Dio().get(url);
if (response.statusCode == 200) {
return response.data.toString() +
DateTime.now().millisecondsSinceEpoch.toString();
} else {
throw ("Error happens");
}
} catch (e) {
throw ("Error happens");
}
}
var displayValue = "Empty data";
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(15.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(child: Text(displayValue)),
Center(
child: ElevatedButton.icon(
onPressed: () async {
displayValue = await remoteApi();
setState(() {});
},
label: const Text('Get API'),
icon: const Icon(Icons.download),
),
)
],
),
));
}
Ah, you don't actually call your API every timer tick, you just decode the same body from the first call.
If you want to call your API periodically, you need to move the actual http.get call inside the timer method.
Got it using the answer found here... moved the Timer that called this.setState() to the initState method
#override
void initState() {
this.getData();
_everySecond = Timer.periodic(Duration(seconds: 5), (Timer t) {
setState(() {
getData();
});
});
}
Once I searched for how to update the state, change state, etc. found the solution quickly...

flutter: how to show loading animation before log in?

I'm working with flutter. After I input my id and password, I want to show a log in animation before entering the home page. I use a dialog but I feel like my code is very blunt and has potential bugs. Is there a better solution?
// this part is for the input content is legal
else {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return LoadingStyle.buildWidget(); // this is a simple loading animation
});
service.createSession(context, code, id).then((response) { // log in part
if (response != null) {
this.saveUser(response); // something about saving user info
} else {
print('null respose');
}
}).catchError((e) {
if (e is GrpcError && e.code == StatusCode.unauthenticated) {
setState(() {
this.errorMessage = "grpc network error";
});
}
}).whenComplete(() {
Navigator.of(context).pop(); // pop dialog here, is this right?
MyRoutersl.goNewPage(context); // enter the new page
});
}
I suggest to use FutureBuilder. There is also some default loading Widget like CircularProgressIndicator() can be used when in progress.
Because login is some sort of Asynchronous progress, you can use FutureBuilder like below:
FutureBuilder(
future: service.createSession(... // Use Async function to return the result
builder: (context, snapshot) {
if(snapshot.hasData && snapshot.connectionState == done ){
// return widget after login successfully
// result should equal to snapshot.data
} else {
// return CircularProgressIndicator();
}
}
)
If you need more fancy loading indicator, you can check this package flutter_spinkit
You can use Libraries from pub.dev like loading_overlay
or you can build your own loading widget, example :
class OverlayWidget extends StatelessWidget {
final Widget child;
final bool isLoading;
OverlayWidget({#required this.child, this.isLoading = false})
: assert(child != null);
#override
Widget build(BuildContext context) {
return Stack(
children: [
child,
Visibility(
visible: isLoading,
child: Container(
color: Colors.grey.withOpacity(0.4),
child: Center(
child: Platform.isIOS
? CupertinoActivityIndicator(
radius: 20,
)
: CircularProgressIndicator(),
),
),
)
],
);
}
}
Please follow this (modal_progress_hud)
import 'package:modal_progress_hud/modal_progress_hud.dart';
......
bool _saving = false
....
#override
Widget build(BuildContext context) {
return Scaffold(
body: ModalProgressHUD(child: Container(
Form(...)
), inAsyncCall: _saving),
);
}

Future builder not firing

I can't seem to get my future builder to update. The api response is working fine I can see it in my logs. (model.getSuburbs). but it doesn't seem like my my future in the FutureBuilder suburbs is doing anything.. Am I missing something obvious (The onSubmitis trigger when I enter the last number and triggers the api)
class PostcodePage extends StatefulWidget {
static Route<dynamic> route() {
return MaterialPageRoute(
builder: (BuildContext context) => PostcodePage(),
);
}
#override
_PostcodeScreenState createState() => _PostcodeScreenState();
}
class _PostcodeScreenState extends State<PostcodePage> {
PostcodeViewmodel model = serviceLocator<PostcodeViewmodel>();
Future<List<Suburb>> suburbs;
String postCode;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
padding: EdgeInsets.all(32),
child: Column(children: [
SizedBox(height: 200),
PinEntryField(
onSubmit: (input) => getSub(pc: input),
),
FutureBuilder<List<Suburb>>(
future: suburbs,
builder: (context, snapshot) {
if (snapshot.connectionState==ConnectionState.active) {
return Text('Would Like something here...');
} else
return Text('But always end up here...');
},
),
// (postCode != null) Text(postCode),
SizedBox(
height: 300,
),
SizedBox(
width: double.maxFinite,
child: OnBoardingButton(
text: 'Begin',
onPressed: () {},
color: Color(0xff00E6B9),
),
),
]),
),
);
}
getSub({String pc}) {
setState(() {
suburbs = model.getSuburbs(country: 'au', postcode: pc);
});
}
}
Try to change your condition inside the builder.
This code snapshot.connectionState==ConnectionState.active could be really really short depending on the suburbs future.
Please try this inside the builder.
if (snapshot.hasData) {
return Text('Would Like something here...');
} else {
return Text('But always end up here...');
}

pull down to REFRESH in Flutter

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.