Complete Movie App — Error Handling (10)
Welcome back, Glad that you are 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 App Dialog.
We have till now focused on happy scenarios like making API calls 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.
This is a companion article for the Video:
DARTZ Recap
If you remember, in the [Repositories & UseCase] video, we have used the Dartz Plugin. We have used only one feature of this plugin — Either. The underlying concept is very simple -
Return Left when error, Right when success. Left and Right are object/data holders.
If you are coming to this tutorial directly, please visit that video first and comeback.
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 Right
object. In case of any exceptions, we returned a AppError
in Left
Object.
Type of Errors
Errors can be of many types. It can be “service not responding”, which 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 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 users 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 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 string
message. Since our requirements have changed, we need to slightly update this to now accept an enum
instead of 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 network is not connected and you make API call. You'll returnAppError
withnetwork
type. - Next, you’ll handle generic
Exception
that will capture any other exception thanSocketException
. We are considering that any other exception thrown when making 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 the logical programming mistake that can be handled while developing. So, we are still handling that exception underapi
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 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 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 instance of the bloc, but to get AppErrorType
in state, you need to pass it.
Update
MovieCarouselError
state:
class MovieCarouselError extends MovieCarouselState {
//1
final AppErrorType errorType;
const MovieCarouselError(this.errorType);
}
- Declare 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 isnetwork
error orapi
error.
Before proceeding further, let’s add the translation strings for error messages and 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 message, use
Text
widget. Based onerrorType
you'll decide the string. Make all the text align in centre and give itsubtitle1
style. - We will layout the 2 buttons in horizontal direction, so use
ButtonBar
. - Use the
Button
that we created in previous videos. On tap of 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 video.
- To bring the content in 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 carousel has loaded successfully but individuals tabs haven’t. We’ll apply 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 on tap of 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 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 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 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 error or 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 with respect to 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
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 with 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 is null or empty, you show the No Movies text. We are using?.
operator, because apart from the empty movies list, the list can be null as well. Ifmovies
is null, you'll returntrue
, that 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 error in future videos as well. I believe this tutorial has given much insight about error handling and edge case handling.
If you loved reading the article, don’t forget to clap 👏. You can reach out to me and follow me on Medium, Twitter, GitHub, YouTube.