Platform Integration
React Native provides extensive access to native device capabilities through Expo SDK and custom native modules. This guide covers how to integrate with platform-specific APIs and handle cross-platform differences.
Expo SDK Overview
Expo SDK provides a comprehensive set of APIs for common mobile functionality:
Device Information
import * as Device from "expo-device"
import Constants from "expo-constants"
function DeviceInfo() {
const deviceInfo = {
brand: Device.brand,
modelName: Device.modelName,
osName: Device.osName,
osVersion: Device.osVersion,
platformApiLevel: Device.platformApiLevel,
isDevice: Device.isDevice,
deviceType: Device.deviceType,
}
return (
<View>
<Text>
Device: {deviceInfo.brand} {deviceInfo.modelName}
</Text>
<Text>
OS: {deviceInfo.osName} {deviceInfo.osVersion}
</Text>
<Text>App Version: {Constants.expoConfig?.version}</Text>
</View>
)
}
Camera Integration
import { Camera, CameraType } from "expo-camera"
import { useState, useRef } from "react"
function CameraScreen() {
const [type, setType] = useState(CameraType.back)
const [permission, requestPermission] = Camera.useCameraPermissions()
const cameraRef = useRef<Camera>(null)
if (!permission) {
return <View />
}
if (!permission.granted) {
return (
<View style={styles.container}>
<Text>We need your permission to show the camera</Text>
<Button onPress={requestPermission} title="Grant Permission" />
</View>
)
}
const takePicture = async () => {
if (cameraRef.current) {
const photo = await cameraRef.current.takePictureAsync()
console.log("Photo taken:", photo.uri)
}
}
return (
<View style={styles.container}>
<Camera style={styles.camera} type={type} ref={cameraRef}>
<View style={styles.buttonContainer}>
<TouchableOpacity style={styles.button} onPress={takePicture}>
<Text style={styles.text}>Take Photo</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.button}
onPress={() => {
setType(type === CameraType.back ? CameraType.front : CameraType.back)
}}
>
<Text style={styles.text}>Flip Camera</Text>
</TouchableOpacity>
</View>
</Camera>
</View>
)
}
Location Services
import * as Location from "expo-location"
import { useState, useEffect } from "react"
function LocationScreen() {
const [location, setLocation] = useState<Location.LocationObject | null>(null)
const [errorMsg, setErrorMsg] = useState<string | null>(null)
useEffect(() => {
;(async () => {
let { status } = await Location.requestForegroundPermissionsAsync()
if (status !== "granted") {
setErrorMsg("Permission to access location was denied")
return
}
let location = await Location.getCurrentPositionAsync({})
setLocation(location)
})()
}, [])
const watchLocation = async () => {
const subscription = await Location.watchPositionAsync(
{
accuracy: Location.Accuracy.High,
timeInterval: 1000,
distanceInterval: 1,
},
(location) => {
setLocation(location)
}
)
return () => subscription.remove()
}
let text = "Waiting..."
if (errorMsg) {
text = errorMsg
} else if (location) {
text = JSON.stringify(location)
}
return (
<View style={styles.container}>
<Text style={styles.paragraph}>{text}</Text>
</View>
)
}
Push Notifications
import * as Notifications from "expo-notifications"
import { useState, useEffect, useRef } from "react"
import { Platform } from "react-native"
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: false,
shouldSetBadge: false,
}),
})
function NotificationScreen() {
const [expoPushToken, setExpoPushToken] = useState<string>("")
const [notification, setNotification] = useState<Notifications.Notification | null>(null)
const notificationListener = useRef<Notifications.Subscription>()
const responseListener = useRef<Notifications.Subscription>()
useEffect(() => {
registerForPushNotificationsAsync().then((token) => setExpoPushToken(token || ""))
notificationListener.current = Notifications.addNotificationReceivedListener((notification) => {
setNotification(notification)
})
responseListener.current = Notifications.addNotificationResponseReceivedListener((response) => {
console.log(response)
})
return () => {
Notifications.removeNotificationSubscription(notificationListener.current!)
Notifications.removeNotificationSubscription(responseListener.current!)
}
}, [])
return (
<View style={styles.container}>
<Text>Your expo push token: {expoPushToken}</Text>
<Button
title="Press to schedule a notification"
onPress={async () => {
await schedulePushNotification()
}}
/>
</View>
)
}
async function schedulePushNotification() {
await Notifications.scheduleNotificationAsync({
content: {
title: "You've got mail! 📬",
body: "Here is the notification body",
data: { data: "goes here" },
},
trigger: { seconds: 2 },
})
}
async function registerForPushNotificationsAsync() {
let token
if (Platform.OS === "android") {
await Notifications.setNotificationChannelAsync("default", {
name: "default",
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: "#FF231F7C",
})
}
if (Device.isDevice) {
const { status: existingStatus } = await Notifications.getPermissionsAsync()
let finalStatus = existingStatus
if (existingStatus !== "granted") {
const { status } = await Notifications.requestPermissionsAsync()
finalStatus = status
}
if (finalStatus !== "granted") {
alert("Failed to get push token for push notification!")
return
}
token = (await Notifications.getExpoPushTokenAsync()).data
} else {
alert("Must use physical device for Push Notifications")
}
return token
}
File System Access
import * as FileSystem from "expo-file-system"
import * as DocumentPicker from "expo-document-picker"
function FileManager() {
const [fileUri, setFileUri] = useState<string | null>(null)
const pickDocument = async () => {
try {
const result = await DocumentPicker.getDocumentAsync({
type: "*/*",
copyToCacheDirectory: true,
})
if (!result.canceled && result.assets[0]) {
setFileUri(result.assets[0].uri)
}
} catch (error) {
console.error("Error picking document:", error)
}
}
const saveFile = async (content: string, filename: string) => {
try {
const fileUri = FileSystem.documentDirectory + filename
await FileSystem.writeAsStringAsync(fileUri, content)
console.log("File saved to:", fileUri)
} catch (error) {
console.error("Error saving file:", error)
}
}
const readFile = async (uri: string) => {
try {
const content = await FileSystem.readAsStringAsync(uri)
return content
} catch (error) {
console.error("Error reading file:", error)
return null
}
}
return (
<View style={styles.container}>
<Button title="Pick Document" onPress={pickDocument} />
{fileUri && <Text>Selected file: {fileUri}</Text>}
</View>
)
}
Biometric Authentication
import * as LocalAuthentication from "expo-local-authentication"
import { useState } from "react"
function BiometricAuth() {
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [authError, setAuthError] = useState<string | null>(null)
const checkBiometricSupport = async () => {
const compatible = await LocalAuthentication.hasHardwareAsync()
const enrolled = await LocalAuthentication.isEnrolledAsync()
const supportedTypes = await LocalAuthentication.supportedAuthenticationTypesAsync()
return {
compatible,
enrolled,
supportedTypes,
}
}
const authenticate = async () => {
try {
const result = await LocalAuthentication.authenticateAsync({
promptMessage: "Authenticate to access your account",
fallbackLabel: "Use Passcode",
})
if (result.success) {
setIsAuthenticated(true)
setAuthError(null)
} else {
setAuthError("Authentication failed")
}
} catch (error) {
setAuthError("Authentication error")
}
}
return (
<View style={styles.container}>
{!isAuthenticated ? (
<>
<Button title="Authenticate" onPress={authenticate} />
{authError && <Text style={styles.error}>{authError}</Text>}
</>
) : (
<Text style={styles.success}>Successfully authenticated!</Text>
)}
</View>
)
}
Platform-Specific Code
Using Platform Module
import { Platform } from "react-native"
const styles = StyleSheet.create({
container: {
flex: 1,
...Platform.select({
ios: {
backgroundColor: "#f8f8f8",
},
android: {
backgroundColor: "#ffffff",
},
default: {
backgroundColor: "#f0f0f0",
},
}),
},
header: {
height: Platform.OS === "ios" ? 88 : 56,
paddingTop: Platform.OS === "ios" ? 44 : 0,
},
})
// Platform-specific components
const MyComponent = Platform.select({
ios: () => require("./MyComponent.ios").default,
android: () => require("./MyComponent.android").default,
default: () => require("./MyComponent").default,
})()
File Extensions for Platform-Specific Code
// MyComponent.ios.tsx
export default function MyComponent() {
return <Text>iOS-specific component</Text>
}
// MyComponent.android.tsx
export default function MyComponent() {
return <Text>Android-specific component</Text>
}
// MyComponent.tsx (fallback)
export default function MyComponent() {
return <Text>Default component</Text>
}
Custom Native Modules
Creating a Simple Native Module
// expo-module.config.json
{
"platforms": ["ios", "android"],
"ios": {
"modules": ["MyNativeModule"]
},
"android": {
"modules": ["MyNativeModule"]
}
}
// iOS Implementation (MyNativeModule.swift)
import ExpoModulesCore
public class MyNativeModule: Module {
public func definition() -> ModuleDefinition {
Name("MyNativeModule")
Function("hello") { (name: String) in
return "Hello \(name)!"
}
AsyncFunction("getDeviceInfo") { (promise: Promise) in
let deviceInfo = [
"model": UIDevice.current.model,
"systemName": UIDevice.current.systemName,
"systemVersion": UIDevice.current.systemVersion
]
promise.resolve(deviceInfo)
}
}
}
// TypeScript definitions
declare module 'expo-my-native-module' {
export function hello(name: string): string;
export function getDeviceInfo(): Promise<{
model: string;
systemName: string;
systemVersion: string;
}>;
}
// Usage in React Native
import { hello, getDeviceInfo } from 'expo-my-native-module';
function NativeModuleExample() {
const [deviceInfo, setDeviceInfo] = useState(null);
useEffect(() => {
getDeviceInfo().then(setDeviceInfo);
}, []);
return (
<View>
<Text>{hello('React Native')}</Text>
{deviceInfo && (
<Text>
Device: {deviceInfo.model} running {deviceInfo.systemName} {deviceInfo.systemVersion}
</Text>
)}
</View>
);
}
Permissions Management
import * as MediaLibrary from "expo-media-library"
import * as Camera from "expo-camera"
import * as Location from "expo-location"
function PermissionsManager() {
const [permissions, setPermissions] = useState({
camera: false,
location: false,
mediaLibrary: false,
})
const requestAllPermissions = async () => {
const cameraPermission = await Camera.requestCameraPermissionsAsync()
const locationPermission = await Location.requestForegroundPermissionsAsync()
const mediaPermission = await MediaLibrary.requestPermissionsAsync()
setPermissions({
camera: cameraPermission.granted,
location: locationPermission.granted,
mediaLibrary: mediaPermission.granted,
})
}
const checkPermissions = async () => {
const cameraStatus = await Camera.getCameraPermissionsAsync()
const locationStatus = await Location.getForegroundPermissionsAsync()
const mediaStatus = await MediaLibrary.getPermissionsAsync()
setPermissions({
camera: cameraStatus.granted,
location: locationStatus.granted,
mediaLibrary: mediaStatus.granted,
})
}
useEffect(() => {
checkPermissions()
}, [])
return (
<View style={styles.container}>
<Text>Camera: {permissions.camera ? "✅" : "❌"}</Text>
<Text>Location: {permissions.location ? "✅" : "❌"}</Text>
<Text>Media Library: {permissions.mediaLibrary ? "✅" : "❌"}</Text>
<Button title="Request All Permissions" onPress={requestAllPermissions} />
</View>
)
}
Background Tasks
import * as BackgroundFetch from "expo-background-fetch"
import * as TaskManager from "expo-task-manager"
const BACKGROUND_FETCH_TASK = "background-fetch"
// Define the background task
TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
const now = Date.now()
console.log(`Got background fetch call at date: ${new Date(now).toISOString()}`)
// Perform background work here
try {
// Example: sync data, send analytics, etc.
await syncUserData()
return BackgroundFetch.BackgroundFetchResult.NewData
} catch (error) {
return BackgroundFetch.BackgroundFetchResult.Failed
}
})
function BackgroundTaskExample() {
const [isRegistered, setIsRegistered] = useState(false)
const [status, setStatus] = useState<BackgroundFetch.BackgroundFetchStatus | null>(null)
useEffect(() => {
checkStatusAsync()
}, [])
const checkStatusAsync = async () => {
const status = await BackgroundFetch.getStatusAsync()
const isRegistered = await TaskManager.isTaskRegisteredAsync(BACKGROUND_FETCH_TASK)
setStatus(status)
setIsRegistered(isRegistered)
}
const registerBackgroundFetchAsync = async () => {
return BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK, {
minimumInterval: 15 * 60 * 1000, // 15 minutes
stopOnTerminate: false,
startOnBoot: true,
})
}
const unregisterBackgroundFetchAsync = async () => {
return BackgroundFetch.unregisterTaskAsync(BACKGROUND_FETCH_TASK)
}
return (
<View style={styles.container}>
<Text>Background fetch status: {status}</Text>
<Text>Background task registered: {isRegistered ? "Yes" : "No"}</Text>
<Button
title={isRegistered ? "Unregister" : "Register"}
onPress={isRegistered ? unregisterBackgroundFetchAsync : registerBackgroundFetchAsync}
/>
</View>
)
}
async function syncUserData() {
// Example background sync operation
const userData = await fetchUserDataFromAPI()
await saveUserDataLocally(userData)
}
Security Considerations
import * as SecureStore from "expo-secure-store"
import * as Crypto from "expo-crypto"
class SecurityManager {
// Secure storage for sensitive data
static async storeSecurely(key: string, value: string) {
try {
await SecureStore.setItemAsync(key, value)
} catch (error) {
console.error("Error storing secure data:", error)
}
}
static async getSecurely(key: string): Promise<string | null> {
try {
return await SecureStore.getItemAsync(key)
} catch (error) {
console.error("Error retrieving secure data:", error)
return null
}
}
// Encrypt sensitive data
static async encryptData(data: string): Promise<string> {
const digest = await Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, data, {
encoding: Crypto.CryptoEncoding.HEX,
})
return digest
}
// Generate secure random values
static async generateRandomKey(length: number = 32): Promise<string> {
const randomBytes = await Crypto.getRandomBytesAsync(length)
return Array.from(randomBytes, (byte) => byte.toString(16).padStart(2, "0")).join("")
}
}
// Usage example
function SecureDataExample() {
const storeAuthToken = async (token: string) => {
await SecurityManager.storeSecurely("auth_token", token)
}
const getAuthToken = async () => {
return await SecurityManager.getSecurely("auth_token")
}
return (
<View style={styles.container}>
<Button title="Store Token" onPress={() => storeAuthToken("my-secure-token")} />
<Button
title="Get Token"
onPress={async () => {
const token = await getAuthToken()
console.log("Retrieved token:", token)
}}
/>
</View>
)
}
Next Steps
Continue with these related topics:
- Data & Networking - API integration and data management
- Performance Optimization - Optimizing platform integrations
- Testing Strategies - Testing platform-specific functionality