Kode platform-specific adalah bagian kecil kode yang sengaja dibedakan untuk iOS dan Android, di tengah kode yang selebihnya dipakai bersama. Bayangin Bu Rini membuka cabang kedua di mal. Menu, harga, dan cara kerjanya sama persis dengan toko di ruko. Yang beda cuma hal yang diatur gedungnya: pintu geser di mal, pintu dorong di ruko. Bu Rini gak menulis buku panduan kedua, cukup satu halaman "khusus cabang mal". Begitu juga aplikasi Laundry Bu Rini: hampir semua kodenya sama, dan cuma beberapa baris yang tahu dia jalan di mana.
🧪 Tebakan cepat: Layar "Minta jemput" sudah berisi catatan untuk kurir, dan ada
BackHandleryang menanyakan "Batal minta jemput?" sebelum layar ditutup. Mbak Wulan memakai iPhone dan menggeser dari tepi kiri layar. Apakah pertanyaan itu muncul? Jawabannya ada di bagian "Back: tombol Android dan geser iOS".
Platform.OS dan Platform.select
import { Platform } from 'react-native';
Platform.OS; // 'ios' atau 'android' ('web' kalau dijalankan di browser)
Platform.Version; // Android: API level berupa angka, misalnya 35. iOS: string, misalnya '18.2'
const perluIzinNotif = Platform.OS === 'android' && Platform.Version >= 33; // Android 13 ke atas
const warnaLink = Platform.select({ ios: '#007aff', android: '#1a73e8', default: '#2563eb' });
Platform.OSuntuk satu-dua barisif.Platform.selectmemilih nilai sesuai platform. Urutannya: kunci platform itu (iosatauandroid), lalunative(untuk iOS dan Android sekaligus), laludefault.Platform.Versionbeda tipe: angka di Android, string di iOS. CekPlatform.OSdulu sebelum membandingkannya.
File .ios.tsx dan .android.tsx
Kalau implementasinya beda total, pisahkan per file. Contoh: tombol "Buka di peta" untuk alamat jemput.
// src/lib/bukaPeta.ios.ts: Apple Maps
import { Linking } from 'react-native';
export function bukaPeta(alamat: string) {
return Linking.openURL(`https://maps.apple.com/?q=${encodeURIComponent(alamat)}`);
}
// src/lib/bukaPeta.android.ts: aplikasi peta bawaan Android
import { Linking } from 'react-native';
export function bukaPeta(alamat: string) {
return Linking.openURL(`geo:0,0?q=${encodeURIComponent(alamat)}`);
}
// src/lib/bukaPeta.ts: cadangan untuk web, dan file yang dibaca TypeScript
import { Linking } from 'react-native';
export function bukaPeta(alamat: string) {
return Linking.openURL(`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(alamat)}`);
}
Semua layar cukup menulis import { bukaPeta } from '@/lib/bukaPeta'. Metro memilih file saat membuat bundle: untuk iOS dia mencari .ios.ts, lalu .native.ts, lalu .ts. Dicek lewat Metro di project Expo SDK 57: bundle iOS cuma berisi maps.apple.com, bundle Android cuma geo:0,0, dan bundle web cuma google.com/maps. Template Expo sendiri memakai cara ini, misalnya app-tabs.web.tsx. File .ts tanpa akhiran tetap perlu, karena TypeScript gak mengenal platform dan mencari file itu.
Pilihannya: beberapa nilai yang beda, pakai Platform.select. Seluruh fungsi atau komponen yang beda, pakai file .ios dan .android.
Bayangan dan font
const styles = StyleSheet.create({
kartu: {
padding: 16,
borderRadius: 12,
backgroundColor: '#fff',
...Platform.select({
ios: { shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.15, shadowRadius: 6 },
android: { elevation: 3 },
}),
},
// atau satu baris yang sama untuk kedua platform:
kartuBaru: { boxShadow: '0 2px 6px rgba(0, 0, 0, 0.15)' },
});
| Style | iOS | Android |
|---|---|---|
shadowColor, shadowOffset, shadowOpacity, shadowRadius |
bayangan | cuma shadowColor yang dipakai (Android 9 ke atas), untuk mewarnai bayangan elevation |
elevation: 3 |
gak ada efek | bayangan bawaan Android |
boxShadow |
bayangan | bayangan (Android 9 ke atas) |
boxShadow ditulis seperti CSS dan hadir sejak React Native 0.76, khusus New Architecture. Sejak 0.82 New Architecture jadi satu-satunya, jadi di Expo SDK 57 dia jalan di kedua platform.
Font juga beda. Tanpa fontFamily, iOS memakai San Francisco dan Android memakai Roboto. Lebar huruf keduanya gak sama, jadi teks yang sama bisa lebih panjang di salah satu HP. Itu alasan lain memberi flex: 1 di bagian yang boleh menyempit.
Back: tombol Android dan geser iOS
Android punya back sistem (tombol atau gestur). iOS gak punya: kembali lewat tombol di header atau geser dari tepi kiri. BackHandler cuma mendengar back sistem Android.
// src/app/jemput.tsx
import { router, Stack, useFocusEffect } from 'expo-router';
import { useCallback, useState } from 'react';
import { Alert, BackHandler, TextInput, View } from 'react-native';
export default function MintaJemput() {
const [catatan, setCatatan] = useState('');
const adaIsian = catatan.length > 0;
useFocusEffect(
useCallback(() => {
const sub = BackHandler.addEventListener('hardwareBackPress', () => {
if (!adaIsian) return false; // biarkan navigasi menutup layar
Alert.alert('Batal minta jemput?', 'Catatan yang belum dikirim akan hilang.', [
{ text: 'Lanjut isi', style: 'cancel' },
{ text: 'Buang', style: 'destructive', onPress: () => router.back() },
]);
return true; // sudah ditangani: layar gak ditutup
});
return () => sub.remove();
}, [adaIsian]),
);
return (
<View style={{ flex: 1, padding: 16 }}>
<Stack.Screen options={{ title: 'Minta jemput', gestureEnabled: !adaIsian }} />
<TextInput value={catatan} onChangeText={setCatatan} placeholder="Catatan untuk kurir" />
</View>
);
}
BackHandler.addEventListenermengembalikan langganan, dansub.remove()melepasnya. Listener yang dipasang paling akhir dipanggil paling dulu.return truemenghentikan back,return falsemeneruskannya ke navigasi.useFocusEffectmemasang listener cuma selama layar ini tampil.gestureEnabled: !adaIsianmematikan geser dari tepi kiri selama ada isian. Opsi ini cuma berlaku di iOS.
Di iPhone, geser dari tepi kiri ditangani navigasi native, bukan BackHandler. Jadi tanpa gestureEnabled: false, layarnya langsung tertutup dan catatannya hilang tanpa pertanyaan. Itu jawaban tebakannya. Tombol back di header juga bukan back sistem; navigasi punya event beforeRemove untuk mencegat semua jalan keluar sekaligus.
Status bar dan izin
<StatusBar style="auto" />dariexpo-status-barmemberi teks status bar gelap di mode terang dan terang di mode gelap, di kedua platform. Warna latar status bar gak diatur lagi: di Android, aplikasi Expo SDK 57 digambar sampai tepi layar, danbackgroundColormilikStatusBarReact Native gak berefek mulai Android 15. Jaga isi layar dengan safe area.- Izin kamera, notifikasi, dan lokasi diminta lewat dialog sistem, dan aturannya beda:
| Mbak Wulan mengetuk "Jangan izinkan" | iOS | Android |
|---|---|---|
| Pertama kali | dialog gak akan muncul lagi, canAskAgain: false |
dialog masih boleh muncul sekali lagi |
| Kedua kali | (dialog sudah gak muncul) | sejak Android 11, dialog gak muncul lagi, canAskAgain: false |
// src/components/FotoNota.tsx, isi komponennya
const [izin, mintaIzin] = useCameraPermissions(); // dari expo-camera
if (!izin) return null; // status izin belum terbaca
if (izin.granted) return <Text>Kamera siap.</Text>;
if (!izin.canAskAgain) return <Button title="Buka Pengaturan" onPress={() => Linking.openSettings()} />;
return <Button title="Izinkan kamera untuk memotret nota" onPress={mintaIzin} />;
Di iOS, setiap izin juga wajib punya kalimat alasan di Info.plist (misalnya NSCameraUsageDescription). Notifikasi di Android 13 (API 33) ke atas juga butuh izin, itu sebabnya ada perluIzinNotif tadi.
Yang disembunyikan modul Expo: expo-status-bar dan useCameraPermissions memberi satu API untuk dua platform, dan config plugin menulis Info.plist serta AndroidManifest.xml untukmu (pelajaran berikutnya). Yang gak disembunyikan: cara kembali, aturan izin, bayangan, font, dan kebiasaan tampilan masing-masing (Material Design di Android, Human Interface Guidelines di iOS).
⚠️ Jebakan yang sering ketemu
- Bayangan cuma untuk satu platform.
elevationsaja: iPhone tanpa bayangan.shadow*saja: Android tanpa bayangan. PakaiPlatform.selectatauboxShadow. - Mengandalkan BackHandler untuk semua cara kembali. Dia cuma mendengar back sistem Android. Geser iOS diatur lewat
gestureEnabled, tombol header lewat navigasi. - Membandingkan
Platform.Versiontanpa cek platform. Di iOS nilainya string seperti'18.2'. TulisPlatform.OS === 'android' && Platform.Version >= 33. - Minta izin begitu aplikasi dibuka. Di iOS dialognya cuma sekali. Minta saat dibutuhkan, misalnya saat Mbak Wulan mengetuk "Foto nota", dengan penjelasan dulu.
TL;DR: Kode React Native sebagian besar dipakai bersama, dan bagian yang beda ditulis dengan Platform.OS ('ios' atau 'android'), Platform.select({ ios, android, native, default }), atau file .ios.tsx dan .android.tsx yang dipilih Metro saat bundle (plus file tanpa akhiran untuk web dan TypeScript). Bayangan: shadow* di iOS, elevation di Android, atau boxShadow untuk keduanya (sejak 0.76, New Architecture). BackHandler cuma untuk back sistem Android (return true menghentikannya); geser dari tepi kiri iOS dimatikan dengan gestureEnabled: false. expo-status-bar dengan style="auto" mengurus warna teks status bar. Izin: iOS cuma bertanya sekali, Android dua kali sejak Android 11; setelah itu canAskAgain: false dan tinggal Linking.openSettings().