Hello, Welcome back. Glad that you’re here.
We're building a Movie App with the best coding practices and tools. Till now in UI, we've made HomeScreen, Navigation Drawer, and About Dialog.
We have till now focused on happy scenarios like making API calls that will never fail. The app will always have a happy path and no failure is a myth. So, let's handle the errors when making API calls.
DARTZ Recap
If you remember, in the Repositories & UseCase tutorial, we have used Dartz Plugin. We have used only one feature of this plugin - Either. The underlying concept is very simple -
Return Left when an error, Right when success. Left and Right are object/data holders.
If you are coming to this tutorial directly, please visit that tutorial) first and come back.
Although, I recommend that you watch the full series and all previous tutorials in this series.
In the repository layer, we used Either<Left, Right>
for all the methods. When making a service call in the data source, if the service call is a success, we returned the response in the Right
object. In case of any exceptions, we returned an AppError
in the Left
Object.
Type of Errors
Errors can be of many types. It can be service not responding, can be a parsing error. Sometimes, when you try to internet access and the internet is off, you will get an exception. For all those scenarios, we will return with the Left
object.
There can be many errors that the TMDb API throws like -
- 501 - Invalid service: this service does not exist
- 401 - Authentication failed: You do not have permissions to access the service
- 400 - Validation failed
- 500 - Internal error: Something went wrong, contact TMDb.
- 503 - The API is undergoing maintenance. Try again later.
These are just a few of the API status codes. You can get all of them from here - Status Codes
We will consider these errors as one type of error in the application and hence will give only one generic error message whenever there is any type of exception thrown by the API.
We will also handle the most popular type of error i.e. network not connected error. What do you do when you make an API call and there is no network.
There are possibly two direct solutions. First, you can stop making API calls when you know there is no internet. Second, you make the call anyway and upon SocketException
you can tell the user about no internet.
To implement the first option, you need to deal with platform-specific APIs, that will observe network changes. You will also need to make UX in such a way that the user is not allowed to make any API call when there is no internet.
No matter what option you choose, you'll have to implement the second way in your app. Because imagine you make 5 subsequent calls and mid-way the user disables the network connection. So, you'll have to handle no network error anyway and intimate the user about the same.
Here, in the Movie App as well, I will show you to implement the second approach only.
App Error
If you remember in the 3rd tutorial [Repositories & UseCase], we created a class AppError
that holds a strong message. Since our requirements have changed, we need to slightly update this to now accept an enum
instead of a dumb String
message.
Update the app_error.dart:
class AppError extends Equatable {
//2
final AppErrorType errorType;
const AppError(this.errorType);
@override
List<Object> get props => [errorType];
}
//1
enum AppErrorType { api, network }
- Create an
enum
with two values.api
is for API exceptions andnetwork
is for no internet. - Instead of
String
useAppErrorType
now.
When you have updated the AppError
class, you should get errors in the MovieRepositoryImpl
. So, let's start updating these methods.
//1
on SocketException {
return Left(AppError(AppErrorType.network));
//2
} on Exception {
return Left(AppError(AppErrorType.api));
}
- Maintain the order. First, handle
SocketException
. You'll get this exception when the network is not connected and you make an API call. You'll returnAppError
with thenetwork
type. - Next, you'll handle generic
Exception
that will capture any other exception thanSocketException
. We are considering that any other exception thrown when making an API call will be from API only. There can be one more exception, which can be when you're parsing JSON. But, I believe that is a logical programming mistake that can be handled while developing. So, we are still handling that exception under theapi
type.
Update all the remaining methods in the repository.
Error Widget
When there is an error, you need to show a text message and 2 buttons as I showcased before. We already have BlocBuilder
in place in the HomeScreen
. We'll handle the extra state MovieCarouselError
and return CarouselLoadErrorWidget
.
Open
HomeScreen
and addelse if
:
else if (state is MovieCarouselError) {
return CarouselLoadErrorWidget(
//1
bloc: movieCarouselBloc,
//2
errorType: state.errorType,
);
}
- Bloc you have already defined in the
HomeScreen
. AppErrorType
will come from the state. We will change the state in just a moment.
In the home/movie_carousel folder, create a new file carousel_load_error_widget.dart:
import 'package:flutter/material.dart';
class CarouselLoadErrorWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container();
}
}
- Create a basic Stateless widget. And update the import in
HomeScreen
.
Turn off the network connection and restart the application. You'll see that we cannot see the carousel widget as the error widget is there now.
Let's build this widget now.
//1
final AppErrorType errorType;
//2
final MovieCarouselBloc bloc;
const CarouselLoadErrorWidget({
Key key,
@required this.errorType,
@required this.bloc,
}) : super(key: key);
- The error text depends on
AppErrorType
so, declare a final field. - When you press the retry button, you'll load the trending movies again, so declare
MovieCarouselBloc
to dispatch theCarouselLoadEvent
later.
Come back to the HomeScreen
and see there is an error. We need to add the required parameters in CarouselLoadErrorWidget
. You'll easily get an instance of the bloc, but to get AppErrorType
in the state, you need to pass it.
Update
MovieCarouselError
state:
class MovieCarouselError extends MovieCarouselState {
//1
final AppErrorType errorType;
const MovieCarouselError(this.errorType);
}
- Declare the field for
AppErrorType
and introduce the constructor.
Next, update the Bloc to return the
errorType
:
//1
(l) => MovieCarouselError(l.appErrorType),
- Fetch the
appErrorType
from theLeft
that we returned from the repository methods. ThisappErrorType
will be different when there is anetwork
error orapi
error.
Before proceeding further, let's add the translation strings for error messages and the retry button.
Update the en.json, es.json and translation_constants.dart:
{
"retry": "Retry",
"somethingWentWrong": "Something went wrong...",
"checkNetwork": "Please check your network connection and press Retry button or put in as a bug by pressing Feedback button.",
}
"retry": "Rever",
"somethingWentWrong": "Algo salió mal...",
"checkNetwork": "Verifique su conexión de red y presione el botón Reintentar o colóquelo como error presionando el botón Comentarios.",
static const String retry = 'retry';
static const String somethingWentWrong = 'somethingWentWrong';
static const String checkNetwork = 'checkNetwork';
After adding translations, you can create the UI in CarouselLoadErrorWidget
:
//7
Padding(
padding: EdgeInsets.symmetric(horizontal: Sizes.dimen_32.w),
//1
child: Column(
//6
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
//2
Text(
errorType == AppErrorType.api
? TranslationConstants.somethingWentWrong.t(context)
: TranslationConstants.checkNetwork.t(context),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.subtitle1,
),
//3
ButtonBar(
children: [
//4
Button(
onPressed: () => bloc.add(CarouselLoadEvent()),
text: TranslationConstants.retry,
),
//5
Button(
onPressed: () => Wiredash.of(context).show(),
text: TranslationConstants.feedback,
),
],
),
],
),
);
Use
Column
to layout elements vertically.For error messages, use the
Text
widget. Based onerrorType
you'll decide the string. Make all the text-align in the center and give itsubtitle1
style.We will layout the 2 buttons in the horizontal direction, so use
ButtonBar
.Use the
Button
that we created in previous tutorials. On tap of the Retry button, we will make another call to fetch the trending movies.Second button will open the Wiredash bottom sheet that we worked on in the last tutorial.
To bring the content in the center, we will use
MainAxisSize.max
andMainAxisAlignment.center
in theColumn
.Give some horizontal padding to avoid the content touching the screen.
Now, restart the app. To see the api
error message, you can change the API_KEY in the ApiConstants
class and restart the app again. You can play with different scenarios now.
Generic Error Widget
How do you handle errors in calling the APIs that are called when we switch tabs? There can be certain scenarios when the carousel has loaded successfully but individuals tabs haven't. We'll apply the same logic here as well.
If we use the existing error widget, we will need to make it so generic that it handles the operation with the tap of the Retry button. You can do that by directly passing the onPressed method in the constructor of CarouselLoadErrorWidget
.
So, let's update the
CarouselLoadErrorWidget
first to make it more generic.
//1
final Function onPressed;
const CarouselLoadErrorWidget({
Key key,
@required this.errorType,
@required this.onPressed,
}) : super(key: key);
//2
Button(
onPressed: onPressed,
text: TranslationConstants.retry,
),
- Add the
onPressed
as the final field. - Use this in the
Button
now.
In the HomeScreen
, instead of passing the bloc itself, pass the functionality you want to perform on the Retry button tap.
//3
CarouselLoadErrorWidget(
onPressed: () => movieCarouselBloc.add(
CarouselLoadEvent(),
),
errorType: state.errorType,
)
Run the app again by turning off the internet first and then turn on the internet and press the Retry button. You'll see Retry working fine.
You can rename this widget and move it to the common widgets folder as it has now become generic to be used anywhere in the application.
Rename it to AppErrorWidget
.
Error Handling in Tabs
Nothing much changes in terms of how the functionality will work. When you switch between tabs there is an API call made, so we will show AppErrorWidget
if the API throws an error or the network is off.
In movie_tabbed_widget.dart, handle
MovieTabLoadError
:
//1
if (state is MovieTabLoadError)
AppErrorWidget(
//2
errorType: state.errorType,
//3
onPressed: () => movieTabbedBloc.add(
MovieTabChangedEvent(
//4
currentTabIndex: currentTabIndex,
),
),
),
- Handle the
MovieTabLoadError
state below theMovieTabChanged
state. - Use the
AppErrorWidget
and pass theerrorType
. We haven't updated theMovieTabLoadError
witherrorType
, will do it just after this. - Now, in
onPressed()
hit theMovieTabChangedEvent
that will make the API call again. - You would have to call the
MovieTabChangedEvent
with the same tab index so that the movies are fetched for the tab selected.
Now, update the
MovieTabLoadError
:
class MovieTabLoadError extends MovieTabbedState {
//1
final AppErrorType errorType;
const MovieTabLoadError({
int currentTabIndex,
//2
@required this.errorType,
}) : super(currentTabIndex: currentTabIndex);
}
- Add the
AppErrorType
as a final field. - Add this field to the constructor as a
required
field.
Now, you need to update the
MovieTabbedBloc
:
//1
(l) => MovieTabLoadError(
currentTabIndex: event.currentTabIndex,
errorType: l.appErrorType,
),
- Go to the place where you're yielding the state and update the
left
side by addingerrorType
inMovieTabLoadError
.
How do you generate a scenario when trying to run this?
You can comment out the current yielding statement and yield the error state all the time. Just to test it out. Let's do it and run it.
//1
yield MovieTabLoadError(
currentTabIndex: event.currentTabIndex,
errorType: AppErrorType.network,
);
- Comment out the existing statement and add the above statement.
Now, run the app and you'll see the error message when changing tabs.
The error message should be in middle, so go to
MovieTabbedWidget
and addExpanded
widget:
Expanded(
child: AppErrorWidget(
errorType: state.errorType,
onPressed: () => movieTabbedBloc.add(
MovieTabChangedEvent(
currentTabIndex: state.currentTabIndex,
),
),
),
)
Reload and change the tab.
You can play with it in various scenarios. Uncomment the commented code and reload. Tap on the Retry button, the movies will be loaded now.
Last Error Scenario?
The scenario that I will now bring is not an error but considered as handling edge cases while coding.
What will happen if the movies you are fetching are not there and you get an empty list of movies. How the UI will work. Apply the below code in MovieTabbedBloc
and run the application.
yield moviesEither.fold(
(l) => MovieTabLoadError(
currentTabIndex: event.currentTabIndex,
errorType: l.appErrorType,
),
(movies) {
return MovieTabChanged(
currentTabIndex: event.currentTabIndex,
//1
movies: [],
);
},
);
- Just don't pass in the movies fetched, instead pass the empty list.
If you run, you'll see no movies being shown in the movies list view.
We will show Sorry, no movies under this section when there are no movies present.
Update the en.json, es.json and translation_constants.dart:
"noMovies": "Sorry, no movies under this section"
"noMovies": "Lo siento, no hay películas en esta sección."
static const String noMovies = 'noMovies';
Now, update the
MovieTabbedWidget
:
//1
state.movies?.isEmpty ?? true
//2
? Expanded(
child: Center(
child: Text(
TranslationConstants.noMovies.t(context),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.subtitle1,
),
),
)
: Expanded(
child: MovieListViewBuilder(movies: state.movies),
),
- In the
MovieTabChanged
if block, add the condition where if movies are null or empty, you show the No Movies text. We are using the?.
operator because apart from the empty movies list, the list can be null as well. Ifmovies
is null, you'll return true, which will show the No Movies text. - If
movies
is empty or null, return anExpanded
Center
widget that will show the No Movies text. Use thet()
extension that will translate the message in different languages.
This is how we will handle errors in future tutorials as well. I believe this tutorial has given much insight into error handling and edge case handling.
See you in the next part. n