How to manage blocs thrown exceptions with StreamBuilder? - flutter

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.

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();
}
}

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.

Related

Signal R how properly take data from events?

My problem is: I subscribed to events in signalR, but I don’t understand how to correctly take the data from this answer and put it in UI. The documentation shows the same method as in my code, but an empty list is returned to me in the user interface. In my case i get the data at the moment when the event comes, until i get the data from the event the list is empty and i thought to capture this data somehow, because i have to show it to the user. But the data from the event is not coming to my UI
But there is data in the console. Here they are - [{warpedBox: [604.3993, 290.7302, 1106.364, 290.7302, 1106.364, 530.2628, 604.3993, 530.2628], name: Cats, date: 2022-09-05T09:01:11.9003992+03:00, additionInfo: new animal detected, baseName: TestBase, imageGuid: 00000000-0000-0000-0000-000000000000}] . How to get data from an event?
Many thanks to Robert Sandberg
I made some changes and now my code is like that (also I added UI part, because I don't understand how to make it work)
My code is now:
typedef CallbackFunc = void Function(List<dynamic>? arguments);
class Animals {
Alarmplayer alarmplayer = Alarmplayer();
Future<void> fetchAnimals(CallbackFunc arguments) async {
final httpConnectionOptions = HttpConnectionOptions(
accessTokenFactory: () => SharedPreferenceService().loginWithToken(),
skipNegotiation: true,
transport: HttpTransportType.WebSockets);
final hubConnection = HubConnectionBuilder()
.withUrl(
'secure_link',
options: httpConnectionOptions,
)
.build();
await hubConnection.start();
hubConnection.on('Animals', (arguments);
alarmplayer.Alarm(url: 'assets/wanted.mp3', volume: 0.01);
await Future.delayed(const Duration(seconds: 2))
.then((value) => alarmplayer.StopAlarm());
});
}
return agruments
}
My UI-part:
class _TestScreenState extends State<TestScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: FutureBuilder<void>(
///can't understand how to pass here arguments
future: fetchAnimals(),
builder: (context, snapshot) {
return ListView.builder(
itemCount: snapshot.data?.length ?? 0,
itemBuilder: (context, index) {
return Column(children: [
Text(snapshot.data?[index]['name']),
]);
},
);
}),
I am sorry but I am really noob in that and can't understand how can I use it in the UI
If I understand you correctly, then you mean that you get an empty list where you do the print(detectedAnimals ); ? With this setup, your method will basically always return an empty list.
And that is because your method have already executed that print line (and the return statement) when you get the event over SignalR.
The callback you send into the hubConnection must have a way to report back to the UI, meaning it have to have a way to communicate back to the method that is calling fetchAnimals() WHEN the callback is executed.
So I'd inject the callback into fetchAnimals() as such:
typedef CallbackFunc = void Function(List<dynamic>? arguments);
Future<void> fetchAnimals(CallbackFunc callback) async {
....
hubConnection.on('Animals', callback);
...
}

How should I fix 'deactivated widget's ancestor is unsafe.' in Flutter?

I know that the actual Context has changed. But I've already read another solutions but is still very confuse.
In my case:
I'm working with Provider Consumer. When the app is on LoginScreen, it goes to LoadingScreen and then return asynchronous to a function inside Login pointing a context that no more exist. And that is the error, but I don't know the best way to fix it.
Main class:
return Consumer<AuthNotifier>(builder: (context, notifier, _) {
switch (notifier.getStatus()) {
case AuthStatus.authenticated:
return Home();
case AuthStatus.unauthenticated:
return Login();
case AuthStatus.authenticating:
return LoadingScreen();
default:
return LoadingScreen();
}
});
Login Class:
This function async is called
void _loginFacebook(context) async {
final faceResult = await FacebookAuth.instance.login();
Provider.of<AuthNotifier>(context, listen: false)
.setStatus(AuthStatus.authenticating);
_requestLogin(context, "facebook", faceResult.accessToken.token);
}
Then: _requestLogin calls _initNewUser:
Future<void> _initNewUser(context, prefix, token) async {
var facebookUser = await FacebookAuth.i.getUserData(fields: "name, email");
//>>>>> ERROR OCCURS HERE >>>>>>>>>>>>>>>>>
Provider.of<UserInfoNotifier>(context, listen: false)
.initNewUser(facebookUser["email"], facebookUser["name"]);
}
But in this case, where should I set a global variable? I'm very confuse which is the best solution. The context are being changed by a Notifier. I don't know how to proceed.

Flutter, "The method 'map' can't be unconditionally invoked because the receiver can be 'null'."

I am getting this error when I am trying to call the .map method on the snapshot.data. I am using an SQLite database.
The code throwing the error is the following:
FutureBuilder(
future: _alarms,
builder: (context, snapshot)
{
if(snapshot.hasData){
return ListView(
children: snapshot.data.map<Widget>((alarms) {
return Container(
And I am creating the _alarms list in the initState method:
#override
void initState() {
_alarmHelper.initializeDatabase().then((value) => print('------------dB initialized'));
_alarms = _alarmHelper.getAlarm();
super.initState();
}
And the .getAlarm(), is defined so:
Future<List<AlarmInfo>> getAlarm() async{
var result;
var db = await this.database;
result = await db?.query(tableName);
result.forEach((element) {
var alarmInfo = AlarmInfo.fromMap(element);
alarms.add(alarmInfo);
}
);
return alarms;
}
I have also tried adding a ?. operator, but then this returns another error which is the .map is not defined for the type object children: snapshot.data?.map<Widget>((alarms) {
Any help is appreciated and if you require any further information feel free to leave a comment.
Thanks :)
I assume it's because you didn't provide a type for the FutureBuilder widget, therefore snapshot.data is from type Object (instead of a list you are expecting there) and the map function does not exist for that.
It should be fixed by writing it like this:
FutureBuilder<List<AlarmInfo>>(
...
),
Additionally since data might be null (but you checked it with snapshot.hasData you have to write:
snapshot.data!.map(...)

Dart make synchronous api call

I'm learning flutter and I want to call an api which returns json. Because HttpClient methods are async and I don't want to deal with Future to build my material app, I've used the sync.http library but when I test the following code :
var result = Map();
try {
final apiUri = Uri.parse('https://api.quotable.io/random');
var request = SyncHttpClient.getUrl(apiUri);
var response = request.close();
var contents = response.toString();
result = jsonDecode(contents);
}
on Exception {
result['content'] = 'Could not contact host';
result['author'] = 'None';
}
return result;
I get the error :
Unhandled exception:
Unsupported operation: unsupported http response format
I think this means that the json format isn't supported by the library but I find this weird. Do you know how I can call my api call synchronous ?
I'm not sure about the SyncHttp client from the docs it looks like an internal client used by flutter or the flutter team. I could be wrong but either way its not a good choice for a UI to have sync http requests.
Flutter provides a FutureBuilder Widget which will allow you to use async methods in the build method.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: FutureBuilder(
future: fetchData(), // Your API Call here
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(''); // Your UI here
} else if (snapshot.hasError) {
return Text('Error');
} else {
return Text('Loading');
}
}
),
);
}
For more info and a performance tips visit
Flutter Widget of the Week - FutureBuilder
FutureBuilder Docs

snapshot.ConnectionState is always waiting in the FutureBuilder using provider package

I am using cloud firestore as a backend and the provider package for state management for my app. I have a BooksProvider class (ChangeNotifierProvider) which I use to fetch, add, update and delete data. In this class, I have an async method to fetch data from cloud firestore.
Future<void> fetchAndSetData() async {
try {
final response = await bookCollection.getDocuments();
List<Book> loadedBooks = [];
final querySnapshot = response.documents;
if (querySnapshot.isEmpty) {
return;
}
querySnapshot.forEach((book) {
loadedBooks.add(
Book(
id: book.documentID,
title: book.data['title'],
description: book.data['description'],
image: book.data['image'],
rate: book.data['rate'],
category: CategoryName.values[book.data['category_index']],
date: book.data['date'],
price: book.data['price'],
isFree: book.data['isFree'],
isFavorite: book.data['isFavorite']),
);
});
_booksList = loadedBooks;
notifyListeners();
} catch (e) {
throw e;
}
}
I am trying to use it with the future builder but it doesn't work as it is always stuck in ConnectionState.waiting case.
class BooksScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
// final booksData = Provider.of<BooksProvider>(context);
// final books = booksData.booksList;
// final _noContent = Center(child: Text('No books are available.'));
return FutureBuilder(
future: Provider.of<BooksProvider>(context).fetchAndSetData(),
builder: (ctx, dataSnapshot) {
switch (dataSnapshot.connectionState) {
case ConnectionState.waiting:
case ConnectionState.active:
return Text('waiting'); //always returning waiting
break;
case ConnectionState.done:
return Text('done');
break;
case ConnectionState.none:
return Text('none');
break;
default:
return Text('default');
}
},
);
}
}
I don't know what might be wrong, I tried to change the return type of the async method so that it returns a list of Book but it didn't work. I also used if statement instead of the switch case and the same thing kept happening.
In addition, I tried it with if (dataSnapshot.hasdata) as it is suggested by on of the answers, and it worked!, but i want to use a spinner while waiting and when i tried it, the same problem occurred again.
I tried the same method with another approach where i used stateful widget and didChangeDependencies() method to fetch the data and it worked just fine, and I am trying future builder approach because I don't know how to handle errors with didChangeDependencies() approach.
so if you please can help me with the future builder or error handling with the latter approach. Thank you in advance.
Use dataSnapshot.hasData instead of dataSnapshot.connectionState