Skip to content

Flutter Networking — HTTP Requests and REST API Integration

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Flutter Networking. We cover key concepts, practical examples, and best practices to help you master this topic.

Flutter networking enables apps to communicate with remote servers using HTTP requests, with packages like http and Dio providing client APIs for RESTful services.

What Will You Learn

  • Making HTTP requests with the http package
  • Using Dio for advanced networking with interceptors
  • JSON serialization and deserialization
  • Handling network errors and timeouts
  • Authentication tokens and headers
  • Uploading files and multipart requests
  • Caching responses locally

Why It Matters

Most mobile apps communicate with backend services. Flutter's networking ecosystem provides both simple (http package) and advanced (Dio) HTTP clients. Proper networking code handles loading states, errors, timeouts, and token refresh. JSON serialization with Code Generation tools eliminates manual mapping code. Understanding these patterns is essential for building apps that fetch and submit data reliably.

Real-World Use

The DodaTech Flutter app uses Dio with interceptors for all API calls. An auth interceptor attaches JWT tokens to requests. A retry interceptor retries failed requests with token refresh. A logging interceptor prints request details during development. JSON serialization uses json_serializable with build_runner for automatic code generation.

Learning Path

flowchart LR
  A[Flutter Forms] --> B[Flutter Networking\nYou are here]
  B --> C[Flutter Theming]
  style B fill:#f90,color:#fff

HTTP Package Basics

The http package provides simple HTTP client functionality:

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

class SimpleApiExample extends StatelessWidget {
  const SimpleApiExample({super.key});

  Future<void> fetchPosts() async {
    final url = Uri.parse('https://jsonplaceholder.typicode.com/posts');

    try {
      final response = await http.get(url);

      if (response.statusCode == 200) {
        final List<dynamic> data = jsonDecode(response.body);
        print('Fetched ${data.length} posts');
        for (var post in data.take(3)) {
          print('Post ${post['id']}: ${post['title']}');
        }
      } else {
        print('Server error: ${response.statusCode}');
      }
    } catch (e) {
      print('Network error: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: fetchPosts,
      child: Text('Fetch Posts'),
    );
  }
}

The http package is simple and adequate for basic requests. Always check statusCode and wrap network calls in try-catch blocks.

Data Models with JSON Serialization

Use json_serializable for automatic JSON mapping:

// In pubspec.yaml:
// dependencies:
//   json_annotation: ^4.8.0
// dev_dependencies:
//   build_runner: ^2.4.0
//   json_serializable: ^6.7.0

import 'package:json_annotation/json_annotation.dart';

part 'user.g.dart';

@JsonSerializable()
class User {
  final int id;
  final String name;
  final String username;
  final String email;
  final Address? address;

  User({
    required this.id,
    required this.name,
    required this.username,
    required this.email,
    this.address,
  });

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

@JsonSerializable()
class Address {
  final String street;
  final String city;
  final String zipcode;

  Address({
    required this.street,
    required this.city,
    required this.zipcode,
  });

  factory Address.fromJson(Map<String, dynamic> json) => _$AddressFromJson(json);
  Map<String, dynamic> toJson() => _$AddressToJson(this);
}

// Usage:
// final user = User.fromJson(json);
// final jsonString = jsonEncode(user.toJson());

Run dart run build_runner build to generate the fromJson and toJson implementations. The @JsonSerializable() annotation enables code generation for the class.

Dio for Advanced Networking

Dio provides interceptors, request cancellation, and better error handling:

import 'package:dio/dio.dart';

class ApiService {
  late final Dio _dio;

  ApiService() {
    _dio = Dio(BaseOptions(
      baseUrl: 'https://api.example.com',
      connectTimeout: Duration(seconds: 10),
      receiveTimeout: Duration(seconds: 10),
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
      },
    ));

    // Logging interceptor
    _dio.interceptors.add(LogInterceptor(
      requestBody: true,
      responseBody: true,
    ));

    // Auth interceptor
    _dio.interceptors.add(AuthInterceptor());

    // Retry interceptor
    _dio.interceptors.add(RetryInterceptor(
      dio: _dio,
      retries: 3,
      retryDelays: [
        Duration(seconds: 1),
        Duration(seconds: 2),
        Duration(seconds: 3),
      ],
    ));
  }

  Future<List<User>> getUsers() async {
    try {
      final response = await _dio.get<List<dynamic>>('/users');
      return response.data!.map((json) => User.fromJson(json as Map<String, dynamic>)).toList();
    } on DioException catch (e) {
      throw _handleError(e);
    }
  }

  Future<User> createUser(User user) async {
    try {
      final response = await _dio.post<Map<String, dynamic>>(
        '/users',
        data: user.toJson(),
      );
      return User.fromJson(response.data!);
    } on DioException catch (e) {
      throw _handleError(e);
    }
  }

  Exception _handleError(DioException e) {
    switch (e.type) {
      case DioExceptionType.connectionTimeout:
      case DioExceptionType.sendTimeout:
      case DioExceptionType.receiveTimeout:
        return Exception('Connection timed out. Please try again.');
      case DioExceptionType.badResponse:
        return Exception('Server error: ${e.response?.statusCode}');
      case DioExceptionType.cancel:
        return Exception('Request was cancelled');
      default:
        return Exception('Network error. Please check your connection.');
    }
  }
}

class AuthInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    // Add auth token
    options.headers['Authorization'] = 'Bearer YOUR_TOKEN';
    handler.next(options);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    if (err.response?.statusCode == 401) {
      // Handle token refresh
      print('Token expired. Refreshing...');
    }
    handler.next(err);
  }
}

Dio's configuration options include base URL, timeouts, default headers, and interceptors. Interceptors can modify requests, responses, and handle errors globally.

Loading States with FutureBuilder

Use FutureBuilder for declarative loading states:

class UserListScreen extends StatelessWidget {
  const UserListScreen({super.key});

  Future<List<User>> _fetchUsers() {
    return ApiService().getUsers();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Users')),
      body: FutureBuilder<List<User>>(
        future: _fetchUsers(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          }

          if (snapshot.hasError) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.error_outline, size: 48, color: Colors.red),
                  SizedBox(height: 16),
                  Text('Error: ${snapshot.error}'),
                  SizedBox(height: 16),
                  ElevatedButton(
                    onPressed: () {
                      // Rebuild to retry
                      (context as Element).reassemble();
                    },
                    child: Text('Retry'),
                  ),
                ],
              ),
            );
          }

          final users = snapshot.data!;
          return ListView.builder(
            itemCount: users.length,
            itemBuilder: (context, index) {
              final user = users[index];
              return ListTile(
                leading: CircleAvatar(child: Text('${user.id}')),
                title: Text(user.name),
                subtitle: Text(user.email),
              );
            },
          );
        },
      ),
    );
  }
}

FutureBuilder handles the connection state lifecycle: none, waiting, active (streams), and done. Always handle both loading and error states.

File Upload

Upload files using multipart requests:

import 'package:dio/dio.dart';
import 'package:http_parser/http_parser.dart';

class FileUploadService {
  final Dio _dio;

  FileUploadService(this._dio);

  Future<String> uploadImage(String filePath) async {
    final formData = FormData.fromMap({
      'file': await MultipartFile.fromFile(
        filePath,
        filename: 'profile.jpg',
        contentType: MediaType('image', 'jpeg'),
      ),
      'description': 'User profile image',
    });

    try {
      final response = await _dio.post<Map<String, dynamic>>(
        '/upload',
        data: formData,
        onSendProgress: (sent, total) {
          final progress = sent / total * 100;
          print('Upload progress: ${progress.toStringAsFixed(1)}%');
        },
      );

      return response.data!['url'] as String;
    } on DioException catch (e) {
      throw Exception('Upload failed: ${e.message}');
    }
  }

  Future<String> uploadMultipleFiles(List<String> filePaths) async {
    final files = filePaths.map((path) => MultipartFile.fromFileSync(
      path,
      contentType: MediaType('image', 'jpeg'),
    )).toList();

    final formData = FormData.fromMap({
      'files': files,
    });

    final response = await _dio.post<Map<String, dynamic>>(
      '/upload-multiple',
      data: formData,
    );

    return response.data!['message'] as String;
  }
}

FormData constructs multipart requests. MultipartFile.fromFile reads the file. onSendProgress provides upload progress for UI indicators.

Caching Responses

Cache network responses locally for offline support:

import 'dart:convert';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:dio/dio.dart';

class CachingService {
  final Dio _dio;
  final CacheManager _cacheManager;

  CachingService(this._dio, this._cacheManager);

  Future<String> fetchWithCache(String url, {Duration maxAge = Duration(hours: 1)}) async {
    // Check cache first
    final cacheFile = await _cacheManager.getFileFromCache(url);
    if (cacheFile != null && cacheFile.validTill.isAfter(DateTime.now())) {
      final cachedData = await cacheFile.file.readAsString();
      print('Returning cached data for $url');
      return cachedData;
    }

    // Fetch from network
    try {
      final response = await _dio.get<String>(url);

      // Store in cache
      await _cacheManager.putFile(
        url,
        utf8.encode(response.data!),
        maxAge: maxAge,
        key: url,
      );

      return response.data!;
    } on DioException catch (e) {
      // If network fails, return stale cache if available
      if (cacheFile != null) {
        print('Network failed, returning stale cache');
        return await cacheFile.file.readAsString();
      }
      throw Exception('Failed to fetch data: ${e.message}');
    }
  }
}

Cache-first Strategy improves perceived performance. Stale cache fallback enables basic offline functionality.

Websocket with Flutter

Flutter supports WebSocket for real-time communication:

import 'dart:convert';
import 'package:web_socket_channel/web_socket_channel.dart';

class WebSocketService {
  WebSocketChannel? _channel;

  void connect(String url) {
    _channel = WebSocketChannel.connect(Uri.parse(url));

    _channel!.stream.listen(
      (message) {
        final data = jsonDecode(message as String);
        print('Received: $data');
        // Update UI with received data
      },
      onError: (error) {
        print('WebSocket error: $error');
        // Reconnect logic
      },
      onDone: () {
        print('WebSocket connection closed');
        // Reconnect logic
      },
    );
  }

  void sendMessage(Map<String, dynamic> message) {
    if (_channel != null) {
      _channel!.sink.add(jsonEncode(message));
    }
  }

  void disconnect() {
    _channel?.sink.close();
  }
}

WebSocket is ideal for chat, real-time notifications, and live data feeds. Handle reconnection in the onDone and onError callbacks.

Common Mistakes

  1. Not handling network errors gracefully: Network calls can fail for many reasons. Always show user-friendly error messages and provide retry options.

  2. Leaving streams open: HTTP responses (especially streams) must be closed. Dio handles this automatically, but custom stream usage requires manual cleanup.

  3. Not setting timeouts: Without timeouts, network requests can hang indefinitely. Always set connect and receive timeouts.

  4. Hardcoding API URLs: Store base URLs in environment config files. Use --dart-define flags or .env files for different environments.

  5. Ignoring response caching: Every network call consumes data and slows the app. Cache responses with appropriate TTL for improved performance and offline support.

Practice Questions

  1. How does Dio's interceptor system differ from the http package's approach?
  2. What is the purpose of json_serializable and build_runner?
  3. How does FutureBuilder handle the different connection states?
  4. What is the advantage of multipart requests for file uploads?
  5. Challenge: Build a post creation screen with Dio. Include fields for title, body, and an image picker. Upload the image as multipart, submit the post data as JSON, show upload progress, and handle errors with retry.

Mini Project

Build a news reader app:

  • Fetch articles from a public API (NewsAPI)
  • Display articles in a ListView with title, description, and image
  • Cache articles for offline reading
  • Pull-to-refresh to reload articles
  • Error handling with retry button
  • Loading indicator during fetch
  • Article detail screen with full content
  • Search functionality using query parameters

FAQ

What is the difference between the http package and Dio?

The http package is simpler and has no dependencies. Dio provides interceptors, request cancellation, form data, file uploads, and better error handling. Choose http for simple APIs, Dio for complex needs.

How do I handle authentication tokens?

Store tokens securely using flutter_secure_storage. Attach to requests via an interceptor (Dio) or manual header setting (http). Refresh tokens silently when they expire.

How can I cancel an HTTP request?

Dio supports CancelToken. Pass the token to the request and call cancelToken.cancel(). The http package does not support cancellation.

What is the best way to serialize/deserialize JSON?

Use json_serializable with build_runner. It generates fromJson/toJson automatically. For simple cases, manual jsonDecode/jsonEncode suffices.

How do I handle SSL certificate errors?

For development, use BadCertificateCallback on HttpClient. For production, ensure valid certificates and never disable SSL verification.

What is Next

Now that you understand networking, learn about theming in Flutter. Proceed to Flutter Theming for custom themes, colors, typography, and dark mode. Then explore Flutter Animations for motion and transitions.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro