Networking
Pengantar
Nylo Website membuat networking menjadi sederhana. Anda mendefinisikan endpoint API di kelas service yang meng-extend NyApiService, kemudian memanggilnya dari halaman Anda. Framework ini menangani dekoding JSON, penanganan error, caching, dan konversi otomatis response ke kelas model Anda (disebut "morphing").
API service Anda berada di lib/app/networking/. Proyek baru menyertakan ApiService default:
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext})
: super(
buildContext,
decoders: modelDecoders,
);
@override
String get baseUrl => getEnv('API_BASE_URL');
@override
Map<Type, Interceptor> get interceptors => {
...super.interceptors,
};
Future fetchUsers() async {
return await network(
request: (request) => request.get("/users"),
);
}
}
Ada tiga cara untuk membuat permintaan HTTP:
| Pendekatan | Mengembalikan | Cocok Untuk |
|---|---|---|
Method praktis (get, post, dll.) |
T? |
Operasi CRUD sederhana |
network() |
T? |
Permintaan yang membutuhkan caching, retry, atau header kustom |
networkResponse() |
NyResponse<T> |
Ketika Anda membutuhkan kode status, header, atau detail error |
Di balik layar, Nylo Website menggunakan Dio, klien HTTP yang powerful.
Method Praktis
NyApiService menyediakan method singkat untuk operasi HTTP umum. Method ini memanggil network() secara internal.
Permintaan GET
Future<User?> fetchUser(int id) async {
return await get<User>(
"/users/$id",
queryParameters: {"include": "profile"},
);
}
Permintaan POST
Future<User?> createUser(Map<String, dynamic> data) async {
return await post<User>("/users", data: data);
}
Permintaan PUT
Future<User?> updateUser(int id, Map<String, dynamic> data) async {
return await put<User>("/users/$id", data: data);
}
Permintaan DELETE
Future<bool?> deleteUser(int id) async {
return await delete<bool>("/users/$id");
}
Permintaan PATCH
Future<User?> patchUser(int id, Map<String, dynamic> data) async {
return await patch<User>("/users/$id", data: data);
}
Permintaan HEAD
Gunakan HEAD untuk memeriksa keberadaan resource atau mendapatkan header tanpa mengunduh body:
Future<bool> checkResourceExists(String url) async {
Response response = await head(url);
return response.statusCode == 200;
}
Helper Network
Method network memberi Anda lebih banyak kontrol daripada method praktis. Method ini mengembalikan data yang sudah diubah (T?) secara langsung.
class ApiService extends NyApiService {
...
Future<User?> fetchUser(int id) async {
return await network<User>(
request: (request) => request.get("/users/$id"),
);
}
Future<List<User>?> fetchUsers() async {
return await network<List<User>>(
request: (request) => request.get("/users"),
);
}
Future<User?> createUser(Map<String, dynamic> data) async {
return await network<User>(
request: (request) => request.post("/users", data: data),
);
}
}
Callback request menerima instance Dio dengan base URL dan interceptor Anda yang sudah dikonfigurasi.
Parameter network
| Parameter | Tipe | Deskripsi |
|---|---|---|
request |
Function(Dio) |
Permintaan HTTP yang akan dilakukan (wajib) |
bearerToken |
String? |
Bearer token untuk permintaan ini |
baseUrl |
String? |
Override base URL service |
headers |
Map<String, dynamic>? |
Header tambahan |
retry |
int? |
Jumlah percobaan retry |
retryDelay |
Duration? |
Jeda antar retry |
retryIf |
bool Function(DioException)? |
Kondisi untuk retry |
connectionTimeout |
Duration? |
Timeout koneksi |
receiveTimeout |
Duration? |
Timeout penerimaan |
sendTimeout |
Duration? |
Timeout pengiriman |
cacheKey |
String? |
Key cache |
cacheDuration |
Duration? |
Durasi cache |
cachePolicy |
CachePolicy? |
Strategi cache |
checkConnectivity |
bool? |
Periksa konektivitas sebelum permintaan |
handleSuccess |
Function(NyResponse<T>)? |
Callback sukses |
handleFailure |
Function(NyResponse<T>)? |
Callback gagal |
Helper networkResponse
Gunakan networkResponse ketika Anda membutuhkan akses ke response lengkap -- kode status, header, pesan error -- bukan hanya datanya. Method ini mengembalikan NyResponse<T> alih-alih T?.
Gunakan networkResponse ketika Anda perlu:
- Memeriksa kode status HTTP untuk penanganan spesifik
- Mengakses header response
- Mendapatkan pesan error detail untuk umpan balik pengguna
- Mengimplementasikan logika penanganan error kustom
Future<NyResponse<User>> fetchUser(int id) async {
return await networkResponse<User>(
request: (request) => request.get("/users/$id"),
);
}
Kemudian gunakan response di halaman Anda:
NyResponse<User> response = await _apiService.fetchUser(1);
if (response.isSuccessful) {
User? user = response.data;
print('Status: ${response.statusCode}');
} else {
print('Error: ${response.errorMessage}');
print('Status: ${response.statusCode}');
}
network vs networkResponse
// network() — mengembalikan data secara langsung
User? user = await network<User>(
request: (request) => request.get("/users/1"),
);
// networkResponse() — mengembalikan response lengkap
NyResponse<User> response = await networkResponse<User>(
request: (request) => request.get("/users/1"),
);
User? user = response.data;
int? status = response.statusCode;
Kedua method menerima parameter yang sama. Pilih networkResponse ketika Anda perlu memeriksa response lebih dari sekadar datanya.
NyResponse
NyResponse<T> membungkus response Dio dengan data yang sudah diubah dan helper status.
Properti
| Properti | Tipe | Deskripsi |
|---|---|---|
response |
Response? |
Response Dio asli |
data |
T? |
Data yang sudah diubah/didekode |
rawData |
dynamic |
Data response mentah |
headers |
Headers? |
Header response |
statusCode |
int? |
Kode status HTTP |
statusMessage |
String? |
Pesan status HTTP |
contentType |
String? |
Content type dari header |
errorMessage |
String? |
Pesan error yang diekstrak |
Pemeriksaan Status
| Getter | Deskripsi |
|---|---|
isSuccessful |
Status 200-299 |
isClientError |
Status 400-499 |
isServerError |
Status 500-599 |
isRedirect |
Status 300-399 |
hasData |
Data tidak null |
isUnauthorized |
Status 401 |
isForbidden |
Status 403 |
isNotFound |
Status 404 |
isTimeout |
Status 408 |
isConflict |
Status 409 |
isRateLimited |
Status 429 |
Helper Data
NyResponse<User> response = await apiService.fetchUser(1);
// Dapatkan data atau throw jika null
User user = response.dataOrThrow('User not found');
// Dapatkan data atau gunakan fallback
User user = response.dataOr(User.guest());
// Jalankan callback hanya jika berhasil
String? greeting = response.ifSuccessful((user) => 'Hello ${user.name}');
// Pattern match pada sukses/gagal
String result = response.when(
success: (user) => 'Welcome, ${user.name}!',
failure: (response) => 'Error: ${response.statusMessage}',
);
// Dapatkan header tertentu
String? authHeader = response.getHeader('Authorization');
Opsi Dasar
Konfigurasikan opsi Dio default untuk API service Anda menggunakan parameter baseOptions:
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext}) : super(
buildContext,
decoders: modelDecoders,
baseOptions: (BaseOptions baseOptions) {
return baseOptions
..connectTimeout = Duration(seconds: 5)
..sendTimeout = Duration(seconds: 5)
..receiveTimeout = Duration(seconds: 5);
},
);
...
}
Anda juga dapat mengkonfigurasi opsi secara dinamis pada instance:
apiService.setConnectTimeout(Duration(seconds: 10));
apiService.setReceiveTimeout(Duration(seconds: 30));
apiService.setSendTimeout(Duration(seconds: 10));
apiService.setContentType('application/json');
Klik di sini untuk melihat semua opsi dasar yang dapat Anda atur.
Menambahkan Header
Header Per-Permintaan
Future fetchWithHeaders() async => await network(
request: (request) => request.get("/test"),
headers: {
"Authorization": "Bearer aToken123",
"Device": "iPhone"
}
);
Bearer Token
Future fetchUser() async => await network(
request: (request) => request.get("/user"),
bearerToken: "hello-world-123",
);
Header Level Service
apiService.setHeaders({"X-Custom-Header": "value"});
apiService.setBearerToken("my-token");
Extension RequestHeaders
Tipe RequestHeaders (typedef Map<String, dynamic>) menyediakan method helper:
@override
Future<RequestHeaders> setAuthHeaders(RequestHeaders headers) async {
String? token = Auth.data(field: 'token');
if (token != null) {
headers.addBearerToken(token);
}
headers.addHeader('X-App-Version', '1.0.0');
return headers;
}
| Method | Deskripsi |
|---|---|
addBearerToken(token) |
Mengatur header Authorization: Bearer |
getBearerToken() |
Membaca bearer token dari header |
addHeader(key, value) |
Menambahkan header kustom |
hasHeader(key) |
Memeriksa apakah header ada |
Mengunggah File
Unggah File Tunggal
Future<UploadResponse?> uploadAvatar(String filePath) async {
return await upload<UploadResponse>(
'/upload',
filePath: filePath,
fieldName: 'avatar',
additionalFields: {'userId': '123'},
onProgress: (sent, total) {
double progress = sent / total * 100;
print('Progress: ${progress.toStringAsFixed(0)}%');
},
);
}
Unggah Beberapa File
Future<UploadResponse?> uploadDocuments() async {
return await uploadMultiple<UploadResponse>(
'/upload',
files: {
'avatar': '/path/to/avatar.jpg',
'document': '/path/to/doc.pdf',
},
additionalFields: {'userId': '123'},
onProgress: (sent, total) {
print('Progress: ${(sent / total * 100).toStringAsFixed(0)}%');
},
);
}
Mengunduh File
Future<void> downloadFile(String url, String savePath) async {
await download(
url,
savePath: savePath,
onProgress: (received, total) {
if (total != -1) {
print('Progress: ${(received / total * 100).toStringAsFixed(0)}%');
}
},
deleteOnError: true,
);
}
Interceptor
Interceptor memungkinkan Anda memodifikasi permintaan sebelum dikirim, menangani response, dan mengelola error. Mereka dijalankan pada setiap permintaan yang dibuat melalui API service.
Gunakan interceptor ketika Anda perlu:
- Menambahkan header autentikasi ke semua permintaan
- Mencatat log permintaan dan response untuk debugging
- Mentransformasi data permintaan/response secara global
- Menangani kode error tertentu (misal, memperbarui token saat 401)
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext}) : super(buildContext, decoders: modelDecoders);
@override
Map<Type, Interceptor> get interceptors => {
...super.interceptors,
BearerAuthInterceptor: BearerAuthInterceptor(),
LoggingInterceptor: LoggingInterceptor(),
};
...
}
Membuat Interceptor Kustom
metro make:interceptor logging
File: app/networking/dio/interceptors/logging_interceptor.dart
import 'package:nylo_framework/nylo_framework.dart';
class LoggingInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
print('REQUEST[${options.method}] => PATH: ${options.path}');
return super.onRequest(options, handler);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
print('RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}');
handler.next(response);
}
@override
void onError(DioException dioException, ErrorInterceptorHandler handler) {
print('ERROR[${dioException.response?.statusCode}] => PATH: ${dioException.requestOptions.path}');
handler.next(dioException);
}
}
Network Logger
Nylo Website menyertakan interceptor NetworkLogger bawaan. Secara default aktif ketika APP_DEBUG bernilai true di environment Anda.
Konfigurasi
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext}) : super(
buildContext,
decoders: modelDecoders,
useNetworkLogger: true,
networkLogger: NetworkLogger(
logLevel: LogLevelType.verbose,
request: true,
requestHeader: true,
requestBody: true,
responseBody: true,
responseHeader: false,
error: true,
),
);
}
Anda dapat menonaktifkannya dengan mengatur useNetworkLogger: false.
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext})
: super(
buildContext,
decoders: modelDecoders,
useNetworkLogger: false, // <-- Nonaktifkan logger
);
Level Log
| Level | Deskripsi |
|---|---|
LogLevelType.verbose |
Cetak semua detail permintaan/response |
LogLevelType.minimal |
Cetak method, URL, status, dan waktu saja |
LogLevelType.none |
Tidak ada output logging |
Memfilter Log
NetworkLogger(
filter: (options, args) {
// Only log requests to specific endpoints
return options.path.contains('/api/v1');
},
)
Menggunakan API Service
Ada dua cara untuk memanggil API service Anda dari halaman.
Instansiasi Langsung
class _MyHomePageState extends NyPage<MyHomePage> {
ApiService _apiService = ApiService();
@override
get init => () async {
List<User>? users = await _apiService.fetchUsers();
print(users);
};
}
Menggunakan Helper api()
Helper api membuat instance menggunakan apiDecoders dari config/decoders.dart:
class _MyHomePageState extends NyPage<MyHomePage> {
@override
get init => () async {
User? user = await api<ApiService>((request) => request.fetchUser());
print(user);
};
}
Dengan callback:
await api<ApiService>(
(request) => request.fetchUser(),
onSuccess: (response, data) {
// data is the morphed User? instance
},
onError: (DioException dioException) {
// Handle the error
},
);
Parameter Helper api()
| Parameter | Tipe | Deskripsi |
|---|---|---|
request |
Function(T) |
Fungsi permintaan API |
context |
BuildContext? |
Build context |
headers |
Map<String, dynamic> |
Header tambahan |
bearerToken |
String? |
Bearer token |
baseUrl |
String? |
Override base URL |
page |
int? |
Halaman paginasi |
perPage |
int? |
Item per halaman |
retry |
int |
Percobaan retry |
retryDelay |
Duration? |
Jeda antar retry |
onSuccess |
Function(Response, dynamic)? |
Callback sukses |
onError |
Function(DioException)? |
Callback error |
cacheKey |
String? |
Key cache |
cacheDuration |
Duration? |
Durasi cache |
Membuat API Service
Untuk membuat API service baru:
metro make:api_service user
Dengan model:
metro make:api_service user --model="User"
Ini membuat API service dengan method CRUD:
class UserApiService extends NyApiService {
...
Future<List<User>?> fetchAll({dynamic query}) async {
return await network<List<User>>(
request: (request) => request.get("/endpoint-path", queryParameters: query),
);
}
Future<User?> find({required int id}) async {
return await network<User>(
request: (request) => request.get("/endpoint-path/$id"),
);
}
Future<User?> create({required dynamic data}) async {
return await network<User>(
request: (request) => request.post("/endpoint-path", data: data),
);
}
Future<User?> update({dynamic query}) async {
return await network<User>(
request: (request) => request.put("/endpoint-path", queryParameters: query),
);
}
Future<bool?> delete({required int id}) async {
return await network<bool>(
request: (request) => request.delete("/endpoint-path/$id"),
);
}
}
Mengubah JSON ke Model
"Morphing" adalah istilah Nylo Website untuk konversi otomatis response JSON ke kelas model Dart Anda. Ketika Anda menggunakan network<User>(...), JSON response dilewatkan melalui decoder Anda untuk membuat instance User -- tanpa perlu parsing manual.
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext}) : super(buildContext, decoders: modelDecoders);
// Mengembalikan satu User
Future<User?> fetchUser() async {
return await network<User>(
request: (request) => request.get("/user/1"),
);
}
// Mengembalikan List User
Future<List<User>?> fetchUsers() async {
return await network<List<User>>(
request: (request) => request.get("/users"),
);
}
}
Decoder didefinisikan di lib/bootstrap/decoders.dart:
final Map<Type, dynamic> modelDecoders = {
User: (data) => User.fromJson(data),
List<User>: (data) =>
List.from(data).map((json) => User.fromJson(json)).toList(),
};
Parameter tipe yang Anda berikan ke network<T>() dicocokkan dengan map modelDecoders Anda untuk menemukan decoder yang tepat.
Lihat juga: Decoders untuk detail tentang mendaftarkan model decoder.
Menyimpan Cache Response
Simpan cache response untuk mengurangi panggilan API dan meningkatkan performa. Caching berguna untuk data yang tidak sering berubah, seperti daftar negara, kategori, atau konfigurasi.
Berikan cacheKey dan opsional cacheDuration:
Future<List<Country>> fetchCountries() async {
return await network<List<Country>>(
request: (request) => request.get("/countries"),
cacheKey: "app_countries",
cacheDuration: const Duration(hours: 1),
) ?? [];
}
Menghapus Cache
// Clear a specific cache key
await apiService.clearCache("app_countries");
// Clear all API cache
await apiService.clearAllCache();
Caching dengan Helper api()
api<ApiService>(
(request) => request.fetchCountries(),
cacheKey: "app_countries",
cacheDuration: const Duration(hours: 1),
);
Kebijakan Cache
Gunakan CachePolicy untuk kontrol yang lebih detail atas perilaku caching:
| Kebijakan | Deskripsi |
|---|---|
CachePolicy.networkOnly |
Selalu ambil dari jaringan (default) |
CachePolicy.cacheFirst |
Coba cache dulu, fallback ke jaringan |
CachePolicy.networkFirst |
Coba jaringan dulu, fallback ke cache |
CachePolicy.cacheOnly |
Hanya gunakan cache, error jika kosong |
CachePolicy.staleWhileRevalidate |
Kembalikan cache langsung, perbarui di background |
Penggunaan
Future<List<Country>> fetchCountries() async {
return await network<List<Country>>(
request: (request) => request.get("/countries"),
cacheKey: "app_countries",
cacheDuration: const Duration(hours: 1),
cachePolicy: CachePolicy.staleWhileRevalidate,
) ?? [];
}
Kapan Menggunakan Setiap Kebijakan
- cacheFirst -- Data yang jarang berubah. Mengembalikan data cache langsung, hanya mengambil dari jaringan jika cache kosong.
- networkFirst -- Data yang seharusnya segar jika memungkinkan. Mencoba jaringan dulu, fallback ke cache saat gagal.
- staleWhileRevalidate -- UI yang membutuhkan response langsung tetapi harus tetap diperbarui. Mengembalikan data cache, kemudian menyegarkan di background.
- cacheOnly -- Mode offline. Melempar error jika tidak ada data cache.
Catatan: Jika Anda memberikan
cacheKeyataucacheDurationtanpa menentukancachePolicy, kebijakan default adalahcacheFirst.
Mencoba Ulang Permintaan Gagal
Secara otomatis mencoba ulang permintaan yang gagal.
Retry Dasar
Future fetchUsers() async {
return await network(
request: (request) => request.get("/users"),
retry: 3,
);
}
Retry dengan Jeda
Future fetchUsers() async {
return await network(
request: (request) => request.get("/users"),
retry: 3,
retryDelay: Duration(seconds: 2),
);
}
Retry Bersyarat
Future fetchUsers() async {
return await network(
request: (request) => request.get("/users"),
retry: 3,
retryIf: (DioException dioException) {
// Only retry on server errors
return dioException.response?.statusCode == 500;
},
);
}
Retry Level Service
apiService.setRetry(3);
apiService.setRetryDelay(Duration(seconds: 2));
apiService.setRetryIf((dioException) => dioException.response?.statusCode == 500);
Pemeriksaan Konektivitas
Gagal cepat ketika perangkat offline alih-alih menunggu timeout.
Level Service
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext}) : super(buildContext, decoders: modelDecoders);
@override
bool get checkConnectivityBeforeRequest => true;
...
}
Per-Permintaan
await network(
request: (request) => request.get("/users"),
checkConnectivity: true,
);
Dinamis
apiService.setCheckConnectivityBeforeRequest(true);
Ketika diaktifkan dan perangkat offline:
- Kebijakan
networkFirstfallback ke cache - Kebijakan lain melempar
DioExceptionType.connectionErrorlangsung
Cancel Token
Kelola dan batalkan permintaan yang tertunda.
// Create a managed cancel token
final token = apiService.createCancelToken();
await apiService.get('/endpoint', cancelToken: token);
// Cancel all pending requests (e.g., on logout)
apiService.cancelAllRequests('User logged out');
// Check active request count
int count = apiService.activeRequestCount;
// Clean up a specific token when done
apiService.removeCancelToken(token);
Mengatur Header Auth
Override setAuthHeaders untuk melampirkan header autentikasi ke setiap permintaan. Method ini dipanggil sebelum setiap permintaan ketika shouldSetAuthHeaders bernilai true (default).
class ApiService extends NyApiService {
...
@override
Future<RequestHeaders> setAuthHeaders(RequestHeaders headers) async {
String? myAuthToken = Auth.data(field: 'token');
if (myAuthToken != null) {
headers.addBearerToken(myAuthToken);
}
return headers;
}
}
Menonaktifkan Header Auth
Untuk endpoint publik yang tidak membutuhkan autentikasi:
// Per-request
await network(
request: (request) => request.get("/public-endpoint"),
shouldSetAuthHeaders: false,
);
// Service-level
apiService.setShouldSetAuthHeaders(false);
Lihat juga: Autentikasi untuk detail tentang mengautentikasi pengguna dan menyimpan token.
Memperbarui Token
Override shouldRefreshToken dan refreshToken untuk menangani kedaluwarsa token. Method ini dipanggil sebelum setiap permintaan.
class ApiService extends NyApiService {
...
@override
Future<bool> shouldRefreshToken() async {
// Check if the token needs refreshing
return false;
}
@override
Future<void> refreshToken(Dio dio) async {
// Use the fresh Dio instance (no interceptors) to refresh the token
dynamic response = (await dio.post("https://example.com/refresh-token")).data;
// Save the new token to storage
await Auth.set((data) {
data['token'] = response['token'];
return data;
});
}
}
Parameter dio di refreshToken adalah instance Dio baru, terpisah dari instance utama service, untuk menghindari loop interceptor.
Singleton API Service
Secara default, helper api membuat instance baru setiap kali. Untuk menggunakan singleton, berikan instance alih-alih factory di config/decoders.dart:
final Map<Type, dynamic> apiDecoders = {
ApiService: () => ApiService(), // New instance each time
ApiService: ApiService(), // Singleton — same instance always
};
Konfigurasi Lanjutan
Inisialisasi Dio Kustom
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext}) : super(
buildContext,
decoders: modelDecoders,
initDio: (Dio dio) {
dio.options.validateStatus = (status) => status! < 500;
return dio;
},
);
}
Mengakses Instance Dio
Dio dioInstance = apiService.dio;
Response response = await dioInstance.request(
'/custom-endpoint',
options: Options(method: 'OPTIONS'),
);
Helper Paginasi
apiService.setPagination(
1,
paramPage: 'page',
paramPerPage: 'per_page',
perPage: '20',
);
Callback Event
apiService.onSuccess((response, data) {
print('Success: ${response.statusCode}');
});
apiService.onError((dioException) {
print('Error: ${dioException.message}');
});
Properti yang Dapat Di-override
| Properti | Tipe | Default | Deskripsi |
|---|---|---|---|
baseUrl |
String |
"" |
Base URL untuk semua permintaan |
interceptors |
Map<Type, Interceptor> |
{} |
Interceptor Dio |
decoders |
Map<Type, dynamic>? |
{} |
Decoder model untuk pengubahan JSON |
shouldSetAuthHeaders |
bool |
true |
Apakah memanggil setAuthHeaders sebelum permintaan |
retry |
int |
0 |
Percobaan retry default |
retryDelay |
Duration |
1 second |
Jeda default antar retry |
checkConnectivityBeforeRequest |
bool |
false |
Periksa konektivitas sebelum permintaan |