Menghubungkan aplikasi mobile ke backend: REST & GraphQL client di tiap stack, serialization, error handling & retry dengan exponential backoff, serta caching — sambil mengintegrasikan API workout Fitku secara nyata

Aplikasi mobile yang berguna hampir selalu butuh data dari server: katalog workout, profil pengguna, riwayat langkah. Di episode 10 ini kita belajar networking & API integration — menghubungkan Fitku ke backend lewat REST/GraphQL dengan benar, menangani error dengan anggun, melakukan retry yang cerdas, dan caching agar aplikasi terasa cepat.
Mengapa topik ini sering jadi sumber bug? Karena di jaringan seluler, koneksi tidak bisa diandalkan: sinyal lemah, latency tinggi, timeout, dan server yang sedang turun. Aplikasi yang hanya "memanggil API" tanpa menangani kegagalan akan crash atau menampilkan layar kosong — reputasi langsung hancur.
REST adalah standar paling umum: resource sebagai URL + method HTTP. GraphQL memakai satu endpoint dengan query yang meminta field spesifik.
GET /api/v1/workouts → daftar workout
GET /api/v1/workouts/42 → detail workout
POST /api/v1/workouts → buat workout baru
PUT /api/v1/workouts/42 → update workout
DELETE /api/v1/workouts/42 → hapus workoutquery TodaySummary {
todaySteps { total }
workouts(date: TODAY) { id name durationMinutes }
}Pilih REST untuk kesederhanaan dan kompatibilitas; GraphQL jika data bertingkat dan over-fetching jadi masalah nyata. Untuk Fitku, REST cukup — kita bisa tambah GraphQL nanti.
interface FitkuApi {
@GET("workouts")
suspend fun fetchWorkouts(@Query("userId") userId: String): List<WorkoutDto>
@POST("workouts")
suspend fun uploadWorkout(@Body workout: WorkoutDto): WorkoutDto
}
// Setup
val api = Retrofit.Builder()
.baseUrl("https://api.fitku.dev/")
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.client(OkHttpClient.Builder()
.addInterceptor(AuthInterceptor(token))
.build())
.build()
.create(FitkuApi::class.java)import 'package:dio/dio.dart';
final dio = Dio(BaseOptions(baseUrl: 'https://api.fitku.dev/'))
..interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
options.headers['Authorization'] = 'Bearer $token';
handler.next(options);
},
));
Future<List<Workout>> fetchWorkouts(String userId) async {
final res = await dio.get<List<dynamic>>('/workouts',
queryParameters: {'userId': userId});
return res.data!
.map((e) => Workout.fromJson(e as Map<String, dynamic>))
.toList();
}const API = "https://api.fitku.dev";
async function fetchWorkouts(userId: string): Promise<Workout[]> {
const res = await fetch(`${API}/workouts?userId=${userId}`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new ApiError(res.status);
return res.json();
}Untuk RN, kombinasikan dengan TanStack Query: ia memberi caching, retry, dan stale-while-revalidate secara gratis.
Note
Serialization wajib dipisahkan dari model domain: WorkoutDto (JSON) vs Workout (domain). Di Android pakai kotlinx.serialization, di Flutter json_serializable, di RN cukup zod untuk validasi runtime. Mencegah error akibat field yang berubah dari server.
Error API perlu dikategorikan:
suspend fun <T> retryWithBackoff(
maxAttempts: Int = 3,
block: suspend () -> T,
): T {
var attempt = 0
while (true) {
try {
return block()
} catch (e: RetryableException) {
attempt++
if (attempt >= maxAttempts) throw e
delay(200L * (1L shl attempt)) // 200ms, 400ms, 800ms
}
}
}Di TanStack Query, retry dan retryDelay dikonfigurasi langsung; di Flutter, Dio punya dio.httpClientAdapter dan kita bisa memakai retry_dio package.
Selalu set timeout — tanpa timeout, request yang menggantung membuat UI "beku" terasa:
connectTimeout: 10s → koneksi ke server
receiveTimeout: 15s → menerima respons (besar bisa lebih lama)Caching adalah kunci UX mobile: data yang pernah dimuat tidak perlu dimuat ulang. Strategi:
Cache-Control/ETag dari server untuk respons yang immutable.First open → muat dari network → simpan ke cache
Second open → tampilkan cache dulu (instan) → refresh di background
Offline → tampilkan cache (episode 23)Tip
Pola stale-while-revalidate adalah standar UX modern: tampilkan data lama seketika, lalu update di latar belakang. TanStack Query, Riverpod, dan banyak library 2026 sudah menerapkannya. Aplikasi terasa instan tanpa menunggu network.
Mari lengkapi repository dari episode 8 dengan error handling nyata:
class WorkoutRepositoryImpl(
private val api: FitkuApi,
private val localDb: WorkoutDao,
) : WorkoutRepository {
override suspend fun fetchWorkouts(userId: String): List<Workout> {
return try {
retryWithBackoff { api.fetchWorkouts(userId) }
.map(WorkoutDto::toDomain)
.also { localDb.insertAll(it) }
} catch (e: RetryableException) {
localDb.fetchCached() // fallback ke cache saat offline/server down
?: throw ApiUnavailable("Server sedang bermasalah, coba lagi nanti")
} catch (e: ApiError) {
throw UserFacingError(e.message ?: "Terjadi kesalahan")
}
}
}ViewModel menangkap error, memetakannya ke UiState.error, dan UI menampilkan pesan + tombol coba lagi.
Warning
Jangan pernah menampilkan error mentah dari server ke pengguna ("500 Internal Server Error"). Map dulu ke pesan yang manusiawi, dan jangan log data sensitif (token, PII) ke konsol produksi.
Pada episode 10 ini, kalian telah menghubungkan Fitku ke dunia luar:
Di episode 11 selanjutnya kita simpan data secara permanen: Local Storage & Database — SQLite/Room, Realm, dan preferences, dengan strategi offline-first untuk Fitku. Sampai jumpa!