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
httpsederhana 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.
CancelTokenmemungkinkan pembatalan request — penting untuk search-as-you-type agar request lama tidak menimpa hasil terbaru.FormDatadenganMultipartFileuntuk upload file — gunakanonSendProgressuntuk progress bar.DioExceptionpunya banyak tipe — selalu handleconnectionTimeout,connectionError,badResponse, dancancelsecara berbeda untuk UX yang baik.