Basics

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 cacheKey atau cacheDuration tanpa menentukan cachePolicy, kebijakan default adalah cacheFirst.

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 networkFirst fallback ke cache
  • Kebijakan lain melempar DioExceptionType.connectionError langsung

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