Why are child widget StreamBuilders not receiving errors?

326

There was an issue about this in the RxDart GitHub (https://github.com/ReactiveX/rxdart/issues/227). BehaviorSubjects were not replaying errors to new listeners.

It was fixed in version 0.21.0. "Breaking Change: BehaviorSubject will now emit an Error, if the last event was also an Error. Before, when an Error occurred before a listen, the subscriber would not be notified of that Error."

Share:
326
rkunboxed
Author by

rkunboxed

Updated on December 10, 2022

Comments

  • rkunboxed
    rkunboxed over 1 year

    I am struggling with RxDart (maybe just straight up Rx programming). I currently have a stateful widget that calls my bloc in it's didChangeDependencies(). That call goes out and gets data via http request and adds it to a stream. I'm using BehaviorSubject and this works fine. I have child widgets using StreamBuilders and they get data no problem. My issue comes in dealing with errors. If my http request fails, I hydrate the stream with addError('whatever error') but my child widget's StreamBuilder is not receiving that error. It doesn't get anything at all.

    So I have a few questions.

    1. Is that expected?
    2. Should error handling not be done in StreamBuilder? Ideally, I want to show something in the UI if something goes wrong so not sure how else to do it.
    3. I could make my child widget stateful and use stream.listen. I do receive the errors there but it seems like overkill to have that and the StreamBuilder.
    4. Am I missing something fundamental here about streams and error handling?

    Here is my bloc:

    final _plans = BehaviorSubject<List<PlanModel>>();
    Observable<List<PlanModel>> get plans => _plans.stream;
    
    fetchPlans() async {
        try {
            final _plans = await _planRepository.getPlans();
            _plans.add(_plans);
        }
        on AuthenticationException {
            _plans.addError('authentication error');
        }
        on SocketException {
            _plans.addError('no network connection');
        }
        catch(error) {
            _plans.addError('fetch unsuccessful');
        }
    }
    

    Simplified Parent Widget:

    class PlanPage extends StatefulWidget {
      @override
      PlanPageState createState() {
        return new PlanPageState();
      }
    }
    
    class PlanPageState extends State<PlanPage> {
    
      @override
      void didChangeDependencies() async {
        super.didChangeDependencies();
        var planBloc = BaseProvider.of<PlanBloc>(context);
        planBloc.fetchPlans();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar( title: const Text('Your Plan') ),
            body: PlanWrapper()
        );
      }
    }
    

    Simplified Child Widget with StreamBuilder:

    class PlanWrapper extends StatelessWidget {
    
      @override
      Widget build(BuildContext context) {
        var planBloc = BaseProvider.of<PlanBloc>(context);
    
        return StreamBuilder(
          stream: planBloc.plans,
          builder: (BuildContext context, AsyncSnapshot<List<PlanModel>> plans) {
            if (plans.hasError) {
              //ERROR NEVER COMES IN HERE
              switch(plans.error) {
                case 'authentication error':
                  return RyAuthErrorCard();
                case 'no network connection':
                  return RyNetworkErrorCard();
                default: 
                  return RyGenericErrorCard(GeneralException().errorMessages()['message']);
              }
            }
            if (plans.hasData && plans.data.isNotEmpty) {
              return ListView(
                physics: const AlwaysScrollableScrollPhysics(),
                children: _buildPlanTiles(context, plans.data)
              );
            }
            return Center(child: const CircularProgressIndicator());
          }
        );
      }
    }