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

Viewed 626

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.

5 Answers

Try this out to get proper state of your data.

  switch(snapshot.connectionState){
                  case ConnectionState.none:
                    return Text('none');
                  case ConnectionState.active:
                  case ConnectionState.waiting:
                    return Text('Active and may be waiting');
                  case ConnectionState.done:
                    return Text('Done');
                  default:
                    return Text('Default');
                }

You stated that the initial value of your stream is an empty list. Therefore, StreamBuilder will display the empty list right away, so you can't see CircularProgressIndicator.

Try the code below, please:

@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.hasData || snapshot.data.length == 0) {
          return Center(
            child: CircularProgressIndicator(),
          );
        } else {
          return SingleChildScrollView(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: snapshot.data,
            ),
          );
        }
      },
    ),
  );
}

Don't forget that this code will show the CircularProgressIndicator even when there is no item added to the list after your future method completed. If you don't want this to happen, leave the initial value of your stream as null and create a new list when you call getListItems. So, you can just check the data for null like !snapshot.hasData to show the loading state.

Alternative Approach

There is another way to display data from a stream besides StreamBuilder. You can listen to the stream inside initState and change the state of your screen when there is data.

class MyListScreen extends StatefulWidget {
  MyListScreen();

  @override
  _MyListScreenState createState() => _MyListScreenState();
}

class _MyListScreenState extends State<MyListScreen> {
  final bloc = MyListBloc();
  List<Widget> myList;

  @override
  void initState() {
    super.initState();

    bloc.getListItems(context).whenComplete(() {
      bloc.myListStream.listen((List<Widget> listData) {
        setState(() {
          myList = listData;
        });
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('My App'),
        centerTitle: false,
      ),
      body: _buildBody(),
    );
  }

  Widget _buildBody() {
    if (myList == null) {
      return Center(
        child: CircularProgressIndicator(),
      );
    } else {
      return SingleChildScrollView(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: myList,
        ),
      );
    }
  }
}

I think the problem should be inside your myListStream. Please share what it doing inside if this is not working.

getListItems() is a Future function (data is null or a list of items)

myListStreamis a Stream function

If you want to concatenate the Future result to Stream, you should return a Stream immediately and yield the updated data if the data is changed.

DO NOT USE

StreamBuilder<List<Widget>>(
  stream: myListStream(),  // myListStream will be null until the data is ready
  builder:
  ...

Future getListItems() async {
  List<Widget> listView = await bloc.getListItems(myContext);
  myListStream = Stream(...)
}

USE

StreamBuilder<List<Widget>>(
  stream: myListStream(), // myListStream is a Stream but the data is null
  builder:
  ...

Stream<List<Widget>> myListStream() async* {
  List<Widget> listView = await bloc.getListItems(myContext);
  yield listView;
}
  

For the best use of Stream, my suggestion is to implement something like getListItemsStream() that can be used directly by StreamBuilder. In your case, I think you can just use FutureBuilder to achieve the same result.

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.

return Scaffold(
   appBar:AppBar(),
   body: FutureBuilder<Widget>(
      future:() => _buildList(bloc,context),// correct way to pass args to function
      builder:(context,snapshot){
          if(snapshot.connectionState == ConnectionState.waiting)
              return  Center(child: CircularProgressIndicator());

          else 
               return FlutterLogo();
     }
   ),
);
Related