3

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.

mac
  • 485
  • 1
  • 6
  • 29
  • 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 – aqwert Feb 23 '21 at 00:28

5 Answers5

0

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');
                }
Shriya Pandya
  • 404
  • 3
  • 12
  • 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 Mar 12 '21 at 14:54
0

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,
        ),
      );
    }
  }
}
Stewie Griffin
  • 4,690
  • 23
  • 42
  • 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 Mar 12 '21 at 14:52
  • Running this on an iPad pro 11" if that means anything. Could this be a threading issue maybe? – mac Mar 12 '21 at 14:59
  • 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? – Stewie Griffin Mar 12 '21 at 15:45
0

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.

yellowgray
  • 4,006
  • 6
  • 28
0

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.

mac
  • 485
  • 1
  • 6
  • 29
-1
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();
     }
   ),
);
Doc
  • 10,831
  • 3
  • 39
  • 63
  • The current page still remains in view until the _buildList is completed. It does not push this list page and show the CircularProgressIndicator – mac Mar 02 '21 at 15:12
  • 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. – Doc Mar 02 '21 at 16:30
  • thanks for looking at this. See my updated code to see if it makes anymore sense. – mac Mar 10 '21 at 15:48
  • 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. – Doc Mar 11 '21 at 03:06
  • 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 Mar 11 '21 at 15:04