DEV Community

loading...
Cover image for Handling Network Calls and Exceptions in Flutter

Handling Network Calls and Exceptions in Flutter

ashishrawat2911 profile image Ashish Rawat ・7 min read

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

  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:

  • When we get the result from the API
    Alt Text

  • When we get an error
    Alt Text

Check out the full code here

GitHub logo 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

Discussion (3)

pic
Editor guide
Collapse
mithileshparmar profile image
Mithilesh Parmar

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!!

Collapse
mcsims profile image
Maksims Puskels

Awesome post Ashish! 👋👍

One thing it lacks is intro with benefits or conclusion 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!

Collapse
devnhq profile image
Nguyễn Hồng Quang

great, but i found it not working correctly, data repeats, sometimes doesn't return, exception doesn't work properly