Feuille de chaleur Expo¶
Expo - La façon la plus rapide de construire des applications autochtones de réaction
Expo est une plateforme open-source pour faire des applications natives universelles pour Android, iOS et le web avec JavaScript et Réagir. Il fournit un ensemble d'outils et de services construits autour des plateformes autochtones et autochtones React.
Sommaire¶
- [Installation] (#installation)
- [Pour commencer] (#getting-started)
- [Structure du projet] (#project-structure)
- [Expo CLI] (#expo-cli)
- [Travaux de développement] (#development-workflow)
- [Composants de base] (#core-components)
- [Navigation] (#navigation)
- [Gestion de l'État] (#state-management)
- [Styling] (#styling)
- [API et services] (#apis-and-services)
- [Avis d'urgence] (#push-notifications)
- [Système de fichiers] (#file-system)
- [Camera et médias] (#camera-and-media)
- [Services de localisation] (#location-services)
- [Authentification] (#authentication)
- [Bâtiment et édition] (#building-and-publishing)
- EAS (Expo Application Services)
- [Meilleures pratiques] (#best-practices)
Installation¶
Préalables¶
# Install Node.js (version 16 or later)
# Download from nodejs.org
# Verify Node.js installation
node --version
npm --version
# Install Yarn (optional but recommended)
npm install -g yarn
Expo CLI Installation¶
# Install Expo CLI globally
npm install -g @expo/cli
# Or with Yarn
yarn global add @expo/cli
# Verify installation
expo --version
# Login to Expo account (optional)
expo login
# Check current user
expo whoami
```_
### Appli Expo Go
```bash
# Download Expo Go from app stores:
# iOS: App Store - search "Expo Go"
# Android: Google Play Store - search "Expo Go"
# Alternative: Use development build
# For custom native code or third-party libraries
```_
## Commencer
### Créer un nouveau projet
```bash
# Create new Expo project
expo init MyApp
# Choose template:
# - blank: minimal app with just the essentials
# - blank (TypeScript): minimal app with TypeScript
# - tabs (TypeScript): several example screens and tabs
# - minimal: bare minimum files
# Navigate to project directory
cd MyApp
# Start development server
expo start
# Alternative: Create with specific template
expo init MyApp --template blank
expo init MyApp --template blank-typescript
expo init MyApp --template tabs
Modèles de projet¶
# Blank template
expo init MyApp --template blank
# TypeScript template
expo init MyApp --template blank-typescript
# Navigation template
expo init MyApp --template tabs
# Bare workflow (advanced)
expo init MyApp --template bare-minimum
# Create with npm
npx create-expo-app MyApp
# Create with specific SDK version
expo init MyApp --sdk-version 49.0.0
Structure du projet¶
Structure de base¶
MyApp/
├── App.js # Main app component
├── app.json # Expo configuration
├── package.json # Dependencies and scripts
├── babel.config.js # Babel configuration
├── assets/ # Images, fonts, etc.
│ ├── icon.png
│ ├── splash.png
│ └── adaptive-icon.png
├── components/ # Reusable components
├── screens/ # Screen components
├── navigation/ # Navigation configuration
├── services/ # API services
├── utils/ # Utility functions
└── constants/ # App constants
App.js Exemple¶
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
export default function App() {
return (
<View style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
pour Configuration¶
{
"expo": {
"name": "MyApp",
"slug": "myapp",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.yourcompany.myapp"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#FFFFFF"
},
"package": "com.yourcompany.myapp"
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}
Exposition CLI¶
Commandes de développement¶
# Start development server
expo start
# Start with specific options
expo start --clear # Clear cache
expo start --offline # Work offline
expo start --tunnel # Use tunnel connection
expo start --lan # Use LAN connection
expo start --localhost # Use localhost
# Platform-specific starts
expo start --ios # Open iOS simulator
expo start --android # Open Android emulator
expo start --web # Open web browser
# Install dependencies
expo install package-name
# Install specific version
expo install package-name@version
# Install multiple packages
expo install package1 package2 package3
Gestion de projet¶
# Check project status
expo doctor
# Upgrade Expo SDK
expo upgrade
# Eject from managed workflow (deprecated)
expo eject
# Prebuild (generate native code)
expo prebuild
# Clean project
expo start --clear
# Run on device
expo start --tunnel
# Generate app icons and splash screens
expo install expo-splash-screen
expo install @expo/vector-icons
Construire des commandes¶
# Build for iOS (legacy)
expo build:ios
# Build for Android (legacy)
expo build:android
# Build for web
expo build:web
# Check build status
expo build:status
# Download build
expo build:download
# Modern EAS Build
eas build --platform ios
eas build --platform android
eas build --platform all
Développement Flux de travail¶
Courir sur le périphérique¶
# Method 1: Expo Go app
# 1. Install Expo Go on your device
# 2. Run: expo start
# 3. Scan QR code with Expo Go (Android) or Camera (iOS)
# Method 2: Development build
# 1. Create development build: eas build --profile development
# 2. Install build on device
# 3. Run: expo start --dev-client
# Method 3: Simulator/Emulator
expo start --ios # iOS Simulator
expo start --android # Android Emulator
Rechargement à chaud¶
// Automatic hot reloading is enabled by default
// Changes to JavaScript files trigger automatic reload
// Disable hot reloading in app.json
{
"expo": {
"packagerOpts": {
"config": "metro.config.js"
}
}
}
// Manual reload
// Shake device or press Cmd+R (iOS) / Cmd+M (Android)
Déboguement¶
# Enable remote debugging
# Shake device > Debug Remote JS
# Use Flipper for debugging
npm install --save-dev react-native-flipper
# Debug with VS Code
# Install React Native Tools extension
# Console logging
console.log('Debug message');
console.warn('Warning message');
console.error('Error message');
# React Native Debugger
# Download from GitHub releases
# Start with: expo start
# Open React Native Debugger
Composantes de base¶
Composantes de base¶
import React from 'react';
import {
View,
Text,
Image,
ScrollView,
TouchableOpacity,
TextInput,
StyleSheet
} from 'react-native';
const BasicComponents = () => {
const [text, setText] = React.useState('');
return (
<ScrollView style={styles.container}>
<View style={styles.section}>
<Text style={styles.title}>Hello, Expo!</Text>
<Image
source={{ uri: 'https://example.com/image.jpg' }}
style={styles.image}
/>
<TextInput
style={styles.input}
placeholder="Enter text here"
value={text}
onChangeText={setText}
/>
<TouchableOpacity
style={styles.button}
onPress={() => alert('Button pressed!')}
>
<Text style={styles.buttonText}>Press Me</Text>
</TouchableOpacity>
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
section: {
padding: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
},
image: {
width: 200,
height: 200,
marginBottom: 20,
},
input: {
borderWidth: 1,
borderColor: '#ccc',
padding: 10,
marginBottom: 20,
borderRadius: 5,
},
button: {
backgroundColor: '#007AFF',
padding: 15,
borderRadius: 5,
alignItems: 'center',
},
buttonText: {
color: 'white',
fontWeight: 'bold',
},
});
export default BasicComponents;
Listes et données¶
import React from 'react';
import {
FlatList,
SectionList,
Text,
View,
StyleSheet
} from 'react-native';
const ListExamples = () => {
const data = [
{ id: '1', title: 'Item 1' },
{ id: '2', title: 'Item 2' },
{ id: '3', title: 'Item 3' },
];
const sectionData = [
{
title: 'Section 1',
data: ['Item 1', 'Item 2'],
},
{
title: 'Section 2',
data: ['Item 3', 'Item 4'],
},
];
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text>{item.title}</Text>
</View>
);
const renderSectionItem = ({ item }) => (
<View style={styles.item}>
<Text>{item}</Text>
</View>
);
const renderSectionHeader = ({ section: { title } }) => (
<View style={styles.header}>
<Text style={styles.headerText}>{title}</Text>
</View>
);
return (
<View style={styles.container}>
<Text style={styles.title}>FlatList Example</Text>
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
style={styles.list}
/>
<Text style={styles.title}>SectionList Example</Text>
<SectionList
sections={sectionData}
renderItem={renderSectionItem}
renderSectionHeader={renderSectionHeader}
keyExtractor={(item, index) => item + index}
style={styles.list}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
title: {
fontSize: 18,
fontWeight: 'bold',
marginVertical: 10,
},
list: {
maxHeight: 200,
},
item: {
padding: 10,
borderBottomWidth: 1,
borderBottomColor: '#ccc',
},
header: {
backgroundColor: '#f0f0f0',
padding: 10,
},
headerText: {
fontWeight: 'bold',
},
});
export default ListExamples;
Navigation¶
Configuration de la navigation de réaction¶
# Install React Navigation
npm install @react-navigation/native
# Install dependencies for Expo
expo install react-native-screens react-native-safe-area-context
# Install navigators
npm install @react-navigation/stack
npm install @react-navigation/bottom-tabs
npm install @react-navigation/drawer
Navigateur de piles¶
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { View, Text, Button } from 'react-native';
const Stack = createStackNavigator();
const HomeScreen = ({ navigation }) => (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home Screen</Text>
<Button
title="Go to Details"
onPress={() => navigation.navigate('Details', { itemId: 86 })}
/>
</View>
);
const DetailsScreen = ({ route, navigation }) => {
const { itemId } = route.params;
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Details Screen</Text>
<Text>Item ID: {itemId}</Text>
<Button
title="Go back"
onPress={() => navigation.goBack()}
/>
</View>
);
};
const App = () => {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: 'My Home' }}
/>
<Stack.Screen
name="Details"
component={DetailsScreen}
options={({ route }) => ({ title: `Item ${route.params.itemId}` })}
/>
</Stack.Navigator>
</NavigationContainer>
);
};
export default App;
Navigateur d'onglets¶
import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
const Tab = createBottomTabNavigator();
const HomeScreen = () => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Home!</Text>
</View>
);
const SettingsScreen = () => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Settings!</Text>
</View>
);
const TabNavigator = () => {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName;
if (route.name === 'Home') {
iconName = focused ? 'home' : 'home-outline';
} else if (route.name === 'Settings') {
iconName = focused ? 'settings' : 'settings-outline';
}
return <Ionicons name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: 'tomato',
tabBarInactiveTintColor: 'gray',
})}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Settings" component={SettingsScreen} />
</Tab.Navigator>
);
};
export default TabNavigator;
Administration de l ' État¶
Crochets de réaction¶
import React, { useState, useEffect, useContext, useReducer } from 'react';
// useState Hook
const CounterComponent = () => {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
return (
<View>
<Text>Count: {count}</Text>
<Button title="+" onPress={() => setCount(count + 1)} />
<Button title="-" onPress={() => setCount(count - 1)} />
<TextInput
value={name}
onChangeText={setName}
placeholder="Enter name"
/>
</View>
);
};
// useEffect Hook
const DataComponent = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return <Text>Loading...</Text>;
}
return (
<View>
<Text>{JSON.stringify(data)}</Text>
</View>
);
};
// useContext Hook
const ThemeContext = React.createContext();
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
const ThemedComponent = () => {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<View style={{ backgroundColor: theme === 'light' ? '#fff' : '#333' }}>
<Text style={{ color: theme === 'light' ? '#000' : '#fff' }}>
Current theme: {theme}
</Text>
<Button title="Toggle Theme" onPress={toggleTheme} />
</View>
);
};
Redux Configuration¶
# Install Redux
npm install @reduxjs/toolkit react-redux
# Install Redux DevTools (optional)
npm install redux-devtools-extension
// store.js
import { configureStore, createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
},
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export const store = configureStore({
reducer: {
counter: counterSlice.reducer,
},
});
// App.js
import React from 'react';
import { Provider } from 'react-redux';
import { store } from './store';
import Counter from './Counter';
const App = () => {
return (
<Provider store={store}>
<Counter />
</Provider>
);
};
// Counter.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, incrementByAmount } from './store';
const Counter = () => {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<View>
<Text>Count: {count}</Text>
<Button title="+" onPress={() => dispatch(increment())} />
<Button title="-" onPress={() => dispatch(decrement())} />
<Button title="+5" onPress={() => dispatch(incrementByAmount(5))} />
</View>
);
};
Style¶
Feuille de style¶
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
const StyledComponent = () => {
return (
<View style={styles.container}>
<Text style={styles.title}>Styled Text</Text>
<View style={styles.box}>
<Text style={styles.boxText}>Box Content</Text>
</View>
<View style={[styles.box, styles.redBox]}>
<Text style={styles.boxText}>Red Box</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: '#f5f5f5',
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
marginBottom: 20,
textAlign: 'center',
},
box: {
backgroundColor: '#007AFF',
padding: 20,
marginVertical: 10,
borderRadius: 10,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
},
redBox: {
backgroundColor: '#FF3B30',
},
boxText: {
color: 'white',
fontSize: 16,
textAlign: 'center',
},
});
export default StyledComponent;
Conception sensible¶
import React from 'react';
import { View, Text, Dimensions, StyleSheet } from 'react-native';
const { width, height } = Dimensions.get('window');
const ResponsiveComponent = () => {
return (
<View style={styles.container}>
<View style={styles.responsiveBox}>
<Text>Responsive Box</Text>
</View>
<View style={styles.flexContainer}>
<View style={styles.flexItem}>
<Text>Item 1</Text>
</View>
<View style={styles.flexItem}>
<Text>Item 2</Text>
</View>
<View style={styles.flexItem}>
<Text>Item 3</Text>
</View>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
responsiveBox: {
width: width * 0.8,
height: height * 0.2,
backgroundColor: '#007AFF',
justifyContent: 'center',
alignItems: 'center',
marginBottom: 20,
},
flexContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
flexItem: {
flex: 1,
backgroundColor: '#34C759',
margin: 5,
padding: 20,
alignItems: 'center',
},
});
export default ResponsiveComponent;
Composants calqués (alternative)¶
import React from 'react';
import styled from 'styled-components/native';
const Container = styled.View`
flex: 1;
padding: 20px;
background-color: #f5f5f5;
`;
const Title = styled.Text`
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
text-align: center;
`;
const Button = styled.TouchableOpacity`
background-color: ${props => props.primary ? '#007AFF' : '#34C759'};
padding: 15px;
border-radius: 10px;
margin-vertical: 10px;
`;
const ButtonText = styled.Text`
color: white;
text-align: center;
font-weight: bold;
`;
const StyledComponentExample = () => {
return (
<Container>
<Title>Styled Components</Title>
<Button primary onPress={() => alert('Primary button')}>
<ButtonText>Primary Button</ButtonText>
</Button>
<Button onPress={() => alert('Secondary button')}>
<ButtonText>Secondary Button</ButtonText>
</Button>
</Container>
);
};
export default StyledComponentExample;
API et services¶
API Expo¶
import * as Device from 'expo-device';
import * as Battery from 'expo-battery';
import * as Network from 'expo-network';
import * as Application from 'expo-application';
const DeviceInfo = () => {
const [deviceInfo, setDeviceInfo] = useState({});
useEffect(() => {
getDeviceInfo();
}, []);
const getDeviceInfo = async () => {
const info = {
deviceName: Device.deviceName,
deviceType: Device.deviceType,
osName: Device.osName,
osVersion: Device.osVersion,
batteryLevel: await Battery.getBatteryLevelAsync(),
networkState: await Network.getNetworkStateAsync(),
appVersion: Application.nativeApplicationVersion,
};
setDeviceInfo(info);
};
return (
<View>
<Text>Device: {deviceInfo.deviceName}</Text>
<Text>OS: {deviceInfo.osName} {deviceInfo.osVersion}</Text>
<Text>Battery: {Math.round(deviceInfo.batteryLevel * 100)}%</Text>
<Text>Network: {deviceInfo.networkState?.type}</Text>
<Text>App Version: {deviceInfo.appVersion}</Text>
</View>
);
};
Demandes HTTP¶
import React, { useState, useEffect } from 'react';
// Using fetch
const FetchExample = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const postData = async (newPost) => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newPost),
});
const result = await response.json();
console.log('Posted:', result);
} catch (err) {
console.error('Error posting data:', err);
}
};
if (loading) return <Text>Loading...</Text>;
if (error) return <Text>Error: {error}</Text>;
return (
<FlatList
data={data}
keyExtractor={item => item.id.toString()}
renderItem={({ item }) => (
<View style={{ padding: 10 }}>
<Text style={{ fontWeight: 'bold' }}>{item.title}</Text>
<Text>{item.body}</Text>
</View>
)}
/>
);
};
// Using Axios (alternative)
// npm install axios
import axios from 'axios';
const AxiosExample = () => {
const [data, setData] = useState(null);
useEffect(() => {
fetchWithAxios();
}, []);
const fetchWithAxios = async () => {
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
setData(response.data);
} catch (error) {
console.error('Axios error:', error);
}
};
const postWithAxios = async (newPost) => {
try {
const response = await axios.post('https://jsonplaceholder.typicode.com/posts', newPost);
console.log('Posted with Axios:', response.data);
} catch (error) {
console.error('Axios post error:', error);
}
};
return (
<View>
{/* Render data */}
</View>
);
};
Notifications de poussée¶
Configuration¶
# Install Expo Notifications
expo install expo-notifications
# For bare workflow, additional setup required
# See: https://docs.expo.dev/push-notifications/overview/
Notifications de base¶
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { Platform } from 'react-native';
// Configure notifications
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: false,
shouldSetBadge: false,
}),
});
const NotificationExample = () => {
const [expoPushToken, setExpoPushToken] = useState('');
const [notification, setNotification] = useState(false);
useEffect(() => {
registerForPushNotificationsAsync().then(token => setExpoPushToken(token));
const notificationListener = Notifications.addNotificationReceivedListener(notification => {
setNotification(notification);
});
const responseListener = Notifications.addNotificationResponseReceivedListener(response => {
console.log(response);
});
return () => {
Notifications.removeNotificationSubscription(notificationListener);
Notifications.removeNotificationSubscription(responseListener);
};
}, []);
const schedulePushNotification = async () => {
await Notifications.scheduleNotificationAsync({
content: {
title: "You've got mail! 📬",
body: 'Here is the notification body',
data: { data: 'goes here' },
},
trigger: { seconds: 2 },
});
};
const sendPushNotification = async (expoPushToken) => {
const message = {
to: expoPushToken,
sound: 'default',
title: 'Original Title',
body: 'And here is the body!',
data: { someData: 'goes here' },
};
await fetch('https://exp.host/--/api/v2/push/send', {
method: 'POST',
headers: {
Accept: 'application/json',
'Accept-encoding': 'gzip, deflate',
'Content-Type': 'application/json',
},
body: JSON.stringify(message),
});
};
return (
<View>
<Text>Your expo push token: {expoPushToken}</Text>
<Button
title="Press to schedule a notification"
onPress={schedulePushNotification}
/>
<Button
title="Press to send a push notification"
onPress={() => sendPushNotification(expoPushToken)}
/>
</View>
);
};
async function registerForPushNotificationsAsync() {
let token;
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');
}
if (Platform.OS === 'android') {
Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#FF231F7C',
});
}
return token;
}
Système de fichiers¶
Opérations de fichiers¶
import * as FileSystem from 'expo-file-system';
const FileSystemExample = () => {
const [fileContent, setFileContent] = useState('');
const writeFile = async () => {
const fileUri = FileSystem.documentDirectory + 'test.txt';
const content = 'Hello, Expo FileSystem!';
try {
await FileSystem.writeAsStringAsync(fileUri, content);
alert('File written successfully!');
} catch (error) {
console.error('Error writing file:', error);
}
};
const readFile = async () => {
const fileUri = FileSystem.documentDirectory + 'test.txt';
try {
const content = await FileSystem.readAsStringAsync(fileUri);
setFileContent(content);
} catch (error) {
console.error('Error reading file:', error);
}
};
const deleteFile = async () => {
const fileUri = FileSystem.documentDirectory + 'test.txt';
try {
await FileSystem.deleteAsync(fileUri);
alert('File deleted successfully!');
setFileContent('');
} catch (error) {
console.error('Error deleting file:', error);
}
};
const getFileInfo = async () => {
const fileUri = FileSystem.documentDirectory + 'test.txt';
try {
const info = await FileSystem.getInfoAsync(fileUri);
console.log('File info:', info);
} catch (error) {
console.error('Error getting file info:', error);
}
};
const downloadFile = async () => {
const fileUri = FileSystem.documentDirectory + 'downloaded.jpg';
const downloadUrl = 'https://example.com/image.jpg';
try {
const { uri } = await FileSystem.downloadAsync(downloadUrl, fileUri);
console.log('Downloaded to:', uri);
} catch (error) {
console.error('Error downloading file:', error);
}
};
return (
<View style={{ padding: 20 }}>
<Button title="Write File" onPress={writeFile} />
<Button title="Read File" onPress={readFile} />
<Button title="Delete File" onPress={deleteFile} />
<Button title="Get File Info" onPress={getFileInfo} />
<Button title="Download File" onPress={downloadFile} />
{fileContent ? (
<View style={{ marginTop: 20 }}>
<Text>File Content:</Text>
<Text>{fileContent}</Text>
</View>
) : null}
</View>
);
};
Caméra et médias¶
Caméra¶
import React, { useState, useEffect, useRef } from 'react';
import { Camera } from 'expo-camera';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
const CameraExample = () => {
const [hasPermission, setHasPermission] = useState(null);
const [type, setType] = useState(Camera.Constants.Type.back);
const cameraRef = useRef(null);
useEffect(() => {
(async () => {
const { status } = await Camera.requestCameraPermissionsAsync();
setHasPermission(status === 'granted');
})();
}, []);
const takePicture = async () => {
if (cameraRef.current) {
const photo = await cameraRef.current.takePictureAsync();
console.log('Photo taken:', photo.uri);
// Save or process the photo
}
};
const recordVideo = async () => {
if (cameraRef.current) {
const video = await cameraRef.current.recordAsync();
console.log('Video recorded:', video.uri);
}
};
const stopRecording = () => {
if (cameraRef.current) {
cameraRef.current.stopRecording();
}
};
if (hasPermission === null) {
return <View />;
}
if (hasPermission === false) {
return <Text>No access to camera</Text>;
}
return (
<View style={styles.container}>
<Camera style={styles.camera} type={type} ref={cameraRef}>
<View style={styles.buttonContainer}>
<TouchableOpacity
style={styles.button}
onPress={() => {
setType(
type === Camera.Constants.Type.back
? Camera.Constants.Type.front
: Camera.Constants.Type.back
);
}}
>
<Text style={styles.text}>Flip</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={takePicture}>
<Text style={styles.text}>Take Photo</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={recordVideo}>
<Text style={styles.text}>Record</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={stopRecording}>
<Text style={styles.text}>Stop</Text>
</TouchableOpacity>
</View>
</Camera>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
camera: {
flex: 1,
},
buttonContainer: {
flex: 1,
backgroundColor: 'transparent',
flexDirection: 'row',
margin: 20,
},
button: {
flex: 0.1,
alignSelf: 'flex-end',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.5)',
padding: 10,
margin: 5,
},
text: {
fontSize: 18,
color: 'white',
},
});
export default CameraExample;
Récupérateur d'images¶
import * as ImagePicker from 'expo-image-picker';
import { Image } from 'react-native';
const ImagePickerExample = () => {
const [image, setImage] = useState(null);
const pickImage = async () => {
// Request permission
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
alert('Sorry, we need camera roll permissions to make this work!');
return;
}
// Pick image
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsEditing: true,
aspect: [4, 3],
quality: 1,
});
if (!result.cancelled) {
setImage(result.uri);
}
};
const takePhoto = async () => {
// Request permission
const { status } = await ImagePicker.requestCameraPermissionsAsync();
if (status !== 'granted') {
alert('Sorry, we need camera permissions to make this work!');
return;
}
// Take photo
const result = await ImagePicker.launchCameraAsync({
allowsEditing: true,
aspect: [4, 3],
quality: 1,
});
if (!result.cancelled) {
setImage(result.uri);
}
};
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button title="Pick an image from camera roll" onPress={pickImage} />
<Button title="Take a photo" onPress={takePhoto} />
{image && <Image source={{ uri: image }} style={{ width: 200, height: 200 }} />}
</View>
);
};
Services de localisation¶
Configuration de l'emplacement¶
import * as Location from 'expo-location';
const LocationExample = () => {
const [location, setLocation] = useState(null);
const [errorMsg, setErrorMsg] = useState(null);
useEffect(() => {
getCurrentLocation();
}, []);
const getCurrentLocation = 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 () => {
let { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
setErrorMsg('Permission to access location was denied');
return;
}
const subscription = await Location.watchPositionAsync(
{
accuracy: Location.Accuracy.High,
timeInterval: 1000,
distanceInterval: 1,
},
(location) => {
setLocation(location);
}
);
// Don't forget to unsubscribe when component unmounts
return () => subscription.remove();
};
const reverseGeocode = async () => {
if (location) {
const { latitude, longitude } = location.coords;
const address = await Location.reverseGeocodeAsync({
latitude,
longitude,
});
console.log('Address:', address);
}
};
const geocode = async (address) => {
const coords = await Location.geocodeAsync(address);
console.log('Coordinates:', coords);
};
let text = 'Waiting..';
if (errorMsg) {
text = errorMsg;
} else if (location) {
text = JSON.stringify(location);
}
return (
<View style={{ padding: 20 }}>
<Text>{text}</Text>
<Button title="Get Current Location" onPress={getCurrentLocation} />
<Button title="Watch Location" onPress={watchLocation} />
<Button title="Reverse Geocode" onPress={reverseGeocode} />
<Button
title="Geocode Address"
onPress={() => geocode('1600 Amphitheatre Parkway, Mountain View, CA')}
/>
</View>
);
};
Authentification¶
Exposition Autres¶
import * as AuthSession from 'expo-auth-session';
import * as WebBrowser from 'expo-web-browser';
WebBrowser.maybeCompleteAuthSession();
const AuthExample = () => {
const [user, setUser] = useState(null);
// Google OAuth
const [request, response, promptAsync] = AuthSession.useAuthRequest(
{
clientId: 'YOUR_GOOGLE_CLIENT_ID',
scopes: ['openid', 'profile', 'email'],
redirectUri: AuthSession.makeRedirectUri({
useProxy: true,
}),
},
{
authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
}
);
useEffect(() => {
if (response?.type === 'success') {
const { authentication } = response;
// Use the access token to get user info
fetchUserInfo(authentication.accessToken);
}
}, [response]);
const fetchUserInfo = async (token) => {
try {
const response = await fetch('https://www.googleapis.com/userinfo/v2/me', {
headers: { Authorization: `Bearer ${token}` },
});
const userInfo = await response.json();
setUser(userInfo);
} catch (error) {
console.error('Error fetching user info:', error);
}
};
const signIn = () => {
promptAsync();
};
const signOut = () => {
setUser(null);
};
return (
<View style={{ padding: 20 }}>
{user ? (
<View>
<Text>Welcome, {user.name}!</Text>
<Text>Email: {user.email}</Text>
<Button title="Sign Out" onPress={signOut} />
</View>
) : (
<Button
disabled={!request}
title="Sign in with Google"
onPress={signIn}
/>
)}
</View>
);
};
Authentification de la base de feu¶
import { initializeApp } from 'firebase/app';
import {
getAuth,
signInWithEmailAndPassword,
createUserWithEmailAndPassword,
signOut,
onAuthStateChanged
} from 'firebase/auth';
const firebaseConfig = {
// Your Firebase config
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const FirebaseAuthExample = () => {
const [user, setUser] = useState(null);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
setUser(user);
});
return unsubscribe;
}, []);
const signUp = async () => {
try {
await createUserWithEmailAndPassword(auth, email, password);
} catch (error) {
alert(error.message);
}
};
const signIn = async () => {
try {
await signInWithEmailAndPassword(auth, email, password);
} catch (error) {
alert(error.message);
}
};
const handleSignOut = async () => {
try {
await signOut(auth);
} catch (error) {
alert(error.message);
}
};
return (
<View style={{ padding: 20 }}>
{user ? (
<View>
<Text>Welcome, {user.email}!</Text>
<Button title="Sign Out" onPress={handleSignOut} />
</View>
) : (
<View>
<TextInput
placeholder="Email"
value={email}
onChangeText={setEmail}
style={{ borderWidth: 1, padding: 10, marginBottom: 10 }}
/>
<TextInput
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
style={{ borderWidth: 1, padding: 10, marginBottom: 10 }}
/>
<Button title="Sign In" onPress={signIn} />
<Button title="Sign Up" onPress={signUp} />
</View>
)}
</View>
);
};
Bâtiment et édition¶
Expo Build (Legacy)¶
# Build for iOS
expo build:ios
# Build for Android
expo build:android
# Build for web
expo build:web
# Check build status
expo build:status
# Download build
expo build:download
# Publish to Expo
expo publish
# Set release channel
expo publish --release-channel production
Déploiement de l'App Store¶
# iOS App Store
# 1. Build IPA: expo build:ios
# 2. Download IPA
# 3. Upload to App Store Connect using Transporter or Xcode
# Google Play Store
# 1. Build APK/AAB: expo build:android
# 2. Download APK/AAB
# 3. Upload to Google Play Console
EAS (Expo Application Services)¶
EAS Configuration¶
# Install EAS CLI
npm install -g eas-cli
# Login to Expo account
eas login
# Initialize EAS in project
eas build:configure
# Create development build
eas build --profile development --platform ios
eas build --profile development --platform android
# Create production build
eas build --profile production --platform ios
eas build --profile production --platform android
# Build for all platforms
eas build --platform all
EAS Configuration¶
// eas.json
{
"cli": {
"version": ">= 2.0.0"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": {
"resourceClass": "m1-medium"
}
},
"preview": {
"distribution": "internal"
},
"production": {
"ios": {
"resourceClass": "m1-medium"
}
}
},
"submit": {
"production": {
"ios": {
"appleId": "your-apple-id@example.com",
"ascAppId": "1234567890",
"appleTeamId": "ABCDEF1234"
},
"android": {
"serviceAccountKeyPath": "../path/to/api-key.json",
"track": "internal"
}
}
}
}
EAS Soumettre¶
# Submit to app stores
eas submit --platform ios
eas submit --platform android
# Submit specific build
eas submit --platform ios --id your-build-id
# Submit with custom configuration
eas submit --platform android --profile production
Mise à jour du SAE¶
# Install EAS Update
expo install expo-updates
# Configure EAS Update
eas update:configure
# Publish update
eas update --branch main --message "Bug fixes"
# Publish to specific channel
eas update --channel production --message "Production update"
# View updates
eas update:list
# Delete update
eas update:delete --group-id your-group-id
Meilleures pratiques¶
Structure du projet¶
src/
├── components/ # Reusable components
│ ├── common/ # Common UI components
│ ├── forms/ # Form components
│ └── navigation/ # Navigation components
├── screens/ # Screen components
│ ├── auth/ # Authentication screens
│ ├── main/ # Main app screens
│ └── settings/ # Settings screens
├── services/ # API services
│ ├── api.js # API configuration
│ ├── auth.js # Authentication service
│ └── storage.js # Storage service
├── utils/ # Utility functions
│ ├── constants.js # App constants
│ ├── helpers.js # Helper functions
│ └── validation.js # Validation functions
├── hooks/ # Custom hooks
├── context/ # React context
└── assets/ # Static assets
├── images/
├── fonts/
└── icons/
Optimisation des performances¶
// Use React.memo for expensive components
const ExpensiveComponent = React.memo(({ data }) => {
return (
<View>
{/* Expensive rendering */}
</View>
);
});
// Use useMemo for expensive calculations
const ExpensiveCalculation = ({ items }) => {
const expensiveValue = useMemo(() => {
return items.reduce((acc, item) => acc + item.value, 0);
}, [items]);
return <Text>{expensiveValue}</Text>;
};
// Use useCallback for event handlers
const ListComponent = ({ items, onItemPress }) => {
const handlePress = useCallback((item) => {
onItemPress(item);
}, [onItemPress]);
return (
<FlatList
data={items}
renderItem={({ item }) => (
<TouchableOpacity onPress={() => handlePress(item)}>
<Text>{item.title}</Text>
</TouchableOpacity>
)}
keyExtractor={item => item.id}
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
/>
);
};
// Optimize images
const OptimizedImage = ({ source, style }) => {
return (
<Image
source={source}
style={style}
resizeMode="cover"
fadeDuration={0}
/>
);
};
Gestion des erreurs¶
// Error Boundary
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Something went wrong.</Text>
<Button
title="Try again"
onPress={() => this.setState({ hasError: false })}
/>
</View>
);
}
return this.props.children;
}
}
// Global error handler
const setGlobalErrorHandler = () => {
const defaultHandler = ErrorUtils.getGlobalHandler();
ErrorUtils.setGlobalHandler((error, isFatal) => {
console.error('Global error:', error);
// Log to crash reporting service
defaultHandler(error, isFatal);
});
};
// Async error handling
const safeAsyncFunction = async () => {
try {
const result = await riskyAsyncOperation();
return result;
} catch (error) {
console.error('Async error:', error);
// Handle error appropriately
throw error;
}
};
Pratiques exemplaires en matière de sécurité¶
// Secure storage
import * as SecureStore from 'expo-secure-store';
const storeSecureData = async (key, value) => {
await SecureStore.setItemAsync(key, value);
};
const getSecureData = async (key) => {
return await SecureStore.getItemAsync(key);
};
// API key protection
// Never store API keys in code
// Use environment variables or secure storage
// Input validation
const validateEmail = (email) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const sanitizeInput = (input) => {
return input.trim().replace(/[<>]/g, '');
};
// Network security
const secureApiCall = async (url, options = {}) => {
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
// Add authentication headers
},
...options,
};
try {
const response = await fetch(url, defaultOptions);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('API call failed:', error);
throw error;
}
};
Résumé¶
Expo est une plateforme puissante qui simplifie Réagir au développement autochtone en fournissant :
- Développement rapide: Configuration rapide du projet et workflow de développement
- Rich API Access: Ensemble complet d'API natives sans code natif
- Applications universelles: Construire pour iOS, Android et web à partir d'une seule base de code
- ** Mises à jour en direct** : Mises à jour en mode push sans approbation du magasin d'applications
- Outils de développement: excellents outils de débogage et de test
- Services de construction: construction et déploiement en nuage avec EAS
- Communauté: grand écosystème et soutien communautaire actif
Expo est idéal pour le prototypage rapide, les MVP et les applications de production qui ne nécessitent pas une grande personnalisation native. Pour les applications qui ont besoin de modules natifs personnalisés, Expo fournit un flux de travail nu et développement construit pour maintenir la flexibilité tout en gardant l'expérience développeur lisse.