Flutter App Core (Chapter 2 API Services)

Agis R Herdiana
7 min readJul 20, 2023

--

Pada artikel kali ini kita akan membuat sebuah helper API Services yang akan di gunakan setiap kali kita request API ke server, biasanya fungsi ini di pakai dalam sebuah Repositories. Seperti yang kita ketahui, dalam sebuah API ada beberapa jenis method pemanggilan, yaitu : GET, POST, PUT dan DELETE. Dalam projek kita kali ini akan menggunakan package dari pihak ketiga untuk service API-nya yaitu menggunakan Dio yang sudah familiar di kalangan Flutter Developer. Selain itu kita juga akan membuat beberapa tambahan fungsi lain seperti interceptor, connection status, dll. Untuk package-nya bisa di akses disini https://pub.dev/packages/dio.

Add Package

Untuk mulai membuat API Services Helper pertama kita tambahkan dulu package Dio dengan menggunakan command di terminal, yaitu :

flutter pub add dio

Write Code

Setelah package di install kita lanjutkan membuat file baru bernama api_service.dart di dalam folder config yang sebelumnya kita buat, kurang lebih seperti gambar di bawah ini :

Setelah selesai, buka file api_service.dart dan tuliskan kode seperti di bawah ini ke dalam file tersebut.

import 'package:dio/dio.dart';

enum MethodRequest { POST, GET, PUT, DELETE }

class ApiService {
final Dio _dio = Dio();

ApiService(String baseUrl) {
_dio.options.baseUrl = baseUrl;
_dio.options.connectTimeout = const Duration(seconds: 90);
_dio.options.receiveTimeout = const Duration(seconds: 50);
_dio.options.headers = {
'Accept': 'application/json',
};
_dio.options.receiveDataWhenStatusError = true;
}

Future<Response> call(
String url, {
MethodRequest method = MethodRequest.POST,
dynamic request,
Map<String, String>? header,
String? token,
bool useFormData = false,
}) async {
if (header != null) {
_dio.options.headers = header;
}
if (token != null) {
if (header != null) {
header.addAll({'Authorization': 'Bearer $token'});
_dio.options.headers = header;
} else {
_dio.options.headers = {
'Accept': 'application/json',
'Authorization': 'Bearer $token'
};
}
}

try {
Response response;
switch (method) {
case MethodRequest.GET:
response = await _dio.get(url, queryParameters: request);
break;
case MethodRequest.PUT:
response = await _dio.put(
url,
data: useFormData ? FormData.fromMap(request!) : request,
);
break;
case MethodRequest.DELETE:
response = await _dio.delete(
url,
data: useFormData ? FormData.fromMap(request!) : request,
);
break;
default:
response = await _dio.post(
url,
data: useFormData ? FormData.fromMap(request!) : request,
);
}
return response;
} on DioException catch (e) {
if (e.response?.data is Map) {
if (!(e.response?.data as Map).containsKey("message")) {
(e.response?.data as Map).addAll(<String, dynamic>{
"message":
"Terjadi kesalahan, silahkan coba dalam beberapa saat lagi.",
});
}

return e.response!;
} else {
Response response = Response(
data: {
"message":
"Terjadi kesalahan, silahkan coba dalam beberapa saat lagi.",
},
requestOptions: e.requestOptions,
statusCode: e.response?.statusCode,
);
return response;
}
}
}
}

Jadi penjelasan kode di atas adalah :

  1. Connection timeout durasinya 90 detik, dan receive timeout 50 detik.
  2. Fungsi call berisi beberapa parameter yang bisa di isi yaitu url sebagai endpoint yang akan kita akses.
  3. Parameter method yaitu berupa tipe request yang di pakai (GET, POST, DELETE), secara otomatis apabila parameternya tidak diisi maka akan menggunakan tipe POST.
  4. Parameter request yaitu data yang kita kirim dari body.
  5. Parameter header digunakan apabila ada tambahan informasi bagi server untuk memproses permintaan API, bisa berupa secret key, Authentikasi dll.
  6. token, parameter ini diisi jika ada token yang perlu dikirim, dalam contoh kasusu ini kita menggunakan JWT Authentication.
  7. Parameter useFormData secara default berisi false, yang artinya data yang kita terima berupa json, tetapi jika diisi true maka datanya akan berupa isian data form.

Connection Status

Adakalanya ketika user melakukan request ke server terkendala dengan koneksi internet mereka, maka dari itu kita harus memberitahu user bahwa koneksi internet mereka terputus. Untuk itu kita akan membuat sebuah file yang bernama connectivity_status.dart di dalam folder yang sama, yaitu folder config, yang nantinya akan kita panggil dari API Service yang telah kita buat. Beriktu kode tersebut :

import 'dart:io';

class ConnectivityStatus {
static Future<bool> hasNetwork() async {
try {
final result = await InternetAddress.lookup('example.com');
return result.isNotEmpty && result[0].rawAddress.isNotEmpty;
} on SocketException catch (_) {
return false;
}
}
}

Dalam kode diatas kita akan melakukan pengecekan koneksi ke domain example.com untuk mengetahui apakah user terkoneksi dengan internet.

Berikut kode yang sudah di tambahakan pengecekan koneksi internet :

import 'package:apps_core/config/connectivity_status.dart';
import 'package:dio/dio.dart';

enum MethodRequest { POST, GET, PUT, DELETE }

class ApiService {
final Dio _dio = Dio();

ApiService(String baseUrl) {
_dio.options.baseUrl = baseUrl;
_dio.options.connectTimeout = const Duration(seconds: 90);
_dio.options.receiveTimeout = const Duration(seconds: 50);
_dio.options.headers = {
'Accept': 'application/json',
};
_dio.options.receiveDataWhenStatusError = true;
}

Future<Response> call(
String url, {
MethodRequest method = MethodRequest.POST,
dynamic request,
Map<String, String>? header,
String? token,
bool useFormData = false,
}) async {

// Check Internet Connection
bool isOnline = await ConnectivityStatus.hasNetwork();
if (isOnline != true) {
Response response = Response(
data: {
"message": "Tidak terhubung ke jaringan",
},
statusCode: 00,
requestOptions: RequestOptions(path: ''),
);
return response;
}

if (header != null) {
_dio.options.headers = header;
}
if (token != null) {
if (header != null) {
header.addAll({'Authorization': 'Bearer $token'});
_dio.options.headers = header;
} else {
_dio.options.headers = {
'Accept': 'application/json',
'Authorization': 'Bearer $token'
};
}
}

try {
Response response;
switch (method) {
case MethodRequest.GET:
response = await _dio.get(url, queryParameters: request);
break;
case MethodRequest.PUT:
response = await _dio.put(
url,
data: useFormData ? FormData.fromMap(request!) : request,
);
break;
case MethodRequest.DELETE:
response = await _dio.delete(
url,
data: useFormData ? FormData.fromMap(request!) : request,
);
break;
default:
response = await _dio.post(
url,
data: useFormData ? FormData.fromMap(request!) : request,
);
}
return response;
} on DioException catch (e) {
if (e.response?.data is Map) {
if (!(e.response?.data as Map).containsKey("message")) {
(e.response?.data as Map).addAll(<String, dynamic>{
"message":
"Terjadi kesalahan, silahkan coba dalam beberapa saat lagi.",
});
}

return e.response!;
} else {
Response response = Response(
data: {
"message":
"Terjadi kesalahan, silahkan coba dalam beberapa saat lagi.",
},
requestOptions: e.requestOptions,
statusCode: e.response?.statusCode,
);
return response;
}
}
}
}

Interceptor

Interceptor digunakan untuk melakukan pra-proses atau pasca-proses panggilan API, funsgi ini membantu penanganan kesalahan global, autentikasi, logging, dan lainnya. Dalam kasus kali ini di karenakan sebagian besar projek kita menggunakan token JWT, dimana token tersebut memiliki masa kadaluwarsa, sehingga mengharuskan kita untuk memperbaharuinya ketika tokennya sudah expired. Maka dari itu kita akan gunakan interceptor untuk request refresh token.

Untuk kodenya bisa dilihat seperti kode di bawah :

import 'package:apps_core/config/connectivity_status.dart';
import 'package:dio/dio.dart';

enum MethodRequest { POST, GET, PUT, DELETE }

class ApiService {
final Dio _dio = Dio();

ApiService(String baseUrl) {
_dio.options.baseUrl = baseUrl;
_dio.options.connectTimeout = const Duration(seconds: 90);
_dio.options.receiveTimeout = const Duration(seconds: 50);
_dio.options.headers = {
'Accept': 'application/json',
};
_dio.options.receiveDataWhenStatusError = true;

//Interceptors
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
options.headers.addAll(
{
if (!options.headers.containsKey("Accept"))
'Accept': 'application/json',
},
);

return handler.next(options);
},
onError: (DioException e, handler) async {
if (e.response?.statusCode == 401) {
// If a 401 response is received, refresh the access token
String newAccessToken = "";
// Update the request header with the new access token
e.requestOptions.headers = {
'Accept': 'application/json',
'Authorization': 'Bearer $newAccessToken'
};
return handler.resolve(await _dio.fetch(e.requestOptions));
}
return handler.next(e);
},
),
);
}

Future<Response> call(
String url, {
MethodRequest method = MethodRequest.POST,
dynamic request,
Map<String, String>? header,
String? token,
bool useFormData = false,
}) async {
// Check Internet Connection
bool isOnline = await ConnectivityStatus.hasNetwork();
if (isOnline != true) {
Response response = Response(
data: {
"message": "Tidak terhubung ke jaringan",
},
statusCode: 00,
requestOptions: RequestOptions(path: ''),
);
return response;
}

if (header != null) {
_dio.options.headers = header;
}
if (token != null) {
if (header != null) {
header.addAll({'Authorization': 'Bearer $token'});
_dio.options.headers = header;
} else {
_dio.options.headers = {
'Accept': 'application/json',
'Authorization': 'Bearer $token'
};
}
}

try {
Response response;
switch (method) {
case MethodRequest.GET:
response = await _dio.get(url, queryParameters: request);
break;
case MethodRequest.PUT:
response = await _dio.put(
url,
data: useFormData ? FormData.fromMap(request!) : request,
);
break;
case MethodRequest.DELETE:
response = await _dio.delete(
url,
data: useFormData ? FormData.fromMap(request!) : request,
);
break;
default:
response = await _dio.post(
url,
data: useFormData ? FormData.fromMap(request!) : request,
);
}
return response;
} on DioException catch (e) {
if (e.response?.data is Map) {
if (!(e.response?.data as Map).containsKey("message")) {
(e.response?.data as Map).addAll(<String, dynamic>{
"message":
"Terjadi kesalahan, silahkan coba dalam beberapa saat lagi.",
});
}

return e.response!;
} else {
Response response = Response(
data: {
"message":
"Terjadi kesalahan, silahkan coba dalam beberapa saat lagi.",
},
requestOptions: e.requestOptions,
statusCode: e.response?.statusCode,
);
return response;
}
}
}
}

Untuk penjelasan kode di atas kita gunakan respon kode 401 sebagai tanda bahwa token yang kita gunakan sudah expired. Kemudian silahkan sesuaikan variabel String newToken dengan fungsi request token kalian.

Setelah API Service dan beberapa fungsi tambahan telah selesai kita buat, kali ini saya akan coba jelaskan fungsi penggunaanya, dimana kita akan membuat sebuah repositories baru seperti kode di bawah ini :

import 'environment.dart';
import 'package:apps_core/config/api_service.dart';

class AuthRepo {
static ApiService apiService = ApiService(EnvironmentConfig.baseUrl());

static Future<dynamic> login({required dynamic jsonData}) async {
var result = await apiService.call(
"auth/login",
method: MethodRequest.POST,
header: {"secret_key": "your_secret_key"},
token: "your_token",
request: jsonData,
);
return result;
}
}

Variable apiService kita inisialisasikan dengan sebuah url yang kita buat di environtment.dart, kemudain fungsi login membutuhkan sebuah data berupa json yang di tampung dalam variabel jsonData. Kemudian kita juga tambahkan beberapa informasi dalam fungsi apiService.call() yaitu url endpoint, method, header, token dan request.

Kesimpulan

Dengan adanya API Service Helper dapat memudahkan kita dalam melakukan request ke server, mengatur konfigurasi request data dan juga menghindari penulisan kode secara berulang, dan yang terpenting kita bisa melakukan maintenance dengan mudah apabila terjadi masalah pada package yang kita pakai dari pihak ketiga.

--

--

Agis R Herdiana
Agis R Herdiana

No responses yet