Dio & HTTP #
Ketika kita membangun aplikasi Flutter yang membutuhkan interaksi dengan server backend, salah satu keputusan paling mendasar yang harus kita ambil adalah memilih HTTP client yang akan kita gunakan. HTTP client bertanggung jawab untuk menyusun paket data request, mengirimkannya melewati jaringan internet, dan menerima response kembali untuk diolah oleh aplikasi.
Di ekosistem pengembangan Dart dan Flutter, terdapat dua pilihan utama untuk mengurusi masalah HTTP client ini:
- Package
http: Pustaka resmi yang dikembangkan langsung oleh tim Dart. Pustaka ini sangat ringan, minimalis, dan sangat cocok untuk kebutuhan pengiriman data yang sederhana atau proses pembuatan prototipe cepat. - Pustaka
dio: Pustaka pihak ketiga yang sangat populer dan tangguh (powerful). Dio dirancang untuk memenuhi kebutuhan aplikasi berskala produksi yang memerlukan konfigurasi tingkat lanjut seperti penanganan interceptor otomatis, pembatalan request, pengunggahan berkas secara multipart, pencatatan log jaringan terpusat, dan pengulangan koneksi otomatis (auto retry).
Dalam dokumen ini, kita akan membahas secara mendalam cara penggunaan kedua pustaka tersebut, membandingkan kelebihan masing-masing, mempelajari pola perancangan singleton client, mengonfigurasi interceptor sebagai middleware, hingga menyusun strategi penanganan error yang kokoh untuk meningkatkan pengalaman pengguna aplikasi kita.
Package http — Sederhana dan Ringkas #
Package http adalah pustaka resmi Dart yang dirancang dengan filosofi kesederhanaan. Pustaka ini tidak memiliki banyak fitur tambahan di luar fungsi dasar pengiriman data HTTP, menjadikannya sangat mudah dipahami bahkan oleh pengembang pemula sekalipun.
Untuk menggunakannya, pertama-tama tambahkan dependensi http di berkas pubspec.yaml proyek kita:
dependencies:
http: ^1.2.2
Implementasi Operasi CRUD dengan Pustaka http #
Berikut adalah contoh lengkap pembuatan kelas klien API untuk mengelola data produk menggunakan pustaka http. Kita memanfaatkan fungsi dasar seperti get, post, put, dan delete.
import 'dart:convert';
import 'package:http/http.dart' as http;
class LayananProdukHttp {
static const String _baseUrl = 'https://api.tokokita.com/v1';
// Header standar untuk komunikasi JSON
final Map<String, String> _headers = {
'Content-Type': 'application/json; charset=UTF-8',
'Accept': 'application/json',
};
// 1. GET - Mengambil daftar produk
Future<List<Map<String, dynamic>>> ambilSemuaProduk() async {
try {
final response = await http.get(
Uri.parse('$_baseUrl/produk'),
headers: _headers,
);
if (response.statusCode == 200) {
final List<dynamic> dataMentah = jsonDecode(response.body);
return dataMentah.map((item) => item as Map<String, dynamic>).toList();
} else {
throw Exception('Gagal mengambil produk. Status: ${response.statusCode}');
}
} catch (e) {
throw Exception('Terjadi kesalahan jaringan: $e');
}
}
// 2. POST - Membuat produk baru
Future<Map<String, dynamic>> buatProdukBaru(Map<String, dynamic> produk) async {
try {
final response = await http.post(
Uri.parse('$_baseUrl/produk'),
headers: _headers,
body: jsonEncode(produk),
);
if (response.statusCode == 201) {
return jsonDecode(response.body) as Map<String, dynamic>;
} else {
throw Exception('Gagal membuat produk baru. Status: ${response.statusCode}');
}
} catch (e) {
throw Exception('Terjadi kesalahan jaringan: $e');
}
}
// 3. PUT - Memperbarui produk secara total
Future<Map<String, dynamic>> perbaruiProduk(String id, Map<String, dynamic> produk) async {
try {
final response = await http.put(
Uri.parse('$_baseUrl/produk/$id'),
headers: _headers,
body: jsonEncode(produk),
);
if (response.statusCode == 200) {
return jsonDecode(response.body) as Map<String, dynamic>;
} else {
throw Exception('Gagal memperbarui produk. Status: ${response.statusCode}');
}
} catch (e) {
throw Exception('Terjadi kesalahan jaringan: $e');
}
}
// 4. DELETE - Menghapus produk
Future<void> hapusProduk(String id) async {
try {
final response = await http.delete(
Uri.parse('$_baseUrl/produk/$id'),
headers: _headers,
);
if (response.statusCode != 200 && response.statusCode != 204) {
throw Exception('Gagal menghapus produk. Status: ${response.statusCode}');
}
} catch (e) {
throw Exception('Terjadi kesalahan jaringan: $e');
}
}
}
Keterbatasan Utama Package http #
Meskipun menulis kode dengan http terasa sangat cepat, kita akan mulai menghadapi hambatan serius ketika aplikasi kita berkembang dan membutuhkan arsitektur jaringan yang lebih dinamis. Beberapa keterbatasan pustaka http meliputi:
- Tanpa Konfigurasi Global: Kita harus menulis ulang alamat base URL dan headers secara manual di setiap fungsi request. Jika base URL berubah, kita harus mengubahnya di banyak tempat atau merancang pembungkus (wrapper) kustom yang melelahkan.
- Tanpa Interceptor Bawaan: Kita tidak bisa menyisipkan token autentikasi secara otomatis ke semua request atau menangani kasus token kedaluwarsa secara terpusat.
- Pengunggahan Berkas yang Rumit: Proses pengiriman data multipart (seperti foto profil pengguna) memerlukan penulisan kelas
MultipartRequestyang bertele-tele. - Tidak Ada Fitur Pembatalan: Kita tidak bisa membatalkan request yang sedang berjalan ketika pengguna menavigasi keluar dari layar secara mendadak, yang dapat memboroskan memori dan performa perangkat.
Pustaka Dio — HTTP Client Tangguh untuk Produksi #
Untuk mengatasi keterbatasan di atas, komunitas Flutter merekomendasikan penggunaan pustaka Dio. Dio menawarkan semua fitur mutakhir yang dibutuhkan oleh aplikasi seluler modern agar dapat berkomunikasi secara efisien dengan server API.
Untuk mulai menggunakan Dio, daftarkan dependensinya di berkas pubspec.yaml:
dependencies:
dio: ^5.7.0
Mengonfigurasi Dio Menggunakan BaseOptions #
Di Dio, kita dapat mengonfigurasi pengaturan jaringan secara terpusat saat pertama kali menginisialisasi instansi Dio menggunakan objek BaseOptions.
import 'package:dio/dio.dart';
final dio = Dio(
BaseOptions(
// Base URL yang akan dipasang secara otomatis di depan path request
baseUrl: 'https://api.tokokita.com/v1',
// Batas waktu toleransi pembukaan koneksi awal (Timeout)
connectTimeout: const Duration(seconds: 10),
// Batas waktu toleransi penerimaan respons data dari server
receiveTimeout: const Duration(seconds: 15),
// Batas waktu pengiriman data dari perangkat ke server
sendTimeout: const Duration(seconds: 15),
// Header global yang selalu disisipkan di setiap request
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
),
);
Implementasi Singleton untuk Instansi Dio #
Di dalam arsitektur aplikasi Flutter yang sehat, kita tidak boleh membuat instansi Dio baru di setiap kelas API. Membuat instansi Dio secara berulang-ulang akan memboroskan memori RAM dan dapat merusak pool koneksi TCP yang telah dioptimalkan oleh sistem operasi.
Kita harus menggunakan Singleton Pattern untuk memastikan hanya ada satu instansi Dio yang hidup di memori selama siklus hidup aplikasi kita berjalan.
Berikut adalah desain kelas klien jaringan berbasis singleton yang aman dan siap digunakan:
// lib/core/network/api_client.dart
import 'package:dio/dio.dart';
class ApiClient {
// 1. Simpan instansi privat statis
static final ApiClient _instance = ApiClient._internal();
// 2. Factory constructor yang mengembalikan instansi yang sama
factory ApiClient() => _instance;
// Variabel penampung instansi Dio
late final Dio dio;
// 3. Named constructor internal untuk inisialisasi satu kali
ApiClient._internal() {
dio = Dio(
BaseOptions(
baseUrl: 'https://api.tokokita.com/v1',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
),
);
// Di sini kita dapat menambahkan Interceptor global kita nantinya
dio.interceptors.addAll([
LoggyInterceptor(), // Contoh interceptor logging
]);
}
}
// Cara penggunaan di seluruh aplikasi kita:
// final dio = ApiClient().dio;
Operasi CRUD Lengkap dengan Dio #
Setelah kita memiliki konfigurasi global dan singleton, menulis kode CRUD dengan Dio menjadi jauh lebih ringkas. Dio secara otomatis mengurai format JSON di latar belakang, sehingga kita tidak perlu memanggil fungsi jsonDecode secara manual.
class LayananProdukDio {
final Dio _dio;
// Dependency injection via konstruktor
LayananProdukDio(this._dio);
// 1. GET dengan query parameters dinamis (misal untuk pencarian & pagination)
Future<List<dynamic>> ambilProduk({
int halaman = 1,
int batas = 20,
String? pencarian,
}) async {
final response = await _dio.get(
'/produk',
queryParameters: {
'page': halaman,
'limit': batas,
if (pencarian != null) 'search': pencarian,
},
);
// Dio secara otomatis mengonversi response body ke tipe data Map/List Dart
return response.data as List<dynamic>;
}
// 2. POST dengan mengirim payload body JSON langsung
Future<Map<String, dynamic>> tambahProduk(Map<String, dynamic> dataProduk) async {
final response = await _dio.post(
'/produk',
data: dataProduk, // Langsung masukkan Map, Dio otomatis meng-encode ke JSON
);
return response.data as Map<String, dynamic>;
}
// 3. PUT untuk pembaruan data total
Future<Map<String, dynamic>> perbaruiProduk(String id, Map<String, dynamic> dataBaru) async {
final response = await _dio.put(
'/produk/$id',
data: dataBaru,
);
return response.data as Map<String, dynamic>;
}
// 4. PATCH untuk pembaruan sebagian field
Future<Map<String, dynamic>> perbaruiSebagianProduk(String id, Map<String, dynamic> fieldUbah) async {
final response = await _dio.patch(
'/produk/$id',
data: fieldUbah,
);
return response.data as Map<String, dynamic>;
}
// 5. DELETE untuk menghapus data
Future<void> hapusProduk(String id) async {
await _dio.delete('/produk/$id');
}
}
Interceptors — Middleware Jaringan Klien #
Salah satu keunggulan terbesar Dio adalah dukungannya terhadap Interceptors. Interceptor bertindak seperti middleware pada lalu lintas jaringan. Interceptor memiliki tiga gerbang utama:
onRequest: Mengintersep paket request sesaat sebelum dikirim ke server.onResponse: Mengintersep paket response sesaat setelah diterima dari server, sebelum dikirim ke pemanggil kode (caller).onError: Mengintersep kesalahan jaringan sebelum error dilemparkan ke UI aplikasi.
Berikut adalah diagram alir kerja Interceptor yang bertindak sebagai pos pemeriksaan lalu lintas data kita:
graph TD
classDef default stroke:#333,stroke-width:2px;
A["Panggilan HTTP Client (Aplikasi)"] -->|"1. Picu Request"| B["Request Interceptor"]
B -->|"2. Tambah Token / Headers"| C["Internet (Server API)"]
C -->|"3. Kirim Response"| D["Response Interceptor"]
D -->|"4. Log / Modifikasi Data"| E["UI / Repository Klien"]
C -. "4a. Jika Error (401/Timeout)" .-> F["Error Interceptor"]
F -. "4b. Refresh Token / Ulangi Request" .-> B
F -. "4c. Teruskan Error jika Gagal" .-> EPembuatan Interceptor Autentikasi Kustom #
Mari kita buat Interceptor kustom yang bertugas menyisipkan token keamanan JWT ke setiap request secara otomatis dari penyimpanan aman, sehingga kita tidak perlu menuliskan header authorization di setiap fungsi API kita secara berulang.
class AuthInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// 1. Ambil token akses JWT dari penyimpanan lokal aman
final tokenAkses = PenyimpananLokal.ambilTokenAkses();
if (tokenAkses != null) {
// 2. Sematkan ke dalam header Authorization secara otomatis
options.headers['Authorization'] = 'Bearer $tokenAkses';
}
// 3. Lanjutkan perjalanan request ke server
super.onRequest(options, handler);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
// Di sini kita bisa mendeteksi jika terjadi error 401 (Unauthorized).
// Kita dapat memicu proses refresh token otomatis (akan dibahas di modul Autentikasi).
if (err.response?.statusCode == 401) {
print('Token kedaluwarsa, butuh refresh token.');
}
super.onError(err, handler);
}
}
CancelToken — Menghentikan Request yang Tidak Dibutuhkan #
Bayangkan skenario berikut: pengguna berada di Halaman Pencarian Produk, mereka mengetik kata kunci dengan cepat, memicu 5 request pencarian beruntun ke server API. Sebelum request kelima selesai, pengguna tiba-tiba menekan tombol kembali (Back) untuk keluar dari layar.
Jika kita tidak membatalkan request-request tersebut, aplikasi akan terus memproses data jaringan di latar belakang secara sia-sia. Dio menyediakan kelas CancelToken untuk mengatasi masalah efisiensi ini.
Berikut adalah implementasi pembatalan request pencarian dinamis:
class LayananPencarianProduk {
final Dio _dio;
CancelToken? _cancelToken;
LayananPencarianProduk(this._dio);
Future<List<dynamic>> cariProduk(String query) async {
// 1. Jika ada request pencarian sebelumnya yang masih berjalan, batalkan segera!
if (_cancelToken != null && !_cancelToken!.isCancelled) {
_cancelToken!.cancel('Pencarian baru dipicu oleh pengguna.');
}
// 2. Buat instansi CancelToken baru
_cancelToken = CancelToken();
try {
final response = await _dio.get(
'/produk/cari',
queryParameters: {'q': query},
cancelToken: _cancelToken, // 3. Sematkan token pembatalan ke request
);
return response.data as List<dynamic>;
} on DioException catch (e) {
// 4. Deteksi apakah error disebabkan oleh pembatalan sengaja
if (CancelToken.isCancel(e)) {
print('Request dibatalkan dengan sukses: ${e.message}');
return []; // Kembalikan array kosong, ini bukan error jaringan sungguhan
}
rethrow; // Jika error jaringan sungguhan, lemparkan ke atas
}
}
}
Mengunggah File (File Upload) Berbasis FormData #
Mengirim file multimedia seperti gambar, video, atau dokumen PDF dari aplikasi Flutter ke server API memerlukan format request khusus yang disebut multipart/form-data. Dio menyediakan kelas FormData untuk menyusun data multipart dengan sangat mudah.
Kita juga dapat memantau persentase progres pengiriman file untuk ditampilkan sebagai indikator ProgressBar di antarmuka pengguna aplikasi kita menggunakan callback onSendProgress.
import 'dart:io';
import 'package:dio/dio.dart';
class LayananUnggahFile {
final Dio _dio;
LayananUnggahFile(this._dio);
Future<String> unggahFotoProfil(File fileGambar, String userId) async {
// 1. Ekstrak nama file dari path lokal
final namaBerkas = fileGambar.path.split('/').last;
// 2. Susun objek FormData (mirip dengan format form HTML)
final formData = FormData.fromMap({
'user_id': userId,
// Membaca berkas fisik menjadi MultipartFile asinkron
'foto': await MultipartFile.fromFile(
fileGambar.path,
filename: namaBerkas,
),
});
try {
final response = await _dio.post(
'/user/upload-foto',
data: formData,
// Menyediakan callback progres pengunggahan byte data
onSendProgress: (int byteTerkirim, int totalByte) {
if (totalByte != -1) {
final double persentase = (byteTerkirim / totalByte) * 100;
print('Progres Pengunggahan: ${persentase.toStringAsFixed(0)}%');
}
},
);
// Kembalikan URL foto hasil unggahan yang diberikan oleh server
return response.data['url_foto'] as String;
} catch (e) {
throw Exception('Gagal mengunggah foto profil: $e');
}
}
}
Mengunduh File (File Download) dengan Progress Bar #
Selain mengunggah, Dio juga menyediakan fungsi siap pakai download() untuk mengunduh berkas biner berukuran besar (seperti PDF laporan keuangan atau file arsip ZIP) dari internet langsung ke direktori lokal perangkat.
Sama seperti proses unggah, kita dapat memantau progres unduhan menggunakan callback onReceiveProgress.
class LayananUnduhFile {
final Dio _dio;
LayananUnduhFile(this._dio);
Future<void> unduhEbook({
required String urlEbook,
required String pathPenyimpananLokal,
required void Function(double progres) updateUiprogres,
}) async {
try {
await _dio.download(
urlEbook,
pathPenyimpananLokal,
// Memantau byte data yang diterima dari server
onReceiveProgress: (int byteDiterima, int totalByte) {
if (totalByte != -1) {
final double rasioProgres = byteDiterima / totalByte;
// Kirim nilai progres (0.0 hingga 1.0) ke fungsi update UI
updateUiprogres(rasioProgres);
}
},
options: Options(
responseType: ResponseType.bytes, // Menerima data dalam bentuk byte biner
followRedirects: true,
),
);
} catch (e) {
throw Exception('Proses pengunduhan berkas gagal: $e');
}
}
}
Penanganan Error Terstruktur Berbasis DioException #
Jaringan internet sangat tidak terduga. Terkadang koneksi Wi-Fi terputus di tengah jalan, server backend mengalami crash, atau paket data mengalami timeout karena cuaca buruk. Untuk menjamin aplikasi kita tetap stabil, kita harus mengelompokkan jenis kegagalan jaringan secara terstruktur menggunakan kelas DioException.
Mari kita buat fungsi pembungkus yang aman (safe request wrapper) untuk memetakan error teknis Dio menjadi objek Exception kustom yang ramah bagi pengguna awam:
// Definisi Custom Exception Kelas Kita
class JaringanException implements Exception {
final String pesan;
JaringanException(this.pesan);
@override
String toString() => pesan;
}
class ApiService {
final Dio _dio;
ApiService(this._dio);
// Wrapper aman untuk mengeksekusi request jaringan kita
Future<T> eksekusiAman<T>(Future<Response<T>> Function() request) async {
try {
final response = await request();
return response.data as T;
} on DioException catch (e) {
// Memetakan kategori DioException
switch (e.type) {
case DioExceptionType.connectionTimeout:
throw JaringanException('Batas waktu koneksi habis. Mohon periksa sinyal internet kita.');
case DioExceptionType.sendTimeout:
throw JaringanException('Gagal mengirim data ke server tepat waktu.');
case DioExceptionType.receiveTimeout:
throw JaringanException('Server terlalu lama menanggapi permintaan kita.');
case DioExceptionType.connectionError:
throw JaringanException('Perangkat kita tidak terhubung ke koneksi internet.');
case DioExceptionType.badResponse:
// Terjadi ketika server membalas dengan status code kegagalan (4xx atau 5xx)
final statusCode = e.response?.statusCode;
final dataError = e.response?.data;
final pesanErrorServer = dataError is Map
? dataError['message'] ?? 'Terjadi kesalahan sistem'
: 'Kesalahan tidak diketahui';
switch (statusCode) {
case 400:
throw JaringanException('Format data yang dikirim tidak sesuai: $pesanErrorServer');
case 401:
throw JaringanException('Sesi masuk kita telah berakhir. Silakan masuk kembali.');
case 403:
throw JaringanException('Kita tidak memiliki akses untuk membuka data ini.');
case 404:
throw JaringanException('Data yang kita cari tidak ditemukan di server.');
case 422:
throw JaringanException('Validasi data gagal: $pesanErrorServer');
case 500:
throw JaringanException('Server internal sedang mengalami gangguan. Mohon coba beberapa saat lagi.');
default:
throw JaringanException('Gangguan Server (Kode: $statusCode): $pesanErrorServer');
}
case DioExceptionType.cancel:
throw JaringanException('Permintaan jaringan dibatalkan oleh sistem.');
default:
throw JaringanException('Terjadi gangguan jaringan yang tidak diketahui.');
}
} catch (e) {
// Menangkap error non-DioException (seperti error casting tipe data)
throw JaringanException('Gagal memproses data: $e');
}
}
}
Komparasi Head-to-Head: http vs Dio #
Untuk mempermudah kita dan tim menentukan pustaka mana yang paling tepat untuk proyek kita, berikut adalah ringkasan perbandingan kelebihan dan kekurangan dari masing-masing pustaka:
| Dimensi Fitur | Package http (Bawaan/Resmi) | Pustaka Dio (Pihak Ketiga) |
|---|---|---|
| Beban Ukuran Proyek | Sangat Kecil / Minimal | Cukup Besar / Sedang |
| Kemudahan Konfigurasi | Mudah di awal, namun verbose di akhir | Memerlukan setup class awal, namun sangat modular |
| Dukungan Middleware (Interceptors) | Tidak Ada (Harus membuat pembungkus manual) | Ada ( onRequest, onResponse, onError bawaan) |
| Pembatalan Request (Cancel) | Tidak Didukung secara default | Didukung secara native lewat kelas CancelToken |
| Progress Unggah/Unduh | Sulit dihitung secara akurat | Sangat mudah lewat callback onSendProgress/onReceiveProgress |
| Autentikasi Ulang Otomatis | Harus ditulis manual di setiap hit API | Sangat mudah dikelola menggunakan QueuedInterceptor |
| Casting JSON otomatis | Tidak Ada (Wajib jsonDecode manual) | Otomatis di-parse oleh parser internal Dio |
Secara umum, gunakanlah package http jika kita sedang membangun aplikasi hobi sederhana, prototipe aplikasi cepat, atau pustaka Dart murni yang tidak boleh bergantung pada pustaka eksternal pihak ketiga. Gunakanlah pustaka Dio sebagai standar utama pembuatan aplikasi komersial berskala produksi yang membutuhkan kestabilan, efisiensi tinggi, penanganan token keamanan yang kompleks, dan skalabilitas arsitektur yang tinggi.
Ringkasan #
- Package
httpdikembangkan resmi oleh tim Dart dengan pendekatan minimalis, sangat cocok untuk pemrosesan request sederhana atau proses prototyping cepat.- Pustaka Dio adalah pustaka HTTP client Flutter paling populer untuk produksi berkat kelengkapan fiturnya seperti interceptor, pembatalan request, dan progress tracking.
- Singleton Pattern wajib kita terapkan untuk instansi DioClient agar menghemat alokasi RAM perangkat dan memaksimalkan penggunaan pool TCP secara efisien.
- Interceptors bertindak sebagai pos keamanan (middleware) terpusat untuk memanipulasi request (menyisipkan token) atau menangani error secara global.
CancelTokenmengizinkan kita membatalkan request asinkron yang sedang berjalan guna menghemat kuota internet dan daya tahan baterai perangkat pengguna.FormDatadigunakan untuk mengirim berkas multimedia multipart (seperti foto atau dokumen) lengkap dengan monitoring progres pengunggahan.DioExceptionmembagi jenis kegagalan jaringan secara spesifik untuk mempermudah kita menampilkan pesan error yang ramah pengguna berdasarkan tipe gangguannya.- Evaluasi Kebutuhan: Pilih
httpuntuk proyek kecil/minimalis, dan pilihDiountuk arsitektur aplikasi berskala produksi.