1. Pengenalan Expo & React Native
Expo adalah platform dan toolkit yang dibangun di atas React Native untuk memudahkan pengembangan aplikasi mobile cross-platform. Dengan Expo, Anda bisa membangun aplikasi iOS dan Android menggunakan JavaScript/TypeScript dan React β tanpa perlu Xcode atau Android Studio untuk development.
React Native sendiri dikembangkan oleh Meta (Facebook) dan memungkinkan developer web membangun aplikasi native menggunakan React. Expo menambahkan layer kemudahan di atas React Native: managed workflow yang abstraksi dari kompleksitas native, ratusan API bawaan, dan tooling modern seperti EAS Build dan OTA Updates.
Keunggulan Expo
| Keunggulan | Penjelasan |
|---|---|
| Zero Config | Tidak perlu konfigurasi native β langsung coding dan jalan di device |
| Expo Go | Test langsung di device fisik tanpa build β scan QR code, langsung jalan |
| OTA Updates | Push update JavaScript ke user tanpa review store β instant deployment |
| EAS Build | Cloud build untuk iOS & Android β tidak perlu Mac untuk build iOS |
| Rich API | 200+ module bawaan: kamera, maps, notifications, file system, dan lainnya |
| TypeScript | Support TypeScript out of the box |
| Web Support | Bisa juga dijalankan di web dengan expo-web |
Expo vs React Native CLI vs Alternatif
| Aspek | Expo | React Native CLI | Flutter |
|---|---|---|---|
| Bahasa | JavaScript/TypeScript | JavaScript/TypeScript | Dart |
| Setup | π’ Sangat mudah | π‘ Sedang | π‘ Sedang |
| Native Code | Managed: tidak bisa, Bare: bisa | Full akses | Full akses |
| OTA Updates | β Built-in | β Perlu CodePush | β οΈ Terbatas |
| Build | EAS Build (cloud) | Local build | Local / CI/CD |
| Ukuran App | Lebih besar | Lebih kecil | Sedang |
| Library Ecosystem | Expo modules + npm | Seluruh npm | pub.dev |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β YOUR JAVASCRIPT CODE β β βββββββββββββββββββββββββββββββββββββββββββββββββββ β β β React Components + Expo Modules β β β βββββββββββββββββββββββββ¬ββββββββββββββββββββββββββ β β βΌ β β βββββββββββββββββββββββββββββββββββββββββββββββββββ β β β Expo SDK (200+ modules) β β β β ββββββββββ ββββββββββββ ββββββββββ ββββββββββ β β β β βCamera β βNotificationsβ βMaps β βFile Sysβ β β β β ββββββββββ ββββββ¬ββββββ ββββββββββ ββββββββββ β β β βββββββββββββββββββββΌββββββββββββββββββββββββββββββ β β βΌ β β βββββββββββββββββββββββββββββββββββββββββββββββββββ β β β React Native (Bridge β Native modules) β β β βββββββββββββββββββββββββ¬ββββββββββββββββββββββββββ β β βΌ β β βββββββββββββββββββββββββββββββββββββββββββββββββββ β β β Native Platform (iOS / Android / Web) β β β βββββββββββββββββββββββββββββββββββββββββββββββββββ β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
2. Setup & Proyek Baru
# Prasyarat: Node.js 18+ dan npm/yarn # 1. Install Expo CLI npm install -g expo-cli # Atau gunakan npx (tanpa install global) npx create-expo-app # 2. Buat proyek baru npx create-expo-app@latest my-app cd my-app # 3. Jalankan development server npx expo start # Output: # βΊ Metro waiting on exp://192.168.1.100:8081 # βΊ Scan the QR code above with Expo Go (Android) # or the Camera app (iOS) # 4. Test di device: # - Android: Install "Expo Go" dari Play Store, scan QR # - iOS: Buka Camera, scan QR code # 5. Jalankan di emulator npx expo start --android # Android emulator npx expo start --ios # iOS simulator (perlu Mac) # 6. Jalankan di web npx expo start --web # Struktur proyek: # my-app/ # βββ app/ # File-based routing (Expo Router) # β βββ (tabs)/ # Tab navigation # β β βββ index.tsx # Home tab # β β βββ explore.tsx # Explore tab # β βββ _layout.tsx # Root layout # β βββ +not-found.tsx # 404 page # βββ assets/ # Gambar, font, ikon # βββ components/ # Komponen reusable # βββ constants/ # Konstanta aplikasi # βββ hooks/ # Custom hooks # βββ app.json # Konfigurasi Expo # βββ package.json
Konfigurasi app.json
{
"expo": {
"name": "My Awesome App",
"slug": "my-awesome-app",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.beebane.myapp",
"buildNumber": "1"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.beebane.myapp",
"versionCode": 1
},
"plugins": [
"expo-router",
[
"expo-camera",
{ "cameraPermission": "Izinkan aplikasi mengakses kamera" }
],
[
"expo-location",
{ "locationAlwaysAndWhenInUsePermission": "Izinkan akses lokasi" }
]
],
"extra": {
"eas": {
"projectId": "your-project-id"
}
}
}
}
3. Managed vs Bare Workflow
Expo menyediakan dua workflow utama yang bisa dipilih sesuai kebutuhan proyek:
| Aspek | Managed Workflow | Bare Workflow |
|---|---|---|
| Konfigurasi native | Dikelola sepenuhnya oleh Expo | Anda yang mengelola (seperti RN CLI) |
| Xcode/Android Studio | Tidak perlu untuk development | Perlu |
| Native dependencies | Hanya yang ada di Expo SDK | Bebas menambahkan apapun |
| Expo Go | β Bisa digunakan | β Tidak bisa |
| OTA Updates | β Full support | β Dengan konfigurasi |
| Build | EAS Build | EAS Build atau local build |
| Cocok untuk | Sebagian besar aplikasi | Aplikasi dengan native module kustom |
Mulailah dengan Managed Workflow untuk sebagian besar proyek. Expo sekarang mendukung development builds yang memungkinkan Anda menambahkan native module kustom tanpa keluar dari ekosistem Expo. Hanya pindah ke Bare Workflow jika benar-benar perlu memodifikasi kode native secara langsung.
4. Core Components & Styling
import React, { useState } from 'react';
import {
View, Text, StyleSheet, ScrollView,
Image, TouchableOpacity, TextInput,
FlatList, SectionList, ActivityIndicator,
Alert, Platform, SafeAreaView
} from 'react-native';
// ===== CORE COMPONENTS =====
// Text β Menampilkan teks
function TextExample() {
return (
<View>
<Text style={styles.heading}>Halo, Expo!</Text>
<Text style={styles.body}>
Ini adalah paragraf dengan{' '}
<Text style={{ fontWeight: 'bold' }}>teks tebal</Text> di dalamnya.
</Text>
<Text numberOfLines={2} ellipsizeMode="tail">
Teks panjang yang akan dipotong setelah dua baris...
</Text>
</View>
);
}
// Image β Menampilkan gambar
function ImageExample() {
return (
<View>
{/* Dari asset lokal */}
<Image
source={require('../assets/logo.png')}
style={{ width: 100, height: 100 }}
/>
{/* Dari URL */}
<Image
source={{ uri: 'https://picsum.photos/200' }}
style={{ width: 200, height: 200, borderRadius: 12 }}
resizeMode="cover"
/>
</View>
);
}
// TextInput β Input teks
function InputExample() {
const [text, setText] = useState('');
return (
<View>
<TextInput
style={styles.input}
placeholder="Ketik sesuatu..."
placeholderTextColor="#999"
value={text}
onChangeText={setText}
autoCapitalize="none"
keyboardType="default"
/>
<Text>Anda mengetik: {text}</Text>
</View>
);
}
// FlatList β List performant
function ListExample() {
const data = [
{ id: '1', name: 'Budi', role: 'Developer' },
{ id: '2', name: 'Ani', role: 'Designer' },
{ id: '3', name: 'Citra', role: 'PM' },
];
return (
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<TouchableOpacity style={styles.card}>
<Text style={styles.cardTitle}>{item.name}</Text>
<Text style={styles.cardSubtitle}>{item.role}</Text>
</TouchableOpacity>
)}
ItemSeparatorComponent={() => <View style={styles.separator} />}
ListHeaderComponent={() => <Text style={styles.sectionTitle}>Tim</Text>}
ListEmptyComponent={() => <Text>Tidak ada data</Text>}
/>
);
}
const styles = StyleSheet.create({
heading: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
},
body: {
fontSize: 16,
color: '#666',
lineHeight: 24,
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#fff',
},
card: {
backgroundColor: '#fff',
padding: 16,
borderRadius: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
cardTitle: { fontSize: 18, fontWeight: '600' },
cardSubtitle: { fontSize: 14, color: '#888', marginTop: 4 },
separator: { height: 12 },
sectionTitle: { fontSize: 20, fontWeight: 'bold', marginBottom: 12 },
});
Styling di React Native
import { StyleSheet, View, Text } from 'react-native';
// StyleSheet β Sama seperti CSS tapi dalam JavaScript
const styles = StyleSheet.create({
container: {
flex: 1, // Mengisi seluruh layar
justifyContent: 'center', // Vertikal alignment
alignItems: 'center', // Horizontal alignment
backgroundColor: '#f5f5f5',
padding: 16,
},
card: {
width: '100%',
backgroundColor: '#fff',
borderRadius: 12,
padding: 16,
// Shadows (berbeda per platform)
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
},
android: {
elevation: 4,
},
}),
},
});
// Flexbox di React Native (default: flexDirection: 'column')
function FlexExample() {
return (
<View style={{ flex: 1 }}>
{/* Row layout */}
<View style={{ flexDirection: 'row', justifyContent: 'space-between', padding: 16 }}>
<View style={{ width: 80, height: 80, backgroundColor: '#ff6b6b', borderRadius: 8 }} />
<View style={{ width: 80, height: 80, backgroundColor: '#4ecdc4', borderRadius: 8 }} />
<View style={{ width: 80, height: 80, backgroundColor: '#45b7d1', borderRadius: 8 }} />
</View>
{/* Center content */}
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text style={{ fontSize: 24, fontWeight: 'bold' }}>Tengah Layar</Text>
</View>
{/* Responsive dengan percentage */}
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
<View style={{ width: '50%', padding: 8 }}>
<View style={{ height: 100, backgroundColor: '#96ceb4', borderRadius: 8 }} />
</View>
<View style={{ width: '50%', padding: 8 }}>
<View style={{ height: 100, backgroundColor: '#ffeaa7', borderRadius: 8 }} />
</View>
</View>
</View>
);
}
5. Navigasi dengan Expo Router
Expo Router adalah sistem navigasi file-based routing untuk Expo/React Native β terinspirasi oleh Next.js. Setiap file di folder app/ secara otomatis menjadi route. Expo Router dibangun di atas React Navigation dan mendukung deep linking, tab navigation, stack navigation, dan modal.
// === STRUKTUR FILE ===
// app/
// βββ _layout.tsx β Root layout (stack navigator)
// βββ index.tsx β Home screen (/)
// βββ profile/
// β βββ _layout.tsx β Profile layout
// β βββ index.tsx β Profile screen (/profile)
// β βββ [id].tsx β Dynamic route (/profile/123)
// βββ (tabs)/
// β βββ _layout.tsx β Tab navigator
// β βββ index.tsx β Home tab
// β βββ explore.tsx β Explore tab
// β βββ settings.tsx β Settings tab
// βββ modal.tsx β Modal screen
// app/_layout.tsx β Root Layout
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: 'Beranda' }} />
<Stack.Screen
name="profile"
options={{
title: 'Profil',
headerShown: false,
}}
/>
<Stack.Screen
name="modal"
options={{
presentation: 'modal',
title: 'Informasi',
}}
/>
</Stack>
);
}
// app/(tabs)/_layout.tsx β Tab Layout
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
export default function TabLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: '#007AFF',
tabBarInactiveTintColor: '#8E8E93',
tabBarStyle: {
backgroundColor: '#fff',
borderTopWidth: 0.5,
borderTopColor: '#E5E5EA',
},
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Beranda',
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="explore"
options={{
title: 'Jelajahi',
tabBarIcon: ({ color, size }) => (
<Ionicons name="compass" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="settings"
options={{
title: 'Pengaturan',
tabBarIcon: ({ color, size }) => (
<Ionicons name="settings" size={size} color={color} />
),
}}
/>
</Tabs>
);
}
// app/profile/[id].tsx β Dynamic Route
import { useLocalSearchParams, useRouter } from 'expo-router';
import { View, Text, Button } from 'react-native';
export default function ProfileScreen() {
const { id } = useLocalSearchParams();
const router = useRouter();
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text style={{ fontSize: 24 }}>Profil: {id}</Text>
<Button title="Kembali" onPress={() => router.back()} />
<Button title="Ke Modal" onPress={() => router.push('/modal')} />
</View>
);
}
6. State Management & API
import React, { useState, useEffect, useCallback } from 'react';
import {
View, Text, FlatList, TouchableOpacity,
ActivityIndicator, RefreshControl, StyleSheet
} from 'react-native';
// ===== FETCH API =====
const API_BASE = 'https://jsonplaceholder.typicode.com';
function PostListScreen() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState(null);
const [page, setPage] = useState(1);
// Fetch data
const fetchPosts = useCallback(async (pageNum = 1) => {
try {
setError(null);
const response = await fetch(
`${API_BASE}/posts?_page=${pageNum}&_limit=20`
);
if (!response.ok) throw new Error('Gagal memuat data');
const data = await response.json();
if (pageNum === 1) {
setPosts(data);
} else {
setPosts(prev => [...prev, ...data]);
}
setPage(pageNum);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
setRefreshing(false);
}
}, []);
// Initial load
useEffect(() => {
fetchPosts(1);
}, [fetchPosts]);
// Pull to refresh
const onRefresh = useCallback(() => {
setRefreshing(true);
fetchPosts(1);
}, [fetchPosts]);
// Load more (infinite scroll)
const onEndReached = useCallback(() => {
fetchPosts(page + 1);
}, [page, fetchPosts]);
// Error state
if (error && posts.length === 0) {
return (
<View style={styles.center}>
<Text style={styles.errorText}>β {error}</Text>
<TouchableOpacity onPress={() => fetchPosts(1)} style={styles.retryBtn}>
<Text style={styles.retryText}>Coba Lagi</Text>
</TouchableOpacity>
</View>
);
}
// Loading state
if (loading && posts.length === 0) {
return (
<View style={styles.center}>
<ActivityIndicator size="large" color="#007AFF" />
<Text style={styles.loadingText}>Memuat postingan...</Text>
</View>
);
}
return (
<FlatList
data={posts}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => (
<TouchableOpacity style={styles.postCard}>
<Text style={styles.postTitle}>{item.title}</Text>
<Text style={styles.postBody} numberOfLines={2}>
{item.body}
</Text>
</TouchableOpacity>
)}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
onEndReached={onEndReached}
onEndReachedThreshold={0.5}
ListFooterComponent={
<ActivityIndicator style={{ padding: 16 }} />
}
/>
);
}
const styles = StyleSheet.create({
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
errorText: { fontSize: 16, color: '#ff3b30', marginBottom: 12 },
retryBtn: { backgroundColor: '#007AFF', padding: 12, borderRadius: 8 },
retryText: { color: '#fff', fontWeight: '600' },
loadingText: { marginTop: 12, color: '#666' },
postCard: { backgroundColor: '#fff', padding: 16, marginHorizontal: 16, marginVertical: 6, borderRadius: 12, elevation: 2 },
postTitle: { fontSize: 16, fontWeight: '600', marginBottom: 4 },
postBody: { fontSize: 14, color: '#666', lineHeight: 20 },
});
7. Native Features & Permissions
import React, { useState, useRef } from 'react';
import { View, Text, Button, Image, Alert } from 'react-native';
// ===== CAMERA =====
import { CameraView, useCameraPermissions } from 'expo-camera';
function CameraScreen() {
const [permission, requestPermission] = useCameraPermissions();
const [photo, setPhoto] = useState(null);
const cameraRef = useRef(null);
if (!permission) return <View />;
if (!permission.granted) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Izin kamera diperlukan</Text>
<Button title="Berikan Izin" onPress={requestPermission} />
</View>
);
}
const takePicture = async () => {
if (cameraRef.current) {
const result = await cameraRef.current.takePictureAsync();
setPhoto(result.uri);
}
};
return (
<View style={{ flex: 1 }}>
{photo ? (
<View style={{ flex: 1 }}>
<Image source={{ uri: photo }} style={{ flex: 1 }} />
<Button title="Ambil Lagi" onPress={() => setPhoto(null)} />
</View>
) : (
<CameraView style={{ flex: 1 }} ref={cameraRef} facing="back">
<View style={{ flex: 1, justifyContent: 'flex-end', alignItems: 'center', marginBottom: 30 }}>
<Button title="πΈ Ambil Foto" onPress={takePicture} />
</View>
</CameraView>
)}
</View>
);
}
// ===== LOCATION =====
import * as Location from 'expo-location';
function LocationScreen() {
const [location, setLocation] = useState(null);
const getLocation = async () => {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
Alert.alert('Izin ditolak', 'Aplikasi memerlukan akses lokasi');
return;
}
const loc = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.High,
});
setLocation(loc.coords);
};
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Button title="Dapatkan Lokasi" onPress={getLocation} />
{location && (
<View style={{ marginTop: 20 }}>
<Text>Latitude: {location.latitude}</Text>
<Text>Longitude: {location.longitude}</Text>
<Text>Akurasi: {location.accuracy}m</Text>
</View>
)}
</View>
);
}
// ===== NOTIFICATIONS =====
import * as Notifications from 'expo-notifications';
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
function NotificationScreen() {
const sendLocalNotification = async () => {
const { status } = await Notifications.requestPermissionsAsync();
if (status !== 'granted') {
Alert.alert('Izin notifikasi diperlukan');
return;
}
await Notifications.scheduleNotificationAsync({
content: {
title: "π Notifikasi Lokal",
body: "Ini adalah contoh notifikasi dari Expo!",
data: { screen: 'detail', id: '123' },
},
trigger: { seconds: 3 },
});
};
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Button title="Kirim Notifikasi (3 detik)" onPress={sendLocalNotification} />
</View>
);
}
8. OTA Updates & EAS Build
OTA (Over-The-Air) Updates memungkinkan Anda mengirim update JavaScript langske device user tanpa perlu review di App Store/Google Play. Update terjadi secara instan saat user membuka aplikasi. EAS (Expo Application Services) adalah platform cloud untuk build, submit, dan mengelola aplikasi Expo.
OTA Updates
import * as Updates from 'expo-updates';
import { useEffect, useState } from 'react';
import { View, Text, Button, Alert } from 'react-native';
function UpdateManager() {
const [isChecking, setIsChecking] = useState(false);
// Cek update saat app dibuka
useEffect(() => {
checkForUpdates();
}, []);
const checkForUpdates = async () => {
if (__DEV__) return; // Skip di development
try {
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
Alert.alert(
'Update Tersedia',
'Ada versi baru. Muat ulang sekarang?',
[
{ text: 'Nanti', style: 'cancel' },
{ text: 'Update', onPress: () => performUpdate() }
]
);
}
} catch (e) {
console.log('Error checking update:', e);
}
};
const performUpdate = async () => {
setIsChecking(true);
try {
const result = await Updates.fetchUpdateAsync();
if (result.isNew) {
await Updates.reloadAsync(); // Restart app dengan update baru
}
} catch (e) {
Alert.alert('Gagal', 'Update gagal. Coba lagi nanti.');
} finally {
setIsChecking(false);
}
};
return (
<View>
<Text>Versi: {Updates.runtimeVersion}</Text>
<Text>Update ID: {Updates.updateId || 'Development'}</Text>
<Button title="Cek Update" onPress={checkForUpdates} disabled={isChecking} />
</View>
);
}
EAS Build & Submit
# Install EAS CLI
npm install -g eas-cli
# Login ke Expo account
eas login
# Konfigurasi EAS (sekali saja)
eas build:configure
# ===== BUILD =====
# Build untuk development (development client)
eas build --profile development --platform android
eas build --profile development --platform ios
# Build untuk preview (internal testing)
eas build --profile preview --platform android
# Build untuk production
eas build --profile production --platform android
eas build --profile production --platform ios
eas build --profile production --platform all # Kedua platform
# Cek status build
eas build:list
# ===== SUBMIT ke Store =====
# Submit ke Google Play
eas submit --platform android
# Submit ke App Store
eas submit --platform ios
# ===== OTA UPDATE =====
# Publish update OTA
eas update --branch production
eas update --branch staging --message "Fix bug login"
# Cek update history
eas update:list
# ===== eas.json (konfigurasi build) =====
{
"cli": {
"version": ">= 7.0.0"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal",
"channel": "staging"
},
"production": {
"channel": "production",
"autoIncrement": true
}
},
"submit": {
"production": {
"android": {
"serviceAccountKeyPath": "./google-services.json"
},
"ios": {
"appleId": "developer@beebane.com",
"ascAppId": "123456789"
}
}
}
}
OTA Update bisa mengubah: JavaScript code, styling, assets (gambar/font). OTA TIDAK bisa mengubah: native dependencies, app.json (bundle ID, version), native code changes. Jika perubahan hanya di JS β gunakan OTA. Jika ada perubahan native β perlu full build & submit ke store.
9. Deployment ke Store
| Langkah | Google Play | App Store |
|---|---|---|
| 1. Akun Developer | Google Play Console ($25 sekali) | Apple Developer ($99/tahun) |
| 2. Build | eas build --platform android | eas build --platform ios |
| 3. Submit | eas submit --platform android | eas submit --platform ios |
| 4. Review | 1-3 hari (biasa lebih cepat) | 1-7 hari (lebih ketat) |
| 5. Published | Otomatis setelah review | Manual setelah approve |
10. Quiz Pemahaman
1. Apa keunggulan utama Managed Workflow di Expo?
2. Apa itu OTA Updates di Expo?
3. Apa yang dimaksud dengan Expo Router?
4. Kapan perlu full build (bukan OTA)?
5. Fungsi FlatList di React Native?
Anda telah mempelajari Expo untuk React Native development β dari setup, core components, navigasi, native features, OTA updates, hingga EAS Build dan deployment. Expo memudahkan pengembangan mobile cross-platform tanpa kompleksitas native!