Dio & HTTP #

Flutter menyediakan dua pilihan utama untuk HTTP client: package http yang sederhana dan resmi dari tim Dart, serta Dio yang lebih powerful untuk kebutuhan produksi. Keduanya melayani kebutuhan yang berbeda — memahami kapan menggunakan masing-masing akan menghindarkan kamu dari over-engineering atau under-engineering solusi networking.

Package http — Sederhana dan Resmi #

Package http adalah pilihan resmi dari tim Dart. Ringan, mudah dipahami, dan cukup untuk kebutuhan dasar.

# pubspec.yaml
dependencies:
  http: ^1.2.2

Operasi CRUD Dasar #

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

class ProdukApiClient {
  static const _baseUrl = 'https://api.contoh.com';
  static const _headers = {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
  };

  // GET -- ambil semua produk
  Future<List<Map<String, dynamic>>> getProduk() async {
    final response = await http.get(
      Uri.parse('$_baseUrl/produk'),
      headers: _headers,
    );

    if (response.statusCode == 200) {
      final List<dynamic> data = jsonDecode(response.body);
      return data.cast<Map<String, dynamic>>();
    } else {
      throw Exception('Gagal mengambil produk: ${response.statusCode}');
    }
  }

  // GET -- ambil satu produk
  Future<Map<String, dynamic>> getProdukById(String id) async {
    final response = await http.get(Uri.parse('$_baseUrl/produk/$id'));

    if (response.statusCode == 200) {
      return jsonDecode(response.body);
    } else if (response.statusCode == 404) {
      throw Exception('Produk tidak ditemukan');
    } else {
      throw Exception('Error: ${response.statusCode}');
    }
  }

  // POST -- tambah produk baru
  Future<Map<String, dynamic>> tambahProduk(Map<String, dynamic> produk) async {
    final response = await http.post(
      Uri.parse('$_baseUrl/produk'),
      headers: _headers,
      body: jsonEncode(produk),
    );

    if (response.statusCode == 201) {
      return jsonDecode(response.body);
    } else {
      throw Exception('Gagal menambah produk: ${response.statusCode}');
    }
  }

  // PUT -- update produk
  Future<Map<String, dynamic>> updateProduk(String id, Map<String, dynamic> produk) async {
    final response = await http.put(
      Uri.parse('$_baseUrl/produk/$id'),
      headers: _headers,
      body: jsonEncode(produk),
    );

    if (response.statusCode == 200) {
      return jsonDecode(response.body);
    } else {
      throw Exception('Gagal mengupdate produk: ${response.statusCode}');
    }
  }

  // DELETE -- hapus produk
  Future<void> hapusProduk(String id) async {
    final response = await http.delete(Uri.parse('$_baseUrl/produk/$id'));
    if (response.statusCode != 204 && response.statusCode != 200) {
      throw Exception('Gagal menghapus produk: ${response.statusCode}');
    }
  }
}

Keterbatasan Package http #

Package http memiliki keterbatasan yang terasa di aplikasi produksi:
  ✗ Tidak ada interceptors -- harus wrap setiap request secara manual
  ✗ Tidak ada retry otomatis
  ✗ Tidak ada cancel token untuk membatalkan request
  ✗ Upload file lebih verbose
  ✗ Tidak ada global configuration (baseUrl, timeout, dll.)
  ✗ Progress callback untuk upload/download terbatas

Pertimbangkan Dio jika aplikasimu butuh salah satu dari fitur di atas.

Dio — HTTP Client untuk Produksi #

Dio adalah HTTP client yang powerful untuk Dart/Flutter. Ia mendukung global configuration, interceptors, FormData, request cancellation, file uploading/downloading, timeout, dan banyak lagi.

# pubspec.yaml
dependencies:
  dio: ^5.7.0

Setup Dasar Dio #

import 'package:dio/dio.dart';

final dio = Dio(BaseOptions(
  baseUrl: 'https://api.contoh.com',
  connectTimeout: const Duration(seconds: 10),
  receiveTimeout: const Duration(seconds: 30),
  sendTimeout: const Duration(seconds: 30),
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
  },
));

Singleton Pattern — Satu Instance untuk Seluruh Aplikasi #

Sangat direkomendasikan menggunakan satu instance Dio di seluruh aplikasi untuk efisiensi memory dan konsistensi konfigurasi:

// lib/core/network/dio_client.dart
class DioClient {
  static final DioClient _instance = DioClient._internal();
  factory DioClient() => _instance;

  late final Dio dio;

  DioClient._internal() {
    dio = Dio(
      BaseOptions(
        baseUrl: AppConfig.baseUrl,
        connectTimeout: const Duration(seconds: 10),
        receiveTimeout: const Duration(seconds: 30),
        headers: {'Accept': 'application/json'},
      ),
    );

    dio.interceptors.addAll([
      _AuthInterceptor(),
      _LoggingInterceptor(),
      _ErrorInterceptor(),
    ]);
  }
}

// Gunakan di seluruh app
final dioClient = DioClient().dio;

Operasi CRUD dengan Dio #

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

  // GET dengan query parameters
  Future<Response> getProduk({
    int page = 1,
    int limit = 20,
    String? kategori,
    String? sortBy,
  }) async {
    return _dio.get(
      '/produk',
      queryParameters: {
        'page': page,
        'limit': limit,
        if (kategori != null) 'kategori': kategori,
        if (sortBy != null) 'sort': sortBy,
      },
    );
  }

  // POST dengan body JSON
  Future<Response> tambahProduk(Map<String, dynamic> data) async {
    return _dio.post('/produk', data: data);
  }

  // PUT
  Future<Response> updateProduk(String id, Map<String, dynamic> data) async {
    return _dio.put('/produk/$id', data: data);
  }

  // PATCH -- update sebagian
  Future<Response> patchProduk(String id, Map<String, dynamic> data) async {
    return _dio.patch('/produk/$id', data: data);
  }

  // DELETE
  Future<Response> hapusProduk(String id) async {
    return _dio.delete('/produk/$id');
  }
}

Interceptors — Middleware untuk Semua Request #

Interceptor adalah fitur terpenting Dio. Mereka bertindak sebagai middleware — mengintersep request sebelum dikirim dan response sebelum diterima:

class _AuthInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    // Tambahkan token ke setiap request
    final token = AuthStorage.getAccessToken();
    if (token != null) {
      options.headers['Authorization'] = 'Bearer $token';
    }
    handler.next(options);  // lanjutkan request
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    // Proses response sebelum diteruskan ke caller
    handler.next(response);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    // Handle error secara global
    handler.next(err);  // teruskan error
    // atau: handler.resolve(response) untuk override dengan response sukses
    // atau: handler.reject(err) untuk reject dengan error custom
  }
}

class _LoggingInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    debugPrint('→ ${options.method} ${options.uri}');
    if (options.data != null) debugPrint('  Body: ${options.data}');
    handler.next(options);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    debugPrint('← ${response.statusCode} ${response.requestOptions.uri}');
    handler.next(response);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    debugPrint('✗ ${err.response?.statusCode} ${err.requestOptions.uri}');
    debugPrint('  ${err.message}');
    handler.next(err);
  }
}

CancelToken — Batalkan Request #

CancelToken memungkinkan pembatalan request yang sedang berjalan — berguna untuk search-as-you-type atau saat pengguna meninggalkan halaman:

class SearchService {
  CancelToken? _cancelToken;

  Future<List<Produk>> search(String query) async {
    // Batalkan request sebelumnya jika masih berjalan
    _cancelToken?.cancel('Query baru dikirim');
    _cancelToken = CancelToken();

    try {
      final response = await dio.get(
        '/produk/search',
        queryParameters: {'q': query},
        cancelToken: _cancelToken,
      );
      return (response.data as List)
          .map((json) => Produk.fromJson(json))
          .toList();
    } on DioException catch (e) {
      if (e.type == DioExceptionType.cancel) {
        // Request dibatalkan -- bukan error sesungguhnya
        return [];
      }
      rethrow;
    }
  }
}

// Di widget -- batalkan saat widget di-dispose
class _SearchState extends State<SearchScreen> {
  final _service = SearchService();

  @override
  Widget build(BuildContext context) {
    return TextField(
      onChanged: (query) => _search(query),
    );
  }
}

Upload File dengan FormData #

Future<String> uploadFoto(File foto, String produkId) async {
  final fileName = foto.path.split('/').last;

  final formData = FormData.fromMap({
    'produk_id': produkId,
    'foto': await MultipartFile.fromFile(
      foto.path,
      filename: fileName,
      contentType: DioMediaType('image', 'jpeg'),
    ),
  });

  final response = await dio.post(
    '/upload/foto',
    data: formData,
    options: Options(
      headers: {'Content-Type': 'multipart/form-data'},
    ),
    onSendProgress: (sent, total) {
      final progress = (sent / total * 100).toStringAsFixed(0);
      debugPrint('Upload: $progress%');
    },
  );

  return response.data['url'];
}

// Upload banyak file sekaligus
Future<void> uploadMultipleFoto(List<File> fotos) async {
  final files = await Future.wait(
    fotos.map((f) => MultipartFile.fromFile(f.path, filename: f.path.split('/').last)),
  );

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

  await dio.post('/upload/fotos', data: formData);
}

Download File dengan Progress #

Future<void> downloadFile({
  required String url,
  required String savePath,
  required void Function(double progress) onProgress,
}) async {
  await dio.download(
    url,
    savePath,
    onReceiveProgress: (received, total) {
      if (total != -1) {
        onProgress(received / total);
      }
    },
    options: Options(
      responseType: ResponseType.bytes,
      followRedirects: true,
    ),
  );
}

// Penggunaan
await downloadFile(
  url: 'https://contoh.com/laporan.pdf',
  savePath: '/storage/downloads/laporan.pdf',
  onProgress: (progress) {
    setState(() => _downloadProgress = progress);
  },
);

Error Handling dengan DioException #

Future<T> safeRequest<T>(Future<T> Function() request) async {
  try {
    return await request();
  } on DioException catch (e) {
    switch (e.type) {
      case DioExceptionType.connectionTimeout:
      case DioExceptionType.sendTimeout:
      case DioExceptionType.receiveTimeout:
        throw NetworkException('Koneksi timeout. Periksa internet kamu.');

      case DioExceptionType.connectionError:
        throw NetworkException('Tidak ada koneksi internet.');

      case DioExceptionType.badResponse:
        final statusCode = e.response?.statusCode;
        final message = e.response?.data?['message'] ?? 'Terjadi kesalahan';
        switch (statusCode) {
          case 400: throw BadRequestException(message);
          case 401: throw UnauthorizedException('Sesi habis. Silakan login kembali.');
          case 403: throw ForbiddenException('Kamu tidak memiliki akses.');
          case 404: throw NotFoundException('Data tidak ditemukan.');
          case 422: throw ValidationException(e.response?.data?['errors']);
          case 500: throw ServerException('Server sedang bermasalah.');
          default:  throw NetworkException('Error: $statusCode');
        }

      case DioExceptionType.cancel:
        throw RequestCancelledException();

      default:
        throw NetworkException(e.message ?? 'Terjadi kesalahan tidak diketahui');
    }
  }
}

http vs Dio — Kapan Memilih? #

Gunakan package http jika:
  ✓ Aplikasi kecil atau prototipe
  ✓ Hanya butuh beberapa request sederhana
  ✓ Tidak butuh interceptors atau retry
  ✓ Ingin dependencies sesedikit mungkin

Gunakan Dio jika:
  ✓ Aplikasi produksi
  ✓ Butuh autentikasi otomatis via interceptor
  ✓ Butuh retry otomatis saat koneksi gagal
  ✓ Butuh cancel token (search, navigasi)
  ✓ Butuh upload file dengan progress
  ✓ Butuh konfigurasi global (baseUrl, timeout, headers)
  ✓ Butuh logging request/response untuk debugging

Ringkasan #

  • Package http sederhana dan resmi — cocok untuk prototyping dan kebutuhan dasar. Keterbatasannya adalah tidak ada interceptors, retry, atau cancel token.
  • Dio adalah pilihan untuk produksi — mendukung BaseOptions (baseUrl, timeout), interceptors, FormData, CancelToken, dan download/upload dengan progress.
  • Gunakan singleton pattern untuk Dio — satu instance untuk seluruh aplikasi memastikan konfigurasi dan interceptors konsisten.
  • Interceptors adalah middleware untuk semua request — gunakan untuk inject auth token, logging, dan error handling global tanpa duplikasi kode.
  • CancelToken memungkinkan pembatalan request — penting untuk search-as-you-type agar request lama tidak menimpa hasil terbaru.
  • FormData dengan MultipartFile untuk upload file — gunakan onSendProgress untuk progress bar.
  • DioException punya banyak tipe — selalu handle connectionTimeout, connectionError, badResponse, dan cancel secara berbeda untuk UX yang baik.

← Sebelumnya: Overview   Berikutnya: JSON & Serialisasi →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact