Expo
モバイルプラットフォーム
Expo
概要
Expoは、React Nativeの開発体験を革命的に向上させるプラットフォームです。ビルド、テスト、デプロイメントを大幅に簡素化し、クロスプラットフォームアプリ開発を加速します。2025年現在、React Native開発の事実上の標準ツールチェーンとして確立されており、EAS(Expo Application Services)により、CI/CDパイプラインの構築も劇的に簡単になりました。
詳細
Expoは、React Nativeエコシステムの中核を成すプラットフォームとして、開発者の生産性向上に重点を置いています。従来のReact Native開発で必要だった複雑な環境設定や、iOS/Android固有の開発環境の準備を最小限に抑え、JavaScriptとTypeScriptの知識だけで本格的なネイティブアプリ開発を可能にします。
2025年の最新アップデートでは、Web開発者にとって特に親しみやすいワークフローを提供し、Expo Go アプリを使った即座のプレビュー機能により、コードの変更をリアルタイムで物理デバイス上で確認できます。また、EAS Build サービスにより、クラウド上でのアプリビルドと配布が可能になり、開発チームの規模に関係なく、プロフェッショナルなアプリ開発ワークフローを実現します。
メリット・デメリット
メリット
- 開発効率の飛躍的向上: Expo CLIにより、プロジェクトのセットアップから本番デプロイまでが数分で完了
- クロスプラットフォーム対応: iOS、Android、Webアプリを単一のコードベースで開発
- 豊富なAPIライブラリ: カメラ、GPS、プッシュ通知など、50以上のネイティブ機能への統一されたアクセス
- ホットリロード: コード変更の即座の反映により、開発サイクルを大幅に短縮
- OTA(Over The Air)更新: アプリストア審査を経ずに、JavaScript部分の更新を配信
- EAS統合: クラウドベースのビルド、テスト、配布サービスとの完全統合
デメリット
- React Native依存: React Nativeの制限や問題の影響を受ける可能性
- カスタムネイティブコード: 特殊なネイティブ機能が必要な場合、追加の設定が必要
- アプリサイズ: Expo SDKにより、アプリのサイズが若干増加する場合がある
- プラットフォーム依存性: Expoエコシステムへの依存度が高い
参考ページ
書き方の例
プロジェクトセットアップと初期化
# Expo CLIのインストール
npm install -g @expo/cli
# 新規プロジェクト作成
npx create-expo-app MyMobileApp
# プロジェクトディレクトリに移動
cd MyMobileApp
# 開発サーバー起動
npx expo start
認証実装(Firebase Auth統合)
// App.tsx
import { initializeApp } from 'firebase/app';
import { getAuth, signInWithEmailAndPassword, createUserWithEmailAndPassword } from 'firebase/auth';
import { useState } from 'react';
import { View, TextInput, Button, Alert } from 'react-native';
const firebaseConfig = {
// Firebase設定
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
export default function AuthScreen() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const signIn = async () => {
try {
await signInWithEmailAndPassword(auth, email, password);
Alert.alert('成功', 'ログインしました');
} catch (error) {
Alert.alert('エラー', error.message);
}
};
const signUp = async () => {
try {
await createUserWithEmailAndPassword(auth, email, password);
Alert.alert('成功', 'アカウントを作成しました');
} catch (error) {
Alert.alert('エラー', error.message);
}
};
return (
<View style={{ padding: 20 }}>
<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="サインイン" onPress={signIn} />
<Button title="サインアップ" onPress={signUp} />
</View>
);
}
バックエンド統合(API通信)
// services/api.ts
const API_BASE_URL = 'https://your-api.com/api';
interface User {
id: string;
name: string;
email: string;
}
class ApiService {
private async request(endpoint: string, options: RequestInit = {}) {
const url = `${API_BASE_URL}${endpoint}`;
const config = {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
};
const response = await fetch(url, config);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
async getUsers(): Promise<User[]> {
return this.request('/users');
}
async createUser(userData: Partial<User>): Promise<User> {
return this.request('/users', {
method: 'POST',
body: JSON.stringify(userData),
});
}
async updateUser(id: string, userData: Partial<User>): Promise<User> {
return this.request(`/users/${id}`, {
method: 'PUT',
body: JSON.stringify(userData),
});
}
}
export const apiService = new ApiService();
// 使用例
import { useEffect, useState } from 'react';
import { FlatList, Text, View } from 'react-native';
export default function UsersList() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadUsers();
}, []);
const loadUsers = async () => {
try {
const data = await apiService.getUsers();
setUsers(data);
} catch (error) {
console.error('Failed to load users:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return <Text>読み込み中...</Text>;
}
return (
<FlatList
data={users}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={{ padding: 10 }}>
<Text>{item.name}</Text>
<Text>{item.email}</Text>
</View>
)}
/>
);
}
プッシュ通知実装
// services/notifications.ts
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { Platform } from 'react-native';
// 通知設定
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: false,
shouldSetBadge: false,
}),
});
export class NotificationService {
static async 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('プッシュ通知の許可が必要です');
return;
}
token = (await Notifications.getExpoPushTokenAsync()).data;
} else {
alert('実際のデバイスでプッシュ通知をテストしてください');
}
return token;
}
static async sendLocalNotification(title: string, body: string) {
await Notifications.scheduleNotificationAsync({
content: {
title,
body,
},
trigger: null,
});
}
}
// App.tsx での使用例
import { useEffect } from 'react';
export default function App() {
useEffect(() => {
NotificationService.registerForPushNotificationsAsync().then(token => {
console.log('Push token:', token);
// サーバーにトークンを送信
});
// 通知受信時の処理
const subscription = Notifications.addNotificationReceivedListener(notification => {
console.log('Notification received:', notification);
});
// 通知タップ時の処理
const responseSubscription = Notifications.addNotificationResponseReceivedListener(response => {
console.log('Notification tapped:', response);
});
return () => {
subscription.remove();
responseSubscription.remove();
};
}, []);
return (
// アプリのコンテンツ
);
}
アナリティクス統合(Firebase Analytics)
// services/analytics.ts
import { initializeApp } from 'firebase/app';
import { getAnalytics, logEvent, setUserId, setUserProperties } from 'firebase/analytics';
const firebaseConfig = {
// Firebase設定
};
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);
export class AnalyticsService {
static logCustomEvent(eventName: string, parameters?: Record<string, any>) {
logEvent(analytics, eventName, parameters);
}
static logScreenView(screenName: string) {
logEvent(analytics, 'screen_view', {
screen_name: screenName,
screen_class: screenName,
});
}
static logUserAction(action: string, item?: string) {
logEvent(analytics, 'user_action', {
action,
item,
timestamp: new Date().toISOString(),
});
}
static setUser(userId: string, properties?: Record<string, any>) {
setUserId(analytics, userId);
if (properties) {
setUserProperties(analytics, properties);
}
}
static logPurchase(transactionId: string, value: number, currency: string) {
logEvent(analytics, 'purchase', {
transaction_id: transactionId,
value,
currency,
});
}
}
// hooks/useAnalytics.ts - React Hook
import { useEffect } from 'react';
import { AnalyticsService } from '../services/analytics';
export const useScreenTracking = (screenName: string) => {
useEffect(() => {
AnalyticsService.logScreenView(screenName);
}, [screenName]);
};
// コンポーネントでの使用例
import { useScreenTracking } from '../hooks/useAnalytics';
export default function HomeScreen() {
useScreenTracking('Home');
const handleButtonPress = () => {
AnalyticsService.logUserAction('button_press', 'home_cta');
// ボタンアクション
};
return (
// UIコンテンツ
);
}
本番環境デプロイ(EAS Build)
# EAS CLI インストール
npm install -g eas-cli
# EAS にログイン
eas login
# プロジェクト設定
eas build:configure
# Development Build(開発用)
eas build --platform android --profile development
eas build --platform ios --profile development
# Production Build(本番用)
eas build --platform android --profile production
eas build --platform ios --profile production
# App Store / Google Play Store への自動提出
eas submit --platform android
eas submit --platform ios
// eas.json - EAS設定ファイル
{
"cli": {
"version": ">= 7.8.0"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": {
"resourceClass": "m-medium"
}
},
"preview": {
"distribution": "internal",
"ios": {
"simulator": true
}
},
"production": {
"ios": {
"resourceClass": "m-medium"
}
}
},
"submit": {
"production": {
"ios": {
"appleId": "[email protected]",
"ascAppId": "1234567890",
"appleTeamId": "ABCDEFG123"
},
"android": {
"serviceAccountKeyPath": "./path-to-google-service-account.json",
"track": "production"
}
}
}
}
// app.config.ts - アプリ設定
export default {
expo: {
name: "My Mobile App",
slug: "my-mobile-app",
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.mymobileapp"
},
android: {
adaptiveIcon: {
foregroundImage: "./assets/adaptive-icon.png",
backgroundColor: "#FFFFFF"
},
package: "com.yourcompany.mymobileapp"
},
web: {
favicon: "./assets/favicon.png"
},
extra: {
eas: {
projectId: "your-eas-project-id"
}
},
updates: {
url: "https://u.expo.dev/your-eas-project-id"
},
runtimeVersion: {
policy: "sdkVersion"
}
}
};