Integrasi API berarti aplikasi mengambil data dari server lewat HTTP, bukan dari data contoh di dalam aplikasi. Mulai pelajaran ini, daftar pesanan Laundry Bu Rini datang dari server Laravel di papan order Bu Rini, dan server cuma mengirim order milik akun yang login. Bayangin Mbak Wulan menelepon toko: "Cucian saya sudah selesai?" Dia harus tahu nomor yang benar, menyebut namanya supaya kasir tahu ordernya yang mana, dan siap kalau teleponnya gak diangkat, berdering terlalu lama, atau ternyata belum ada order. Request ke API sama persis: alamat, identitas (token), dan jawaban yang bisa lambat, gagal, atau kosong.
🧪 Tebakan cepat: Server Laravel jalan di laptop dengan
php artisan serve, alamatnyahttp://localhost:8000. Aplikasi di emulator Android memanggilfetch('http://localhost:8000/api/me/orders'). Apa yang terjadi? Jawabannya ada di bagian "Alamat server per lingkungan".
Kontrak API pesanan
Endpoint ini ditambahkan ke server papan order (Laravel, routes/api.php, di belakang middleware auth:sanctum):
| Request | Jawaban |
|---|---|
GET /api/me/orders |
200 {"data":[{"id":"13","nama":"Mbak Wulan","kg":2,"status":"Disetrika"}]}: cuma order aktif milik akun yang login |
GET /api/orders/13 |
200 {"data":{"id":"13", ...}}. Order milik pelanggan lain: 404 |
| tanpa token yang sah | 401 {"message":"Unauthenticated."} |
- Setiap request mengirim
Accept: application/jsondanAuthorization: Bearer <token>. Token didapat saat login. iddikirim sebagai string dankgdiambil dari kolomberat, supaya bentuknya sama persis dengantype Orderdisrc/data/orders.ts.- Tanpa
Accept: application/json, Laravel menganggap request-nya dari browser dan mencoba mengarahkan ke halaman login. Di API yang gak punya route bernamalogin, hasilnya error 500Route [login] not defined.
Alamat server per lingkungan
| Aplikasi jalan di | Alamat laptop | Kenapa |
|---|---|---|
| Emulator Android | http://10.0.2.2:8000 |
10.0.2.2 adalah alias ke 127.0.0.1 milik laptop. localhost di emulator = emulator itu sendiri |
| Simulator iOS | http://localhost:8000 |
simulator memakai jaringan Mac yang sama |
| HP asli, Wi-Fi sama | http://192.168.100.96:8000 |
IP laptop di Wi-Fi. Jalankan php artisan serve --host=0.0.0.0, karena bawaannya 127.0.0.1 cuma menerima dari laptop sendiri |
| Aplikasi rilis | https://api.laundryburini.id |
wajib HTTPS, lihat bagian terakhir |
Jadi dari emulator Android, localhost:8000 menunjuk ke emulator itu sendiri, gak ada server di sana, dan request-nya gagal tersambung. Itu jawaban tebakannya. Pakai 10.0.2.2. Alamatnya ditaruh di .env.local di root project:
# .env.local (gak masuk git)
EXPO_PUBLIC_API_URL=http://10.0.2.2:8000
Variabel berawalan EXPO_PUBLIC_ dibaca lewat process.env.EXPO_PUBLIC_API_URL dan nilainya ditanam ke dalam bundle JavaScript. Jadi jangan pernah menaruh rahasia di sana.
Mengambil pesanan: fetch dan TanStack Query
// src/lib/api.ts
import * as SecureStore from 'expo-secure-store';
import type { Order } from '@/data/orders';
const API_URL = process.env.EXPO_PUBLIC_API_URL ?? 'http://localhost:8000';
export async function ambilPesananSaya(): Promise<Order[]> {
const token = await SecureStore.getItemAsync('token'); // disimpan saat login
const res = await fetch(`${API_URL}/api/me/orders`, {
headers: { Accept: 'application/json', Authorization: `Bearer ${token}` },
signal: AbortSignal.timeout(10_000), // gak dijawab dalam 10 detik: batalkan
});
if (!res.ok) throw new Error(`Server menjawab ${res.status}`);
const body: { data: Order[] } = await res.json();
return body.data;
}
fetchsudah ada di React Native. Di Expo SDK 57,fetchglobal adalahexpo/fetch, dan Expo menambahkanAbortSignal.timeout.fetchgak punya batas waktu sendiri. Di Android, client HTTP bawaan React Native malah diset tanpa batas waktu, jadi server yang macet bisa membuat layar loading selamanya.AbortSignal.timeout(10_000)membatalkannya setelah 10 detik.fetchcuma gagal kalau koneksinya gagal. Jawaban401atau500tetap dianggap berhasil, jadi periksares.oksendiri.- Token disimpan di
expo-secure-store(Keychain di iOS, Keystore di Android), bukan di penyimpanan biasa. Detailnya di pelajaran penyimpanan lokal. - Axios juga jalan di React Native dan punya opsi
timeoutsendiri. Apa pun pilihannya, gabungkan dengan TanStack Query untuk cache dan retry.
// src/app/(tabs)/index.tsx
import { useQuery } from '@tanstack/react-query';
import { ActivityIndicator, Button, FlatList, Text, View } from 'react-native';
import { ambilPesananSaya } from '@/lib/api';
export default function Pesanan() {
const { data, isPending, isError, error, refetch, isRefetching } = useQuery({
queryKey: ['orders', 'saya'],
queryFn: ambilPesananSaya,
});
if (isPending) return <ActivityIndicator style={{ flex: 1 }} />;
if (isError) {
return (
<View style={{ flex: 1, justifyContent: 'center', padding: 16, gap: 12 }}>
<Text>Gagal memuat pesanan: {error.message}</Text>
<Button title="Coba lagi" onPress={() => refetch()} />
</View>
);
}
return (
<FlatList
data={data}
keyExtractor={(o) => o.id}
renderItem={({ item }) => <Text>#{item.id} {item.nama}: {item.status}</Text>}
refreshing={isRefetching}
onRefresh={refetch}
ListEmptyComponent={<Text>Belum ada order aktif.</Text>}
/>
);
}
Empat keadaan layar: loading (isPending), error (dengan tombol coba lagi), kosong (ListEmptyComponent), dan berisi. Saat request gagal, TanStack Query mencoba lagi 3 kali dengan jeda 1, 2, lalu 4 detik. Kalau server selalu menjawab 500:
| Detik ke | Yang terjadi |
|---|---|
| 0 | percobaan 1: 500 |
| 1 | percobaan 2: 500 |
| 3 | percobaan 3: 500 |
| 7 | percobaan 4: 500, isError jadi true, tampil "Gagal memuat pesanan: Server menjawab 500" |
Tarik untuk refresh, dan status yang berubah
refreshing dan onRefresh di FlatList membuat RefreshControl bawaan: tarik daftar ke bawah, spinner muncul, refetch dipanggil. Pakai isRefetching, bukan isLoading: isLoading cuma true di muatan pertama, jadi spinner tarik-untuk-refresh gak akan pernah tampil.
GET /api/me/orders 200 {"data":[{"id":"13","nama":"Mbak Wulan","kg":2,"status":"Disetrika"}]}
(Bu Rini memindahkan kartu #13 di papan)
(Mbak Wulan menarik daftar ke bawah)
GET /api/me/orders 200 {"data":[{"id":"13","nama":"Mbak Wulan","kg":2,"status":"Siap diambil"}]}
Status #13 berubah dari "Disetrika" ke "Siap diambil" tanpa menutup aplikasi. Cara lain: muat ulang saat aplikasi kembali ke depan (focusManager di pelajaran state), refetchInterval: 60_000 untuk tiap menit, atau kabar langsung dari server lewat realtime (track Realtime) dan push notification (pelajaran berikutnya).
HTTP polos diblokir di build rilis
- Android: sejak Android 9 (API 28), HTTP tanpa enkripsi (cleartext) dimatikan secara bawaan. Hasil
npx expo prebuildmenaruhandroid:usesCleartextTraffic="true"cuma di manifest debug, jadihttp://10.0.2.2:8000jalan saat development. Di build rilis, request yang sama ditolak:CLEARTEXT communication to 10.0.2.2 not permitted by network security policy. - iOS: App Transport Security (ATS) mewajibkan HTTPS.
Info.plisthasil prebuild berisiNSAllowsArbitraryLoads=falsedanNSAllowsLocalNetworking=true: HTTP kelocalhost, alamat IP, dan nama.localboleh, tapihttp://api.laundryburini.iddiblokir. - Jadi server sungguhan wajib HTTPS. HTTP polos cuma untuk server di laptop selama development.
⚠️ Jebakan yang sering ketemu
localhostdari emulator Android atau HP asli. Itu menunjuk ke HP-nya sendiri. Pakai10.0.2.2di emulator, IP laptop di HP asli (dengan--host=0.0.0.0).refreshing={isLoading}. Spinner tarik-untuk-refresh gak pernah muncul. PakaiisRefetching.- Gak memeriksa
res.ok. Jawaban401 {"message":"Unauthenticated."}dianggap berhasil, danbody.datajadiundefined. Lempar error kalau!res.ok. - Lupa
Accept: application/json. Laravel mengarahkan ke halaman login, dan API tanpa routeloginmenjawab 500. - Token di variabel
EXPO_PUBLIC_atau penyimpanan biasa. Isinya bisa dibaca dari bundle atau dari HP. Simpan token diexpo-secure-store.
TL;DR: Aplikasi mengambil pesanan Mbak Wulan dari GET /api/me/orders (cuma order akun yang login, {"data":[...]}), dengan Accept: application/json dan Authorization: Bearer <token> dari expo-secure-store. Alamat server per lingkungan lewat EXPO_PUBLIC_API_URL: 10.0.2.2:8000 dari emulator Android (localhost di emulator adalah emulator itu sendiri), localhost dari simulator iOS, IP laptop dari HP asli dengan php artisan serve --host=0.0.0.0. Cara yang direkomendasikan: fetch bawaan (atau axios) plus TanStack Query untuk cache dan retry (3 kali, jeda 1, 2, 4 detik). Periksa res.ok, pasang batas waktu dengan AbortSignal.timeout, tampilkan keadaan loading, error, dan kosong, dan pakai refreshing={isRefetching} dengan onRefresh={refetch} untuk tarik-untuk-refresh, yang mengubah #13 dari "Disetrika" ke "Siap diambil". HTTP polos cuma jalan di development: build rilis Android (sejak Android 9) dan iOS (ATS) memblokirnya, jadi server sungguhan wajib HTTPS.