Issues handling Navigation when exception occurs [Bloc] - flutter

I'm creating a flutter app using bloc/cubit pattern. I'm having issues preventing navigation when an exception occurs, here's my button onTap function:
function: () async {
if (state.termsAndConditionsAccepted &&
state.status.isValid &&
!state.status.isPure) {
await context
.read<SignUpCubit>()
.onClickSignUp()
.then(((value) => {Navigator.pop(context)}));
}
and here's the logic of sign in
Future<void> onClickSignUp() async {
try {
// sign in logic...
// Provoking an exception...
throw Exception('An error ocrred. Please try again.');
} on Exception catch (exception) {
String exMessage = exception.toString();
log(exMessage);
// This emits a new state with the error and shows an error snackbar based on this new state
emit(state.copyWith(
status: FormzStatus.submissionFailure,
exceptionErrorMessage: exMessage));
}
As far as I know, the .then() callback function will only be executed if the Future function completes successfully(onClickSignUp()) but the Navigation occurs even if the exception is thrown.
I managed make it work by returning a boolean value to the then() callback and do the navigation depending in that value(if true navigate/if false do nothing), but that would require me to return true when function completes successfully and return false in all my catch blocks which does not seems a good practice.
Since I'm handling the states from my cubit class I cannot use .onError or .catchError() callbacks on the button's function.
Hopefully someone can suggest a better error handling in this case or what could be an appropriate error handling for bloc pattern

Related

How to unit test Flutter's StreamSubscription onData?

This is what I tried so far.
Let's say result is a StreamSubscription.
This is my flutter file
try {
result.listen(
(event) async {
// This converts the JSON data
final news = NewsModel.fromJSON(jsonDecode(event));
// This saves the data to local database
await localDataSource.saveNews([news]);
},
onError: (e) {
debugPrint('$e');
},
);
} catch (e) {
debugPrint('$e');
}
this is my flutter test since I want to test if method localDataSource.saveNews() fails
await newsRepository.subscribe(); calls the try catch above
controller is a StreamController to add new data to the stream
news is a dummy data, it doesn't matter because whatever the localDataSource do it will throw a LocalDBException
also I am using Mockito https://pub.dev/packages/mockito to mock the localDataSource above
test(
'should handle fail save news method',
() async {
// arrange
when(mockLocalDataSource.saveNews(any)).thenThrow(LocalDBException());
// act
await newsRepository.subscribe();
controller.add(news)
// assert
},
);
As you can see I don't have any condition to pass the flutter test, but that's beyond the point as this flutter test already breaks the stream even if I have a onError on my listener.
if I use controller.addError(LocalDBException()) the onError works, but if I deliberately throw an exception from the method localDataSource.saveNews() it breaks the stream.
Given this context I want to know 2 things:
I want to know how to handle the error inside the onData of StreamSubscription, if a method / function throw an exception, as it ignores the onError if a method / function throw an exception inside the listener.
Is adding an error through addError() function the same as throwing an exception inside the stream?

Flutter: Consider canceling any active work during "dispose" when internet changes its state

I am getting the following message when internet goes off.
E/flutter (26162): [ERROR:flutter/lib/ui/ui_dart_state.cc(186)] Unhandled Exception: This widget has been unmounted, so the State no longer has a context (and should be considered defunct).
E/flutter (26162): Consider canceling any active work during "dispose" or using the "mounted" getter to determine if the State is still active.
It is showing the message from this section of my code.
#override
void initState() {
super.initState();
try {
InternetAddress.lookup('google.com').then((result) {
if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) {
// internet conn available
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) =>
(Constants.prefsMobile.getString("mobile") == null
? Login()
// : SignupPayoutPassword(signupdata: [])),
: Home(signindata: signinData)),
));
} else {
// no conn
_showdialog();
}
}).catchError((error) {
// no conn
_showdialog();
});
} on SocketException catch (_) {
// no internet
_showdialog();
}
Connectivity()
.onConnectivityChanged
.listen((ConnectivityResult connresult) {
if (connresult == ConnectivityResult.none) {
} else if (previous == ConnectivityResult.none) {
// internet conn
Navigator.of(context).pop();
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) =>
(Constants.prefsMobile.getString("mobile") == null
? Login()
: Home(signindata: signinData)),
));
}
previous = connresult;
});
}
I have not used any dispose method for this. If any one know please let me know how can I solve this problem. How to dispose. I am getting a crash report after my app close as follows
E/AndroidRuntime( 8064): java.lang.RuntimeException: Unable to destroy activity {com.example.aa_store/com.example.aa_store.MainActivity}: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter activity
is this crash message for the above problem? Please help.
Please use.
#override
void dispose() {
Connectivity().onConnectivityChanged.cancel();
super.dispose();
}
Better, define your stream outside the initState:
Stream _connectivityStream = Connectivity().onConnectivityChanged;
and in dispose use _connectivityStream.cancel();.
The error means that you instantiated a stream, which on changes of events, triggers build changes. This stream is setup during initState, meaning when the widget is first created. Connectivity().onConnectivityChanged.listen(....etc).
But you never tell flutter to cancel listening to this stream when the widget is disposed.
This is the role of the dispose method. Similar to how you want logic to be performed when the widget is built, you use initState, you should also tell it when you are no longer interested in these changes in logic.
Failing to do so, will result in the error you are having, aside from memory leaks also.
This is the translation of the error This widget has been unmounted, so the State no longer has a context (and should be considered defunct). which you posted. "Hey, this widget isn't in the tree anymore, its state is not mounted, I can't rebuild it, and you need to pay attention to it.
Please consider using the dispose method for these Flutter elements, not to mention all of them, but from the top of my mind:
AnimationControllers.
Timers.
Streams listeners.

Flutter firebase onBackgroundMessage throws exception

When i add this function to my code.
FirebaseMessaging.onBackgroundMessage(
(message) => _handleBG(message, currentUserId));
I get exception
[ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: NoSuchMethodError: The method 'toRawHandle' was called on null.
onMessage method below is working fine.
FirebaseMessaging.onMessage.listen((event) async {
// event.messageType
print('hearing to foreground messsage');
if (event.data['messageType'] == 'chat') {
await ChatHelper.handleMessageReceived(event, context, currentUserId);
//setState(() {});
}
});
FirebaseMessaging.onBackgroundMessage(
(message) => _handleBG(message, currentUserId));
The background message handler should be declared as a top-level function so that it can be called independently, as you never know the state of your app and its object when the notification is received. It looks like that your _handleBG method is a private method available to a specific class only.
From code documentation
/// This provided handler must be a top-level function and cannot be
/// anonymous otherwise an [ArgumentError] will be thrown.

How to catch specified exception, display warning and logout effectively?

Web application contains many screens with list views and edit dialogs for edit selected records. List view uses "standard display behavour" using StreamBuilder.
Simplified bloc.dart
class Bloc {
final _subject = StreamController<Data>.broadcast();
Stream<Data> get data => _subject.asyncMap(_getData);
Future<Data> _getData(...) {
try {
final data = await api.getUrl(...);
return data;
} on SomeException catch (e) {
// This exception is catched by StreamBuilder
throw ApiException('could not get data');
}
}
}
Simplified widget.dart
StreamBuilder<...>(
stream: bloc.data,
builder: (context, snapshot) {
if (snapshot.hasError) {
// Bloc throws an exception which thrown by API client (HttpClient).
return ErrorWidget(snapshot.error.toString());
}
if (snapshot.hasData) {
return ListView.builder(...);
} else {
return CircularProgressIndicator();
}
}
),
Each edit dialog uses showDialog to display selected item details and save modifications.
There may be a situation when (for example) access token is invalid (or expired) during his already authenticated session. It is necessary to inform user about this (showing him a warning) and after closing warning destroy the session and return to login screen. Is it possible to implement effective solution to catch specific exception (eg. TokenInvalidException) in a single place without adding symbolic code like:
if (snapshot.error is TokenInvalidException) {
// destroy session
// return to Login
}
into each StreamBuilder list view and some code to check if exception occurred on each Dialog's save action?
I have 100500 screens and I do not want to implement the same code for each screen. Actually I want some global exception catcher which catches the required exception and executes specific action.
Now I see some global StreamController with stream which accepts such exception (events) and is listen by some global parent widget (MaterialApp or maybe Main).
final globalExceptionController = StreamController<TokenInvalidException>.broadcast();
// somewhere
globalExceptionController.stream.listen((e) async {
// Show warning
final result = await showDialog(...);
// Go to login
Navigator.pushReplacement(context, <login page route>);
}) {}
The problem I see here is the bloc object which throws the exception will have strong dependency on stream controller which is not interested to it absolutely. But actually I need to link somehow the bloc and GUI because to show warning and switch the route I need a context.
Any ideas or critics are appreciated.
Maybe the coordinator bloc pattern described here helps you with this problem

How to wait for a method that is already being executed?

I'm developing a Flutter app which has some tabs inside, each of them depend on the database that is loaded on the first run. State is stored in a ScopedModel.
On every tab I have this code:
#override
void initState() {
super.initState();
loadData();
}
void loadData() async {
await MyModel.of(context).scopedLoadData();
_onCall = MyModel.of(context).onCall;
setState(() {
});
}
And this is the code snippet that matters for the ScopedModel:
Future<Null> scopedLoadData() async {
if (_isLoading) return;
_isLoading = true;
(...)
_isLoading = false;
}
If the user waits on the first tab for a few seconds everything is fine, since Database is loaded. However, if the user switches tabs right after app launch, the method scopedLoadData is still being executed so that I get a runtime error ("Unhandled Exception: NoSuchMethodError: The method 'ancestorWidgetOfExactType' was called on null.").
This exception happens because the scopedLoadData has not yet been completed. So I'm looking for a way to wait for a method that is still being executed.
Thanks!
Not sure without seeing your build method but I would start your build method with a guard clause.
if (_oncall == null) return Container(); // or Text("loading") or similar
use should be using a FutureBuilder on each tab to make sure the data is loaded before you try to build the widget... more code would be helpful
I solved the exception by getting rid of every:
setState(() { });
and implementing ScopedModelDescendant on every relevant tab on top of using notifyListeners() at the end of the database loading method.
This pulls the responsibility from the tabs for updating views and gives it to the scopedLoadData().