Multimedia dan Device API (Camera, Location, Notification)
Pertemuan 13 membahas integrasi fitur multimedia dan device API dalam aplikasi React Native menggunakan Expo. Materi mencakup akses kamera, galeri foto, lokasi GPS, dan notifikasi untuk menciptakan aplikasi mobile yang lebih interaktif.
Slide 2: Tujuan Pembelajaran Pertemuan 13
Mahasiswa mampu mengintegrasikan fitur device native seperti kamera, galeri, GPS, dan notifikasi dalam aplikasi React Native. Mahasiswa memahami cara menangani permission secara proper dan mengimplementasikan best practices dalam penggunaan device API.
Fitur multimedia meningkatkan user experience dan interaktivitas aplikasi. Aplikasi modern seperti Instagram, WhatsApp, dan Gojek sangat bergantung pada akses kamera, lokasi, dan media library. Kemampuan mengakses hardware device membuat aplikasi lebih powerful dan sesuai kebutuhan user.
Slide 4: Overview Device API yang Akan Dipelajari
API yang akan dipelajari meliputi:
- Camera: Mengambil foto dan video
- Image Picker: Akses galeri dan kamera
- Location: Mendapatkan koordinat GPS
- Notifications: Mengirim notifikasi lokal
- Media Library: Mengakses file media device
Slide 5: Perbedaan Expo CLI vs React Native CLI untuk Device Access
Expo menyediakan pre-built modules yang mudah digunakan tanpa konfigurasi native code. React Native CLI memerlukan linking manual dan konfigurasi platform-specific. Expo cocok untuk rapid development, sedangkan RN CLI memberikan kontrol lebih detail namun kompleks.
import * as Camera from "expo-camera";
import { RNCamera } from "react-native-camera";
Slide 6: Pengenalan Expo Camera
Expo Camera adalah library untuk mengakses kamera device dengan mudah. Mendukung foto, video, barcode scanning, dan face detection. Tersedia untuk iOS dan Android dengan API yang konsisten.
Slide 7: Instalasi dan Konfigurasi expo-camera
Instalasi menggunakan npm atau yarn, kemudian import ke dalam project.
npx expo install expo-camera
import { Camera, CameraType } from 'expo-camera';
import { useState, useRef } from 'react';
Slide 8: Permission Handling untuk Kamera
Permission harus diminta sebelum mengakses kamera untuk keamanan dan privacy user.
import { Camera } from "expo-camera";
import { useEffect, useState } from "react";
const CameraScreen = () => {
const [hasPermission, setHasPermission] = useState(null);
useEffect(() => {
(async () => {
const { status } = await Camera.requestCameraPermissionsAsync();
setHasPermission(status === "granted");
})();
}, []);
if (hasPermission === null) {
return <Text>Meminta permission...</Text>;
}
if (hasPermission === false) {
return <Text>Akses kamera ditolak</Text>;
}
return <Camera style={{ flex: 1 }} />;
};
Slide 9: Struktur Dasar Komponen Camera
Komponen Camera memerlukan style dengan flex untuk menampilkan preview kamera.
import { Camera, CameraType } from "expo-camera";
import { StyleSheet, View } from "react-native";
const CameraScreen = () => {
const [type, setType] = useState(CameraType.back);
return (
<View style={styles.container}>
<Camera style={styles.camera} type={type}>
{/* UI overlay di sini */}
</Camera>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
camera: {
flex: 1,
},
});
Slide 10: Props dan Methods Camera API
Camera component memiliki berbagai props untuk konfigurasi dan methods untuk kontrol.
<Camera
type={CameraType.back}
flashMode={FlashMode.off}
zoom={0}
ratio="16:9"
onCameraReady={() => console.log("Camera ready")}
onMountError={(error) => console.log(error)}
/>
Slide 11: Capture Photo: Konsep dan Implementasi
Menggunakan ref untuk mengakses camera instance dan method takePictureAsync untuk capture.
import { Camera } from "expo-camera";
import { useRef, useState } from "react";
import { Button, Image } from "react-native";
const CameraScreen = () => {
const cameraRef = useRef(null);
const [photo, setPhoto] = useState(null);
const takePicture = async () => {
if (cameraRef.current) {
const options = { quality: 0.5, base64: true };
const data = await cameraRef.current.takePictureAsync(options);
setPhoto(data.uri);
console.log("Photo URI:", data.uri);
}
};
return (
<>
<Camera style={{ flex: 1 }} ref={cameraRef} />
<Button title="Take Photo" onPress={takePicture} />
{photo && (
<Image source={{ uri: photo }} style={{ width: 200, height: 200 }} />
)}
</>
);
};
Slide 12: Camera Types: Front vs Back Camera
Toggle antara kamera depan dan belakang menggunakan state.
import { Camera, CameraType } from "expo-camera";
import { useState } from "react";
import { Button } from "react-native";
const CameraScreen = () => {
const [type, setType] = useState(CameraType.back);
const toggleCameraType = () => {
setType((current) =>
current === CameraType.back ? CameraType.front : CameraType.back
);
};
return (
<>
<Camera style={{ flex: 1 }} type={type} />
<Button title="Flip Camera" onPress={toggleCameraType} />
</>
);
};
Slide 13: Pengenalan expo-image-picker
Image Picker memungkinkan user memilih foto/video dari galeri atau mengambil foto baru tanpa membuat komponen kamera sendiri.
Slide 14: Mengakses Galeri vs Kamera Langsung
Image Picker menyediakan dua metode utama: launchImageLibraryAsync untuk galeri dan launchCameraAsync untuk kamera.
import * as ImagePicker from "expo-image-picker";
const pickImage = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4, 3],
quality: 1,
});
if (!result.canceled) {
console.log(result.assets[0].uri);
}
};
const takePhoto = async () => {
let result = await ImagePicker.launchCameraAsync({
allowsEditing: true,
aspect: [1, 1],
quality: 0.5,
});
if (!result.canceled) {
console.log(result.assets[0].uri);
}
};
Request permission sebelum mengakses galeri atau kamera melalui Image Picker.
import * as ImagePicker from "expo-image-picker";
import { useEffect } from "react";
const App = () => {
useEffect(() => {
(async () => {
const { status } =
await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== "granted") {
alert("Permission ditolak!");
}
const cameraStatus = await ImagePicker.requestCameraPermissionsAsync();
if (cameraStatus.status !== "granted") {
alert("Camera permission ditolak!");
}
})();
}, []);
};
Slide 16: ImagePicker Options dan Konfigurasi
Berbagai opsi untuk mengkustomisasi behavior image picker.
const options = {
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsEditing: true,
aspect: [16, 9],
quality: 0.8,
allowsMultipleSelection: false,
selectionLimit: 3,
base64: false,
exif: false,
};
const result = await ImagePicker.launchImageLibraryAsync(options);
Slide 17: Image Compression dan Quality Control
Mengatur kualitas gambar untuk menghemat storage dan bandwidth.
const pickImageWithCompression = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 0.5,
allowsEditing: true,
aspect: [4, 3],
});
if (!result.canceled) {
const asset = result.assets[0];
console.log("Original size:", asset.fileSize);
console.log("URI:", asset.uri);
console.log("Width:", asset.width);
console.log("Height:", asset.height);
}
};
Slide 18: Handling Selected Images
Menyimpan dan menampilkan gambar yang dipilih user.
import { useState } from "react";
import { Image, Button, View } from "react-native";
import * as ImagePicker from "expo-image-picker";
const ImagePickerExample = () => {
const [selectedImage, setSelectedImage] = useState(null);
const pickImage = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
quality: 1,
});
if (!result.canceled) {
setSelectedImage(result.assets[0].uri);
}
};
return (
<View>
<Button title="Pilih Gambar" onPress={pickImage} />
{selectedImage && (
<Image
source={{ uri: selectedImage }}
style={{ width: 300, height: 300 }}
/>
)}
</View>
);
};
Slide 19: Pengenalan Expo Location
Expo Location API menyediakan akses ke GPS device untuk mendapatkan koordinat, tracking posisi, dan geocoding.
Slide 20: Permission Handling untuk Lokasi
Request location permission dengan granularity yang berbeda.
import * as Location from "expo-location";
import { useEffect, useState } from "react";
const LocationScreen = () => {
const [location, setLocation] = useState(null);
const [errorMsg, setErrorMsg] = useState(null);
useEffect(() => {
(async () => {
let { status } = await Location.requestForegroundPermissionsAsync();
if (status !== "granted") {
setErrorMsg("Permission ditolak");
return;
}
let location = await Location.getCurrentPositionAsync({});
setLocation(location);
})();
}, []);
return <Text>{errorMsg ? errorMsg : JSON.stringify(location)}</Text>;
};
Slide 21: getCurrentPositionAsync: Mendapatkan Lokasi Saat Ini
Method untuk mendapatkan posisi device satu kali.
import * as Location from "expo-location";
const getLocation = async () => {
try {
let { status } = await Location.requestForegroundPermissionsAsync();
if (status !== "granted") {
console.log("Permission ditolak");
return;
}
let location = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.High,
});
console.log("Latitude:", location.coords.latitude);
console.log("Longitude:", location.coords.longitude);
console.log("Altitude:", location.coords.altitude);
console.log("Speed:", location.coords.speed);
console.log("Heading:", location.coords.heading);
} catch (error) {
console.error(error);
}
};
Slide 22: watchPositionAsync: Tracking Real-time Location
Monitoring perubahan lokasi secara real-time untuk aplikasi tracking.
import * as Location from "expo-location";
import { useEffect, useState } from "react";
const LocationTracker = () => {
const [location, setLocation] = useState(null);
useEffect(() => {
let subscription;
(async () => {
let { status } = await Location.requestForegroundPermissionsAsync();
if (status !== "granted") return;
subscription = await Location.watchPositionAsync(
{
accuracy: Location.Accuracy.High,
timeInterval: 5000,
distanceInterval: 10,
},
(newLocation) => {
setLocation(newLocation);
console.log("Updated:", newLocation.coords);
}
);
})();
return () => {
if (subscription) {
subscription.remove();
}
};
}, []);
return (
<Text>
{location
? `Lat: ${location.coords.latitude}, Lng: ${location.coords.longitude}`
: "Loading..."}
</Text>
);
};
Slide 23: Accuracy Levels pada Location API
Berbagai level akurasi untuk menyeimbangkan presisi dan battery consumption.
import * as Location from "expo-location";
const accuracyLevels = {
lowest: Location.Accuracy.Lowest,
low: Location.Accuracy.Low,
balanced: Location.Accuracy.Balanced,
high: Location.Accuracy.High,
highest: Location.Accuracy.Highest,
bestForNavigation: Location.Accuracy.BestForNavigation,
};
const getAccurateLocation = async () => {
const location = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.BestForNavigation,
});
console.log(location);
};
Slide 24: Reverse Geocoding dengan Expo Location
Mengkonversi koordinat GPS menjadi alamat yang dapat dibaca manusia.
import * as Location from "expo-location";
const reverseGeocode = async (latitude, longitude) => {
try {
const address = await Location.reverseGeocodeAsync({
latitude,
longitude,
});
if (address.length > 0) {
const location = address[0];
console.log("Street:", location.street);
console.log("City:", location.city);
console.log("Region:", location.region);
console.log("Country:", location.country);
console.log("Postal Code:", location.postalCode);
return `${location.street}, ${location.city}, ${location.country}`;
}
} catch (error) {
console.error(error);
}
};
const fullAddress = await reverseGeocode(-6.2088, 106.8456);
Slide 25: Pengenalan Expo Notifications
Expo Notifications memungkinkan pengiriman notifikasi lokal dan push notifications ke device user.
Slide 26: Local Notifications vs Push Notifications
Local notifications dijadwalkan dari aplikasi itu sendiri, sedangkan push notifications dikirim dari server eksternal.
import * as Notifications from "expo-notifications";
const scheduleLocalNotification = async () => {
await Notifications.scheduleNotificationAsync({
content: {
title: "Local Notification",
body: "Ini notifikasi dari aplikasi lokal",
},
trigger: { seconds: 5 },
});
};
const getPushToken = async () => {
const token = await Notifications.getExpoPushTokenAsync();
console.log("Push Token:", token.data);
};
Slide 27: Scheduling Notifications
Menjadwalkan notifikasi untuk waktu tertentu atau berulang.
import * as Notifications from "expo-notifications";
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
const scheduleNotification = async () => {
await Notifications.scheduleNotificationAsync({
content: {
title: "Reminder",
body: "Jangan lupa minum air!",
data: { userId: 123 },
},
trigger: { seconds: 10 },
});
};
const scheduleDailyNotification = async () => {
await Notifications.scheduleNotificationAsync({
content: {
title: "Good Morning!",
body: "Waktunya memulai hari",
},
trigger: {
hour: 9,
minute: 0,
repeats: true,
},
});
};
const cancelNotification = async (notificationId) => {
await Notifications.cancelScheduledNotificationAsync(notificationId);
};
const cancelAll = async () => {
await Notifications.cancelAllScheduledNotificationsAsync();
};
Media Library memberikan akses ke foto dan video yang tersimpan di device.
import * as MediaLibrary from "expo-media-library";
import { useEffect, useState } from "react";
const MediaLibraryExample = () => {
const [albums, setAlbums] = useState([]);
const [photos, setPhotos] = useState([]);
useEffect(() => {
(async () => {
const { status } = await MediaLibrary.requestPermissionsAsync();
if (status !== "granted") {
alert("Permission ditolak");
return;
}
const albumList = await MediaLibrary.getAlbumsAsync();
setAlbums(albumList);
const media = await MediaLibrary.getAssetsAsync({
first: 20,
mediaType: "photo",
sortBy: "creationTime",
});
setPhotos(media.assets);
})();
}, []);
return (
<View>
<Text>Total Albums: {albums.length}</Text>
<Text>Recent Photos: {photos.length}</Text>
</View>
);
};
const saveToGallery = async (uri) => {
try {
const asset = await MediaLibrary.createAssetAsync(uri);
await MediaLibrary.createAlbumAsync("My App Photos", asset, false);
console.log("Saved to gallery");
} catch (error) {
console.error(error);
}
};
Slide 29: Best Practices Permission Handling
Strategi menangani permission dengan user experience yang baik.
import * as Location from "expo-location";
import { Alert } from "react-native";
const requestLocationWithFallback = async () => {
const { status: existingStatus } =
await Location.getForegroundPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== "granted") {
const { status } = await Location.requestForegroundPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== "granted") {
Alert.alert(
"Permission Diperlukan",
"Aplikasi memerlukan akses lokasi untuk fitur ini. Silakan aktifkan di Settings.",
[
{ text: "Cancel", style: "cancel" },
{ text: "Open Settings", onPress: () => Linking.openSettings() },
]
);
return false;
}
return true;
};
const handleLocationFeature = async () => {
const hasPermission = await requestLocationWithFallback();
if (hasPermission) {
const location = await Location.getCurrentPositionAsync({});
console.log(location);
}
};
Slide 30: Studi Kasus: Aplikasi dengan Multiple Device Features
Contoh aplikasi lengkap yang mengintegrasikan kamera, lokasi, dan notifikasi.
import { useState, useEffect } from "react";
import { View, Button, Text, Image } from "react-native";
import * as ImagePicker from "expo-image-picker";
import * as Location from "expo-location";
import * as Notifications from "expo-notifications";
const TravelJournalApp = () => {
const [photo, setPhoto] = useState(null);
const [location, setLocation] = useState(null);
useEffect(() => {
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
(async () => {
await ImagePicker.requestCameraPermissionsAsync();
await Location.requestForegroundPermissionsAsync();
await Notifications.requestPermissionsAsync();
})();
}, []);
const takePhotoWithLocation = async () => {
const result = await ImagePicker.launchCameraAsync({
quality: 0.7,
allowsEditing: true,
});
if (result.canceled) return;
const currentLocation = await Location.getCurrentPositionAsync({});
const address = await Location.reverseGeocodeAsync({
latitude: currentLocation.coords.latitude,
longitude: currentLocation.coords.longitude,
});
setPhoto(result.assets[0].uri);
setLocation({
coords: currentLocation.coords,
address: address[0],
});
await Notifications.scheduleNotificationAsync({
content: {
title: "Foto Tersimpan!",
body: `Lokasi: ${address[0].city}, ${address[0].country}`,
},
trigger: { seconds: 1 },
});
};
return (
<View style={{ padding: 20 }}>
<Button
title="Ambil Foto dengan Lokasi"
onPress={takePhotoWithLocation}
/>
{photo && (
<>
<Image
source={{ uri: photo }}
style={{ width: 300, height: 300, marginTop: 20 }}
/>
{location && (
<View style={{ marginTop: 10 }}>
<Text>Latitude: {location.coords.latitude.toFixed(4)}</Text>
<Text>Longitude: {location.coords.longitude.toFixed(4)}</Text>
<Text>Lokasi: {location.address.street}</Text>
<Text>
{location.address.city}, {location.address.country}
</Text>
</View>
)}
</>
)}
</View>
);
};
export default TravelJournalApp;