Handling Network Calls and Exceptions in Flutter
Tags: flutter
Published: 2020-07-11 09:23:07

In this article I will show you how you can handle network calls and exceptions using `dio`, `flutter_bloc` and [`freezed`](pub.dev/packages/freezed) package

To start with this we need to add the dependencies in the `pubspec.yaml` file.

##Add the dependencies 

```yaml
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](https://pub.dev/packages/dio) for making network calls
- [flutter_bloc](https://pub.dev/packages/flutter_bloc) for UI and state management
- [freezed](pub.dev/packages/freezed) for generating Unions/Sealed classes
- [build_runner](https://pub.dev/packages/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

{% gist https://gist.github.com/ashishrawat2911/74416e528ae46bf25a81825c9a8c1c23 %}

We will be accessing this class using the repository 
```dart
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
```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

```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
```dart
  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


{% gist https://gist.github.com/ashishrawat2911/1dcfde5d713f389dcebbd9a69b863145 %}

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.
{% gist https://gist.github.com/ashishrawat2911/a5c3dd7fec38dce9f1f7e1a6d5684f13 %}

##Fetch the movie API in APIRepository class

```dart
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

```dart
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

```dart
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.

```dart
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

```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) 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.


```dart
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 

```dart
  @override
  void initState() {

    context.bloc<MovieBloc>().add(LoadMovies());

    super.initState();
  }
```

And we will listen the changes in the `BlocBuilder`.
```dart
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

```dart
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:

- When we get the result from the API
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/juhraucq3c87zre1227d.gif)

- When we get an error
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/2eun2vl2lw520puaiqzb.gif)


Check out the full code here

{% github ashishrawat2911/network_handling %}


------------------------------------------------------------- 


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 ](https://www.ashishrawat.dev)