In this article I will show you how you can handle network calls and exceptions using dio
, flutter_bloc
and freezed
package
To start with this we need to add the dependencies in the pubspec.yaml
file.
Add the dependencies
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.3
dio: 3.0.8
freezed: 0.10.9
flutter_bloc: 5.0.0
dev_dependencies:
flutter_test:
sdk: flutter
build_runner:
We are using
- dio for making network calls
- flutter_bloc for UI and state management
- freezed for generating Unions/Sealed classes
- build_runner for generating output files from input files
First of all, we will need to add the to set up the API call.
Create an API client
we are using the dio
package so we will create a DioClient
class
We will be accessing this class using the repository
import 'package:dio/dio.dart';
import 'package:network_handling/services/dio_client.dart';
class APIRepository {
DioClient dioClient;
String _baseUrl = "";
APIRepository() {
var dio = Dio();
dioClient = DioClient(_baseUrl, dio);
}
}
Now if you see when we hit an API we have 2 result
Api Call
--->> Success 🏆
--->> Failure ❌
Create an API Result handler
Create 2 freezed
classes to handle data,
- API Result
- Network Exceptions (In case API result returns a failure)
api_result.dart
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'network_exceptions.dart';
part 'api_result.freezed.dart';
@freezed
abstract class ApiResult<T> with _$ApiResult<T> {
const factory ApiResult.success({@required T data}) = Success<T>;
const factory ApiResult.failure({@required NetworkExceptions error}) =
Failure<T>;
}
network_exceptions.dart
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'network_exceptions.freezed.dart';
@freezed
abstract class NetworkExceptions with _$NetworkExceptions {
const factory NetworkExceptions.requestCancelled() = RequestCancelled;
const factory NetworkExceptions.unauthorisedRequest() = UnauthorisedRequest;
const factory NetworkExceptions.badRequest() = BadRequest;
const factory NetworkExceptions.notFound(String reason) = NotFound;
const factory NetworkExceptions.methodNotAllowed() = MethodNotAllowed;
const factory NetworkExceptions.notAcceptable() = NotAcceptable;
const factory NetworkExceptions.requestTimeout() = RequestTimeout;
const factory NetworkExceptions.sendTimeout() = SendTimeout;
const factory NetworkExceptions.conflict() = Conflict;
const factory NetworkExceptions.internalServerError() = InternalServerError;
const factory NetworkExceptions.notImplemented() = NotImplemented;
const factory NetworkExceptions.serviceUnavailable() = ServiceUnavailable;
const factory NetworkExceptions.noInternetConnection() = NoInternetConnection;
const factory NetworkExceptions.formatException() = FormatException;
const factory NetworkExceptions.unableToProcess() = UnableToProcess;
const factory NetworkExceptions.defaultError(String error) = DefaultError;
const factory NetworkExceptions.unexpectedError() = UnexpectedError;
}
If you see the ApiResult
class, in case of the success, I am returning the data of T
type but in the case of failure, I have to return a network exception.
How will I return network exception and determine which network exception has occurred?
Now you have seen the line part of'<file-name>.freezed.dart'
We will need to generate the freezer file
- Run this command in terminal ```
flutter packages pub run build_runner build
- Run this command in terminal to watch auto change
flutter packages pub run build_runner watch
- Run this command in terminal to watch auto change and delete previously generated files
flutter packages pub run build_runner watch --delete-conflicting-outputs
####Create a new method in NetworkExceptions class which will return NetworkExceptions
```dart
static NetworkExceptions getDioException(error) {
if (error is Exception) {
try {
NetworkExceptions networkExceptions;
if (error is DioError) {
switch (error.type) {
case DioErrorType.CANCEL:
networkExceptions = NetworkExceptions.requestCancelled();
break;
case DioErrorType.CONNECT_TIMEOUT:
networkExceptions = NetworkExceptions.requestTimeout();
break;
case DioErrorType.DEFAULT:
networkExceptions = NetworkExceptions.noInternetConnection();
break;
case DioErrorType.RECEIVE_TIMEOUT:
networkExceptions = NetworkExceptions.sendTimeout();
break;
case DioErrorType.RESPONSE:
switch (error.response.statusCode) {
case 400:
networkExceptions = NetworkExceptions.unauthorisedRequest();
break;
case 401:
networkExceptions = NetworkExceptions.unauthorisedRequest();
break;
case 403:
networkExceptions = NetworkExceptions.unauthorisedRequest();
break;
case 404:
networkExceptions = NetworkExceptions.notFound("Not found");
break;
case 409:
networkExceptions = NetworkExceptions.conflict();
break;
case 408:
networkExceptions = NetworkExceptions.requestTimeout();
break;
case 500:
networkExceptions = NetworkExceptions.internalServerError();
break;
case 503:
networkExceptions = NetworkExceptions.serviceUnavailable();
break;
default:
var responseCode = error.response.statusCode;
networkExceptions = NetworkExceptions.defaultError(
"Received invalid status code: $responseCode",
);
}
break;
case DioErrorType.SEND_TIMEOUT:
networkExceptions = NetworkExceptions.sendTimeout();
break;
}
} else if (error is SocketException) {
networkExceptions = NetworkExceptions.noInternetConnection();
} else {
networkExceptions = NetworkExceptions.unexpectedError();
}
return networkExceptions;
} on FormatException catch (e) {
// Helper.printError(e.toString());
return NetworkExceptions.formatException();
} catch (_) {
return NetworkExceptions.unexpectedError();
}
} else {
if (error.toString().contains("is not a subtype of")) {
return NetworkExceptions.unableToProcess();
} else {
return NetworkExceptions.unexpectedError();
}
}
}
This method will return the NetworkExceptions and we can use this to show different types of errors.
Also, sometimes we will need to show the error message on different errors
Let's create a new method for the error message
static String getErrorMessage(NetworkExceptions networkExceptions) {
var errorMessage = "";
networkExceptions.when(notImplemented: () {
errorMessage = "Not Implemented";
}, requestCancelled: () {
errorMessage = "Request Cancelled";
}, internalServerError: () {
errorMessage = "Internal Server Error";
}, notFound: (String reason) {
errorMessage = reason;
}, serviceUnavailable: () {
errorMessage = "Service unavailable";
}, methodNotAllowed: () {
errorMessage = "Method Allowed";
}, badRequest: () {
errorMessage = "Bad request";
}, unauthorisedRequest: () {
errorMessage = "Unauthorised request";
}, unexpectedError: () {
errorMessage = "Unexpected error occurred";
}, requestTimeout: () {
errorMessage = "Connection request timeout";
}, noInternetConnection: () {
errorMessage = "No internet connection";
}, conflict: () {
errorMessage = "Error due to a conflict";
}, sendTimeout: () {
errorMessage = "Send timeout in connection with API server";
}, unableToProcess: () {
errorMessage = "Unable to process the data";
}, defaultError: (String error) {
errorMessage = error;
}, formatException: () {
errorMessage = "Unexpected error occurred";
}, notAcceptable: () {
errorMessage = "Not acceptable";
});
return errorMessage;
}
Have a look at the full network exceptions class
Call the API in the APIRepository class
I am using the movieDB
API to get popular movies.
Create a PODO for parsing the response JSON from the API.
Fetch the movie API in APIRepository class
import 'package:dio/dio.dart';
import 'package:network_handling/model/movie_response.dart';
import 'package:network_handling/services/api_result.dart';
import 'package:network_handling/services/dio_client.dart';
import 'package:network_handling/services/network_exceptions.dart';
class APIRepository {
DioClient dioClient;
final String _apiKey = <apikey>;
String _baseUrl = "http://api.themoviedb.org/3/";
APIRepository() {
var dio = Dio();
dioClient = DioClient(_baseUrl, dio);
}
Future<ApiResult<List<Movie>>> fetchMovieList() async {
try {
final response = await dioClient
.get("movie/popular", queryParameters: {"api_key": _apiKey});
List<Movie> movieList = MovieResponse.fromJson(response).results;
return ApiResult.success(data: movieList);
} catch (e) {
return ApiResult.failure(error: NetworkExceptions.getDioException(e));
}
}
}
Now, this is just the API call, How to show it in the UI?
I am using flutter_bloc
to manage the data in the UI
Create a Bloc class
In the bloc
class, we will need two thing
- event (input)
- state (output)
Create Event class
Since we have only one event which is to call the API we are going to have only one event in the class
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'movie_event.freezed.dart';
@freezed
abstract class MovieEvent with _$MovieEvent {
const factory MovieEvent.loadMovie() = LoadMovies;
}
Create State class
Now we need to understand why this is important.
When we generally open any app we see some events are happening like loading, displaying data, showing error.
That's what we need to implement in our app , so all these things will be our result states
- Idle
- Loading
- Data
- Error
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:network_handling/services/network_exceptions.dart';
part 'result_state.freezed.dart';
@freezed
abstract class ResultState<T> with _$ResultState<T> {
const factory ResultState.idle() = Idle<T>;
const factory ResultState.loading() = Loading<T>;
const factory ResultState.data({@required T data}) = Data<T>;
const factory ResultState.error({@required NetworkExceptions error}) =
Error<T>;
}
Now generate these files
flutter packages pub run build_runner build
Now that we have created the event and states, Let's create Bloc class.
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:network_handling/api_repository.dart';
import 'package:network_handling/bloc/movie/movie_event.dart';
import 'package:network_handling/bloc/movie/result_state.dart';
import 'package:network_handling/model/movie_response.dart';
class MovieBloc extends Bloc<MovieEvent, ResultState<List<Movie>>> {
final APIRepository apiRepository;
MovieBloc({this.apiRepository})
: assert(apiRepository != null),
super(Idle());
@override
Stream<ResultState<List<Movie>>> mapEventToState(MovieEvent event) {
}
}
now we need to call the API here and sink the states into the blocs
class MovieBloc extends Bloc<MovieEvent, ResultState<List<Movie>>> {
final APIRepository apiRepository;
MovieBloc({this.apiRepository})
: assert(apiRepository != null),
super(Idle());
@override
Stream<ResultState<List<Movie>>> mapEventToState(MovieEvent event) async* {
yield ResultState.loading();
if (event is LoadMovies) {
ApiResult<List<Movie>> apiResult = await apiRepository.fetchMovieList();
yield* apiResult.when(success: (List<Movie> data) async* {
yield ResultState.data(data: data);
}, failure: (NetworkExceptions error) async* {
yield ResultState.error(error: error);
});
}
}
}
If you see we have implemented all the states
-
Idle
state in the beginning - and an event comes up and it starts to pass the
Loading
state - at last, it depends on the API call whether it returns the data or error.
Now we have created the Bloc
class and implemented the logic inside, Now we need to use this bloc class in the UI.
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<MovieBloc>(
create: (BuildContext context) {
return MovieBloc(apiRepository: APIRepository());
},
child: MyHomePage(),
)
],
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(),
),
);
}
}
Add the BlocProvider top of the widget tree so we can access the data.
Now in the HomePage
we will call the API
@override
void initState() {
context.bloc<MovieBloc>().add(LoadMovies());
super.initState();
}
And we will listen the changes in the BlocBuilder
.
BlocBuilder<MovieBloc, ResultState<List<Movie>>>(
builder: (BuildContext context, ResultState<List<Movie>> state) {
return state.when(
loading: () {
return Center(child: CircularProgressIndicator());
},
idle: () {
return Container();
},
data: (List<Movie> data) {
return dataWidget(data);
},
error: (NetworkExceptions error) {
return Text(NetworkExceptions.getErrorMessage(error));
},
);
},
)
Check out the Ui code
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
void initState() {
context.bloc<MovieBloc>().add(LoadMovies());
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Movies"),
),
body: BlocBuilder<MovieBloc, ResultState<List<Movie>>>(
builder: (BuildContext context, ResultState<List<Movie>> state) {
return state.when(
loading: () {
return Center(child: CircularProgressIndicator());
},
idle: () {
return Container();
},
data: (List<Movie> data) {
return dataWidget(data);
},
error: (NetworkExceptions error) {
return Text(NetworkExceptions.getErrorMessage(error));
},
);
},
),
);
}
Widget dataWidget(List<Movie> data) {
return ListView.builder(
itemCount: data.length,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 300,
width: 300,
child: Card(
elevation: 1,
child: Image.network(
"https://image.tmdb.org/t/p/w342${data[index].posterPath}",
),
),
);
},
);
}
}
Now let's see what we have built:
Check out the full code here
ashishrawat2911 / network_handling
Network and Exception handling in Flutter 💙
Network and Error Handling in FLutter
Network and error handling in flutter using Dio , flutter_bloc and freezed package
Screenshots
Thanks for reading this article ❤
If I got something wrong, Let me know in the comments. I would love to improve.
Let's connect: https://www.ashishrawat.dev
Top comments (5)
Hi Ashish,
This is kind of new of me as I never comment on articles but your article is so fantastically awesome that I'd like to request you to keep posting such articles more. This has made my life much more easier in terms of handling the states of my app. Eagerly waiting for new content. Cheers!!
Awesome post Ashish! 👋👍
One thing it lacks is
intro with benefits
orconclusion
section. As I am new to Flutter I would like to see how this approach is helpful and why I must have it in my project ⭐️Personally I like the idea of having 4 state on each screen and the approach kind of forcing me to create error state and loading states (those I always forget in my iOS projects).
Thanks again! Good work!
Excellent article, used your advice which can be an example for others on my open source system at GrowERP
Thanks Brother
great, but i found it not working correctly, data repeats, sometimes doesn't return, exception doesn't work properly