AYT Technologies

We develop game-changer web and mobile software and applications by using cutting edge technologies.

Follow publication

Flutter | Manage API Errors Globally with Custom ErrorDialogManager

Samed Harman
AYT Technologies
Published in
3 min read5 days ago

User experience is crucial in mobile applications. One of the key factors that directly impact the app experience is how errors are handled. There is no such thing as error-free code — errors will always occur. Even if they don’t originate from the client, they may come from the server. In such cases, our primary goal is to provide the user with the right feedback.

In this article, you will learn how to manage network errors from your service in a centralized way within your Flutter applications. Let’s get started.

First, let’s create our dialog manager class, which will handle error display.

class InterceptorErrorDialogManager {
const InterceptorErrorDialogManager._();

static final StreamController<AppException> _errorStreamController =
StreamController<AppException>.broadcast();

static Stream<AppException> get errorStream => _errorStreamController.stream;

static void addError(AppException exception) {
_errorStreamController.add(exception);
}

static void show({
required ErrorDialogData data,
BuildContext? context,
VoidCallback? onDismiss,
}) {
showDialog<void>(
context: context ??
routerConfig.configuration.navigatorKey.currentState!.context,
builder: (context) => AlertDialog(
title: Text(data.title),
actionsAlignment: MainAxisAlignment.center,
content: Text(data.body),
icon: const Icon(Icons.error),
actions: [
ElevatedButton(
onPressed: data.onAction,
child: Text(data.actionTitle ?? 'Ok'),
),
],
),
).then((_) {
onDismiss?.call();
});
}
}

class ErrorDialogData {
ErrorDialogData({
required this.title,
required this.body,
this.onAction,
this.actionTitle,
});

final String title;
final String body;
final String? actionTitle;
final FutureOr<void> Function()? onAction;
}

In this class, an important point to note is that the stream structure inside it accepts an AppException base class. You can define this class as follows.

abstract class AppException implements Exception {}

Instead of using the AppException class, you could directly use the Exception class. However, in our projects, we usually avoid using a class, data type, or package exactly as it is. Adding an extra layer can be beneficial for future business logic requirements. Let this exception be specific to our app and stay there for future use.

Now, let’s define our interceptor class. This class will handle errors specific to the responses from our APIs. For example, we can create a class to handle errors coming from our authentication service like this:

class AuthServiceInterceptor extends InterceptorsWrapper {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
if (err.response?.statusCode == 401) {
InterceptorErrorDialogManager.addError(
SpecialException(code: 401, path: 'https://example.com'),
);
}
super.onError(err, handler);
}
}

From this code, we can see that the SpecialException class is derived from our AppException class. However, checking only the status code of the incoming error might not be sufficient. After all, Dio does not return a 401 status code for just a single request.

Since this is written for testing purposes, we assume we already know the error code, which is why we handle it this way. In a real project, you might also want to check err.requestOptions.path to verify whether the error occurred for the specific request path you were expecting.

Don’t forget to add AuthServiceInterceptor as an interceptor when creating the Dio instance.

Alright, we added our error to the stream using addError, but how do we listen to it across the entire application? There's a perfect place for this—right on MaterialApp.

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp.router(
debugShowCheckedModeBanner: false,
routerConfig: routerConfig,
builder: (context, child) {
AppInitializer.handleAppErrors(); // Here is good. :)
return child!;
},
);
}
}
class AppInitializers{
static void handleAppErrors() {
InterceptorErrorDialogManager.errorStream.listen((error) {
if(error is SpecialException){
// showDialog
}
});
}
}

You can add any exceptions you want inside the if block and display them accordingly. If this section gets too cluttered, you can move the logic to a separate ErrorManager class. But for now, this should work fine.

We used the InterceptorErrorDialogManager class to handle errors coming from Dio interceptors, but don't think it's limited to just that. You can call show() anywhere in your application to display a dialog.

I hope this was helpful — or at least gave you some new ideas. Thanks for reading! Feel free to ask me any questions. Have a great Sunday 🎈.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

AYT Technologies
AYT Technologies

Published in AYT Technologies

We develop game-changer web and mobile software and applications by using cutting edge technologies.

Samed Harman
Samed Harman

Written by Samed Harman

Flutter/Android Mobile App Developer | Computer Engineer

No responses yet

Write a response