Flutter - Page Does Not Appear to Push Until FutureBuilder is Done

564

This is how I ended up fixing it. It doesn't make a whole lot of sense to me, but it worked. The page would never show while the StreamBuilder connection state was waiting. In my widget.bloc.getListItems(context) code, I was previously doing all the work then adding to the stream at the end.

PREVIOUS:

import 'dart:async';

import 'package:flutter/material.dart';

import 'bloc.dart';

class MyListBloc implements Bloc {
  List<Widget> listItems = <Widget>[];

  final _myListController = StreamController<List<Widget>>();

  Stream<List<Widget>> get myListStream =>
      _myListController.stream;

  MyListBloc();

  Future<void> getListItems(BuildContext context) async {
    var widgets = <Widget>[];
    
    ///Do all the async work which may take a while

    listItems = widgets;
    update();
  }



  void update() {
    this._myListController.sink.add(listItems);
  }

  @override
  void dispose() {
    // TODO: implement dispose
  }
}

So what I actually ended up doing was something more like this and it worked:

import 'dart:async';

import 'package:flutter/material.dart';

import 'bloc.dart';

class MyListBloc implements Bloc {
  List<Widget> listItems = <Widget>[];

  final _myListController = StreamController<List<Widget>>();

  Stream<List<Widget>> get myListStream =>
      _myListController.stream;

  MyListBloc();

  Future<void> getListItems(BuildContext context) async {
    var widgets = <Widget>[];

    ///Go ahead and set the content to be the indicator and add it to the stream now.
    widgets.add(Center(child:CircularProgressIndicator()));
    update();

    ///NOW set the content back to empty then Do all the async work which may take a while. 
    
    widgets = <Widget>[];

    ///await async stuff here (data retrieval...etc)

    ///Then set the listItems to the resulting content and add it to the stream.
    listItems = widgets;
    update();
  }



  void update() {
    this._myListController.sink.add(listItems);
  }

  @override
  void dispose() {
    // TODO: implement dispose
  }
}

Doing it this way I got the page to show with the circular progress indicator until the list building was complete. It then shows the list content.

Share:
564
mac
Author by

mac

Software/Web Developer

Updated on December 27, 2022

Comments

  • mac
    mac over 1 year

    I have a page that shows a list of items. This page is reached by button click on a previous page. I use a FutureBuilder to populate the list of items when the retrieval is complete. While the ConnectionState is "waiting," I show a circular progress indicator. However, the progress indicator never appears and the list page does not appear to push until the FutureBuilder completes. If I place a breakpoint in the "if (snapshot.connectionState == ConnectionState.waiting)" block, it hits that breakpoint, but the screen doesn't change on return. The screen does appear as expected, but as the list of items grows it takes longer and longer for the screen to appear. Am I missing something here:

    class MyListScreen extends StatelessWidget {
       RelevantObject relevantObject;
    
       MyListScreen(this.relevantObject, {Key key}) : super(key: key);
    
       @override
       Widget build(BuildContext context) {
       final bloc = MyListBloc(relevantObject);
    
       return FutureBuilder<Widget>(
         future: _buildList(bloc, context),
         builder: (BuildContext context, AsyncSnapshot<Widget> snapshot) {
           if (snapshot.connectionState == ConnectionState.waiting) {
             return Scaffold(
               appBar: AppBar(
                 centerTitle: false,
                 title: Text(
                   "My Title",
                 ),
               ),
               body: Center(child: CircularProgressIndicator()),
             );
           } else {
             return BlocProvider<MyListBloc>(
                 bloc: bloc, child: snapshot.data);
           }
         },
       );
     } 
    
     Future<Widget> _buildList(
         MyListBloc bloc, BuildContext myContext) async {
       //await bloc.init();
       List<Widget> listView = await bloc.getListItems(myContext);
       return StreamBuilder<List<MyListItem>>(
           stream: bloc.drawingCanvasListStream,
           builder: (context, snapshot) {
             return Scaffold(
               appBar: AppBar(
                 title: Text('My Title'),
                 centerTitle: false,
               ),
               body: SingleChildScrollView(
                 child: Column(mainAxisSize: MainAxisSize.min, children: listView),
               ),
             );
           });
     }
    }
    

    UPDATE:

    I altered my approach and tried to make it a bit simpler. Here is my updated "List Screen.":

    class MyListScreen extends StatefulWidget{
    
     final bloc = MyListBloc();
     @override
     _MyListState createState() => _MyListState();
    
    }
    
    class _MyListState extends State<MyListScreen>{
     @override
     void initState() {
       widget.bloc.getListItems(context);
       super.initState();
     }
    
     @override
     Widget build(BuildContext context) {
       // TODO: implement build
       return StreamBuilder<List<Widget>>(
           stream: widget.bloc.myListStream,
           builder: (context, snapshot) {
             return Scaffold(
               appBar: AppBar(
                 title: Text('My App'),
                 centerTitle: false,
               ),
               body:
               SingleChildScrollView(
                 child: Column(
                     mainAxisSize: MainAxisSize.min, children: snapshot.data == null ? [Center(child: CircularProgressIndicator())] :
              snapshot.data
              )
              ,
            ),
          );
    
        });
     }
    }
    

    The MyListBloc class has a Stream of List<Widget> that is initially an empty list. When the getListItems finishes, it adds those widgets to the stream. The MyListScreen still is not showing up until that async getListItems has finished. Hopes this new code makes it a little easier to point out my misunderstanding.

    UPDATE 2:

    I tried this as well to no avail:

    import 'package:flutter/material.dart';
    
    class MyListScreen extends StatefulWidget{
    
     final bloc = MyListBloc();
     @override
     _MyListState createState() => _MyListState();
    
    }
    
    class _MyListState extends State<MyListScreen>{
     @override
     void initState() {
       widget.bloc.getListItems(context);
       super.initState();
     }
    
     @override
     Widget build(BuildContext context) {
       return Scaffold(
         appBar: AppBar(
           title: Text('My App'),
           centerTitle: false,
         ),
         body: StreamBuilder<List<Widget>>(
             stream: widget.bloc.myListStream,
             builder: (context, snapshot) {
               if(snapshot.connectionState == ConnectionState.waiting){
                 return Center(child: CircularProgressIndicator());
               }
               else {
                 return SingleChildScrollView(
                   child: Column(
                       mainAxisSize: MainAxisSize.min, children: snapshot.data
                   )
                   ,
                 );
               }
             })
       );
     }
    }
    

    widget.bloc.getListItems(context) is an async method that I do not await. Yet, the screen still does not appear until snapshot.data != null.

    • aqwert
      aqwert about 3 years
      Probably because it cannot tell the difference between the 2 scaffolds. Put a ValueKey on those and see if that helps. Ideally though that you don't build widgets inside the future. Just have the bloc.drawingCanvasListStream o the future of the FutureBuilder and render all the widgets in the builder
  • mac
    mac about 3 years
    The current page still remains in view until the _buildList is completed. It does not push this list page and show the CircularProgressIndicator
  • Doc
    Doc about 3 years
    replace the return BlocProvider part under else with Text("done"); does this show up after loading indicator? If yes then there is some issue with BlocProvider widget you are using. If not then there is some issue with your future not finishing correctly.
  • mac
    mac about 3 years
    thanks for looking at this. See my updated code to see if it makes anymore sense.
  • Doc
    Doc about 3 years
    you still won't show anything unless there is some data from the stream. Change the code to have StreamBuilder draw the body and not the whole screen. This way the AppBar and other stuff will still show without the stream data. Check my code for outline.
  • mac
    mac about 3 years
    Apologies, but that still isn't working as expected. See UPDATE 2. widget.bloc.getListItems(context) is an async method that I do not await. However, the page still does not show up until snapshot.connectionState != waiting. I place a breakpoint on both returns inside of StreamBuilder. I see it hit the first return while connectionState == ConnectionState.waiting. I continue and the screen does not appear. When it hits the other return and continues when snapshot.connectionState != ConnectionState.waiting.....the screen finally appears.
  • mac
    mac about 3 years
    I have done that. However, the previous page is still in view until it returns the SingleChildScrollView. The CircularProgressIndicator never shows. Tried leaving it as null as well. The breakpoint will hit if(snapshot.data == null){ return CircularProgressInidcator();}, but the page still doesn't show up until after widget.bloc.getListItems returns
  • mac
    mac about 3 years
    The only two connection states that it ever has is ConnectionState.waiting and ConnectionState.active (set breakpoint in first line of builder and those are the only two I get).
  • mac
    mac about 3 years
    Running this on an iPad pro 11" if that means anything. Could this be a threading issue maybe?
  • Stewie Griffin
    Stewie Griffin about 3 years
    That's odd. It's hard to say something without seeing what you have in the previous screen and also your bloc class. I added alternative approach to your situation. Can you try it and then let me know if it works?