Why is futureBuilder stuck on the progress loader sometimes?

1,215

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.

Share:
1,215
Abeer Sul
Author by

Abeer Sul

Updated on December 18, 2022

Comments

  • Abeer Sul
    Abeer Sul over 1 year

    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 :/

    enter image description here

    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

    enter image description here

    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

    • Kahou
      Kahou about 4 years
      There is no PARSE SUCCESS1 in bad logs, so future state still loading.
    • Abeer Sul
      Abeer Sul about 4 years
      @Kahou, yes but what is the explanation?
  • Abeer Sul
    Abeer Sul about 4 years
    good suggestion. I tried adding try/catch and keeping compute => still same issue without any execptions. Then I tried removing compute, tested it many times and seems like the issue is gone, any clue why? I will test more before I mark this as correct. But I would like to understand why compute hangs without exceptions. And will this affect app's performance?
  • Gazihan Alankus
    Gazihan Alankus about 4 years
    Did you place try/catch inside parseEvents, around all lines of it? I never used compute, should read up about how to properly handle errors while using it.