Connecting Flutter to a Backend with Dio: Models, Errors and Clean Structure

Every real app talks to an API. This guide shows how to make requests with Dio, turn responses into Dart models, handle errors properly and keep network code in a clean layer.

3 min read1 views
Connecting Flutter to a Backend with Dio: Models, Errors and Clean Structure

Almost every real app talks to a server. Flutter can use the http package, but Dio adds interceptors, request cancellation and upload progress, and is more common in serious projects.

#Setup

Create one shared Dio instance and give the base settings once:

final dio = Dio(BaseOptions(
  baseUrl: 'https://api.example.com/api/v1',
  connectTimeout: const Duration(seconds: 10),
  receiveTimeout: const Duration(seconds: 20),
  headers: {'Accept': 'application/json'},
));

#Models

Convert the JSON response into a Dart class so the rest of your code works with typed objects, not raw Maps:

class Course {
  final String id;
  final String name;
  final int price;
  const Course({required this.id, required this.name, required this.price});

  factory Course.fromJson(Map<String, dynamic> json) => Course(
        id: json['id'] as String,
        name: json['name'] as String,
        price: json['price'] as int,
      );
}

#The repository layer

Widgets should not call Dio directly. Create an intermediary class:

class CourseRepository {
  CourseRepository(this._dio);
  final Dio _dio;

  Future<List<Course>> fetchCourses() async {
    final response = await _dio.get('/courses');
    return (response.data as List)
        .map((e) => Course.fromJson(e as Map<String, dynamic>))
        .toList();
  }
}

If the API address or format changes one day, only this class changes.

#Error handling

Network errors come in several kinds: no connection, a timeout, an error response from the server. Turn them all into one understandable message:

String friendlyMessage(Object error) {
  if (error is DioException) {
    switch (error.type) {
      case DioExceptionType.connectionTimeout:
      case DioExceptionType.receiveTimeout:
        return 'The server did not respond. Please try again.';
      case DioExceptionType.connectionError:
        return 'Check your internet connection.';
      default:
        final data = error.response?.data;
        if (data is Map && data['message'] is String) return data['message'];
    }
  }
  return 'Something went wrong.';
}

#Interceptors for repetitive work

Anything that must happen on every request (such as adding an auth header or logging) belongs in an interceptor:

dio.interceptors.add(InterceptorsWrapper(
  onRequest: (options, handler) {
    final token = tokenStore.read();
    if (token != null) options.headers['Authorization'] = 'Bearer $token';
    handler.next(options);
  },
));

Keep sensitive values such as tokens in flutter_secure_storage, not in SharedPreferences.

#Final tips

  • When showing data, always handle three states: loading, success and error.
  • Plan pagination from the start for large lists.
  • When the user leaves a screen, cancel in-flight requests with a CancelToken.

With this structure, switching servers or adding a new endpoint touches a single file.

Learn Flutter the professional, project-based way

The FastFlutter VIP course takes you from zero to a published app, with direct support from the instructor.

View the VIP course