I have an issue with futureBuilders in my whole flutter app. In each screen there can be 3 or 4 sections, each one loads data from a specific API endpoint. The problem is that sometimes, whole screen loads fine, and sometimes a section or two are stuck with the progress bar!.. I debugged and searched a lot and can't find the real cause. This is making me hate flutter :/
For example, here is the Home Page Code:
import 'package:carousel_pro/carousel_pro.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:soul_space/PODOs/categories.dart';
import 'package:soul_space/PODOs/event.dart';
import 'package:soul_space/PODOs/image.dart';
import 'package:soul_space/PODOs/space.dart';
import 'package:soul_space/custom_widgets/category-card.dart';
import 'package:soul_space/custom_widgets/error-card.dart';
import 'package:soul_space/custom_widgets/no-data.dart';
import 'package:soul_space/custom_widgets/search-widget.dart';
import 'package:soul_space/services/categories-service.dart';
import 'package:soul_space/services/events-service.dart';
import 'package:flutter/foundation.dart';
import 'package:soul_space/services/spaces-service.dart';
import '../custom_widgets/home-row.dart';
import 'package:async/async.dart';
class HomeTab extends StatefulWidget {
HomeTab({Key key, this.title, this.mainScaffoldKey}) : super(key: key);
final GlobalKey<ScaffoldState> mainScaffoldKey;
final String title;
@override
HomeTabState createState() => HomeTabState();
}
class HomeTabState extends State<HomeTab> with AutomaticKeepAliveClientMixin {
Future _homeImagesFuture;
final AsyncMemoizer _categoriesMemoizer = AsyncMemoizer();
Future _categoriesFuture;
final AsyncMemoizer _eventsMemoizer = AsyncMemoizer();
Future _eventsFuture;
final AsyncMemoizer _spacesMemoizer = AsyncMemoizer();
Future _spacesFuture;
var isLoading = false;
final List<String> rowsTitles = ['New Events', 'Featured Spaces'];
List colors = [
Colors.red[500],
Colors.teal[300],
Colors.yellow[500],
Colors.orange[300],
Colors.red[400],
Colors.blue
];
Random random = new Random();
@override
void initState() {
super.initState();
_categoriesFuture = fetchCategories();
_eventsFuture = fetchEvents();
_spacesFuture = fetchSpaces();
_homeImagesFuture = getHomeImages();
}
fetchCategories() async {
return this._categoriesMemoizer.runOnce(() async {
return await getCategories();
});
}
fetchEvents() async {
return this._eventsMemoizer.runOnce(() async {
return await fetchAllEvents('approved');
});
}
fetchSpaces() async {
return this._spacesMemoizer.runOnce(() async {
return await fetchAllSpaces('approved');
});
}
String _homeSliderImages;
Future<List<String>> getHomeImages() async {
List<LoadedAsset> assets = await fetchSliderImages();
List<String> temp = [];
for (var i = 0; i < assets.length; i++) {
temp.add(assets[i].link);
}
return temp;
}
@override
Widget build(BuildContext context) {
super.build(context);
debugPrint('REBUILD:' + DateTime.now().toString());
return Scaffold(
body: SingleChildScrollView(
child: Column(
children: <Widget>[
Stack(
children: [
SearchBar(scaffoldKey: widget.mainScaffoldKey),
],
),
Padding(
padding: const EdgeInsets.only(left: 10.0, right: 10.0),
child: FutureBuilder(
future: _eventsFuture,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
debugPrint('***************************DONE1');
if (snapshot.hasError) {
debugPrint(snapshot.error.toString());
return ErrorCard(
isSmall: true,
);
} else if (snapshot.hasData) {
final List<Event> events = snapshot.data;
return HomeRow(
title: rowsTitles[0],
events: events,
route: '/allEvents');
} else {
return NoDataCard(
isSmall: true,
);
}
} else {
debugPrint('***************************LOADER1');
return Container(
margin: EdgeInsets.symmetric(vertical: 0.0),
height: 250.0,
child: Center(child: RefreshProgressIndicator()));
}
}),
),
Padding(
padding: const EdgeInsets.only(left: 10.0, right: 10.0),
child: FutureBuilder(
future: _spacesFuture,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
debugPrint('***************************DONE2');
if (snapshot.hasError) {
debugPrint(snapshot.error.toString());
return ErrorCard(
isSmall: true,
);
} else if (snapshot.hasData) {
final List<Space> spaces = snapshot.data;
return HomeRow(
title: rowsTitles[1],
spaces: spaces,
route: '/allSpaces');
} else {
return NoDataCard(
isSmall: true,
);
}
} else {
debugPrint('***************************LOADER2');
return Container(
margin: EdgeInsets.symmetric(vertical: 0.0),
height: 250.0,
child: Center(child: RefreshProgressIndicator()));
}
}),
)
],
),
));
}
@override
bool get wantKeepAlive => true;
}
And this is a sample of a service function that parses json data from API:
Future<List<Event>> fetchAllEvents(String approval, {int id = -1}) async {
var url = globals.apiUrl +
'/events/' +
(id != -1 ? id.toString() + '/' : '') +
_getQueryParams(false, approval);
dio.Response<String> response = await get(url);
debugPrint('***************************GET SUCCEES1');
var temp = await compute(parseEvents, response.data);
debugPrint('***************************PARSE SUCCEES1');
return temp;
}
.
.
List<Event> parseEvents(String responseBody) {
final parsedJson = json.decode(responseBody);
var list = parsedJson['data'] as List;
if (list != null) {
List<Event> eventsList = list.map((i) => Event.fromJson(i)).toList();
return eventsList;
} else {
return [];
}
}
Furthermore, here are examples of good and bad build logs, hope they help conclude the cause:
- 'LOADER' is printed when progress bar is displayed
- 'GET SUCCESS' is printed when get returns
- 'PARSE SUCCESS' is printed when compute function succeeds in parsing json
- 'DONE' is printed when the widget is loaded correctly
Any solutions?
EDIT
I removed "compute" function as suggested in the accepted answer and it fixed it, but not sure why it failed sometimes without exceptions
Execution is not advancing beyond this line:
var temp = await compute(parseEvents, response.data);
Probably parseEvents
is throwing something.
Notice how you never use try/catch
. You have to write code to catch exceptions. parseEvents
is probably throwing an exception (malformed json?, empty json->null?) and since you are not catching it you have no idea what is happening.
I would first try to get rid of the compute
and just call parseEvents
to see if the exception is silently ignored because of compute
. Then I would place try/catch
inside the parseEvents function and gradually have all execution paths to have try/catch
in them, including async
functions and await
calls.