Mobile

Expo: React Native Development

TOKEN

Panduan lengkap Expo untuk React Native β€” dari managed workflow, navigasi, state management, OTA updates, EAS Build hingga deployment ke App Store dan Google Play

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

KeunggulanPenjelasan
Zero ConfigTidak perlu konfigurasi native β€” langsung coding dan jalan di device
Expo GoTest langsung di device fisik tanpa build β€” scan QR code, langsung jalan
OTA UpdatesPush update JavaScript ke user tanpa review store β€” instant deployment
EAS BuildCloud build untuk iOS & Android β€” tidak perlu Mac untuk build iOS
Rich API200+ module bawaan: kamera, maps, notifications, file system, dan lainnya
TypeScriptSupport TypeScript out of the box
Web SupportBisa juga dijalankan di web dengan expo-web

Expo vs React Native CLI vs Alternatif

AspekExpoReact Native CLIFlutter
BahasaJavaScript/TypeScriptJavaScript/TypeScriptDart
Setup🟒 Sangat mudah🟑 Sedang🟑 Sedang
Native CodeManaged: tidak bisa, Bare: bisaFull aksesFull akses
OTA Updatesβœ… Built-in❌ Perlu CodePush⚠️ Terbatas
BuildEAS Build (cloud)Local buildLocal / CI/CD
Ukuran AppLebih besarLebih kecilSedang
Library EcosystemExpo modules + npmSeluruh npmpub.dev
Diagram: Arsitektur Expo
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  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

Bash β€” Instalasi & Setup
# 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

JSON β€” 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:

AspekManaged WorkflowBare Workflow
Konfigurasi nativeDikelola sepenuhnya oleh ExpoAnda yang mengelola (seperti RN CLI)
Xcode/Android StudioTidak perlu untuk developmentPerlu
Native dependenciesHanya yang ada di Expo SDKBebas menambahkan apapun
Expo Goβœ… Bisa digunakan❌ Tidak bisa
OTA Updatesβœ… Full supportβœ… Dengan konfigurasi
BuildEAS BuildEAS Build atau local build
Cocok untukSebagian besar aplikasiAplikasi dengan native module kustom
πŸ’‘ Rekomendasi

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

JSX β€” Core Components
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

JSX β€” Styling Patterns
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>
  );
}

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.

JSX β€” Expo Router (File-based)
// === 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

JSX β€” API & State
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

JSX β€” Camera, Location, Notifications
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

JSX β€” 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

Bash β€” EAS Commands
# 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"
      }
    }
  }
}
πŸ’‘ Kapan OTA vs Full Build?

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

LangkahGoogle PlayApp Store
1. Akun DeveloperGoogle Play Console ($25 sekali)Apple Developer ($99/tahun)
2. Buildeas build --platform androideas build --platform ios
3. Submiteas submit --platform androideas submit --platform ios
4. Review1-3 hari (biasa lebih cepat)1-7 hari (lebih ketat)
5. PublishedOtomatis setelah reviewManual 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?

πŸŽ‰ Selamat!

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!

πŸ” Zoom
100%
🎨 Tema