How to manage blocs thrown exceptions with StreamBuilder?
Asked Answered
D

2

5

I'm trying to return a snapshot error state to my StreamBuilder when my provider has problems during the http.get() call. In my case I throw an exception when the the http.get() return a state different from 200 (OK). I would like to be able to return a bad state to snapshot and execute the specific code for this situation. Now when I throw the exception the application simply crash.

Provider:

class FmsApiProvider {
  Future<List<FmsListResponse>> fetchFmsList() async {
    print("Starting fetch FMS..");
    final Response response = await httpGet('fms');
    if (response.statusCode == HttpStatus.ok) {
      // If the call to the server was successful, parse the JSON
      return fmsListResponseFromJson(response.body);
    } else {
      // If that call was not successful, throw an error.
      //return Future.error(List<FmsListResponse>());
      throw Exception('Failed to load FMSs');
    }
  }
}

Repository:

class Repository {
  final fmsApiProvider = FmsApiProvider();

  Future<List<FmsListResponse>> fetchAllFms() => fmsApiProvider.fetchFmsList();
}

Bloc:

class FmsBloc {
  final _fmsRepository = Repository();

  final _fmsFetcher = PublishSubject<List<FmsListResponse>>();

  Observable<List<FmsListResponse>> get allFms => _fmsFetcher.stream;

  fetchAllFms() async {
    List<FmsListResponse> itemModel = await _fmsRepository.fetchAllFms();
    _fmsFetcher.sink.add(itemModel);
  }

  dispose() {
    _fmsFetcher.close();
  }
}

My StreamBuilder:

StreamBuilder(
            stream: bloc.allFms,
            builder: (context, AsyncSnapshot<List<FmsListResponse>> snapshot) {
              if (snapshot.hasData) {
                return RefreshIndicator(
                    onRefresh: () async {
                      bloc.fetchAllFms();
                    },
                    color: globals.fcsBlue,
                    child: ScrollConfiguration(
                      behavior: NoOverScrollBehavior(),
                      child: ListView.builder(
                          shrinkWrap: true,
                          itemCount:
                              snapshot.data != null ? snapshot.data.length : 0,
                          itemBuilder: (BuildContext context, int index) {
                            final fms = snapshot.data[index];
                            //Fill a global list that contains the FMS for this instances
                            globals.currentFMSs.add(
                                FMSBasicInfo(id: fms.id, code: fms.fmsCode));
                            return MyCard(
                              title: _titleContainer(fms.fmsData),
                              fmsId: fms.id,
                              wmId: fms.fmsData.workMachinesList.first
                                  .id, //pass the firs element only for compose the image url
                              imageType: globals.ImageTypeEnum.iteCellLayout,
                              scaleFactor: 4,
                              onPressed: () => _onPressed(fms),
                            );
                          }),
                    ));
              } else if (snapshot.hasError) {
                return Text('Fms snapshot error!');
              }
              return FCSLoader();
            })

When the exception is thrown I would like to obtain a snapshot error and then visualize only a text in my page.

Dael answered 20/6, 2019 at 12:28 Comment(0)
N
8

You should wrap the api call in a try catch and then add the error to your sink.

class FmsBloc {
  final _fmsRepository = Repository();

  final _fmsFetcher = PublishSubject<List<FmsListResponse>>();

  Observable<List<FmsListResponse>> get allFms => _fmsFetcher.stream;

  fetchAllFms() async {
    try {
      List<FmsListResponse> itemModel = await _fmsRepository.fetchAllFms();
      _fmsFetcher.sink.add(itemModel);
    } catch (e) {
      _fmsFetcher.sink.addError(e);
    }
  }

  dispose() {
    _fmsFetcher.close();
  }
}
Nestorius answered 20/6, 2019 at 12:38 Comment(2)
Yes, this is correct. Then in your StreamBuilder use snapshot.hasError to specify a displayed widget for an error case.Laurellaurella
I am doing the exact same thing but my StreamBuilder is not getting called. Any idea what I might be doing wrong? Note that calling sink.add() does invoke StreamBuilder but sink.addError() does not invoke it.Chronon
J
0

The answer marked as correct did not work for me. Doing some debug I saw that the problem is entering in the catch/throw: you actually never go there, even if you see the Exception in the debug console.

To me, in Debug the application doesn't crash but will have a breakpoint on the Exception and you can continue playing it with the Play button. With the Run button instead you have the same behaviour without the breakpoint (like a real user).

This is the flow of my BLoC implementation: http call -> provider -> repository -> bloc -> ui.

I was trying to handle the case of missing internet connection without checking for it and handling a general error case.

My evidence has been that a throw Exception ('Error'); in the provider does not propagate to the right of the flow. I tried also with other approaches like the try/catch, and applied them at different levels in the code.

Basically what I needed to achieve is to call fetcher.sink.addError('Error'); but from inside the provider, where the error occurs. Then checking snapshot.hasError in the UI will return true, and the error could easily handled.

This is the only (ugly) thing that worked to me: to give the sink object as input in the calls down to the provider itself, and in the onCatchError() function of the http call add the error to the sink through its function.

I hope it could be useful to someone. I know it is not the best practice actually but I just needed a quick-and-dirty solution. If anyone has a better solution/explanation, I will read the comment with pleasure.

Jaguar answered 22/10, 2020 at 9:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.