React Native
モバイルプラットフォーム
React Native
概要
React Nativeは、Meta(旧Facebook)が開発するクロスプラットフォームモバイル開発フレームワークとして、Reactの宣言的UIパラダイムとJavaScript/TypeScriptによるネイティブアプリ開発を実現します。「Learn Once, Write Anywhere」の哲学のもと、iOS・Android両プラットフォームで真のネイティブパフォーマンスを提供しながら、Web開発者にとって親しみやすい開発体験を提供します。2025年現在、New Architecture(TurboModules + Fabric)の安定化、React 18との完全統合、TypeScript First開発体験、そして強化されたパフォーマンス最適化により、エンタープライズ級アプリケーション開発の最前線プラットフォームとして進化しています。
詳細
React Native 2025年版は、New Architecture の完全安定化により、従来のBridgeベースの制限を突破し、JSI(JavaScript Interface)による直接的なネイティブモジュール通信とFabricレンダラーによる高速UI更新を実現しています。特に注目すべきは、React 18のConcurrent Featuresとの深い統合により、Suspense、Transitions、自動バッチングなどの最新React機能をモバイルアプリでフル活用できることです。Metro bundlerの高速化、Flipper統合の強化、Expo Dev Toolsとの連携、そしてCodegen による自動型生成により、開発効率とアプリケーション品質の両立を図る包括的な開発プラットフォームを提供しています。
主な特徴
- New Architecture: TurboModules + Fabricによる高速レンダリング
- React 18統合: Concurrent Features、Suspense、Transitionsサポート
- TypeScript First: 完全な型安全性と優れた開発体験
- Hot Reloading: 瞬時のコード変更反映とデバッグ効率
- ネイティブモジュール: iOS・Android固有機能への直接アクセス
- 豊富なエコシステム: 膨大なライブラリとコミュニティサポート
2025年の最新機能
- New Architecture安定版: TurboModules、Fabric、JSIの本格導入
- React 18完全対応: Concurrent Features、Automatic Batchingサポート
- TypeScript 5.0対応: 最新型システムと開発ツール統合
- Performance Optimizations: 起動時間50%短縮、メモリ使用量削減
- Enhanced Developer Experience: Flipper v2、Metro 改良、デバッグ強化
- Web Platform Alignment: React Native Web との統合強化
メリット・デメリット
メリット
- Web開発者にとって学習コストが低いReactベース開発
- iOS・Android間での高いコード共有率(70-90%)
- 真のネイティブパフォーマンスとUI体験
- 豊富なサードパーティライブラリとコミュニティ
- Hot Reloadingによる高速開発サイクル
- Meta による継続的な開発と大企業での実績
- Expo との統合による開発効率化
デメリット
- 大規模アプリでの複雑性管理の難しさ
- New Architecture 移行の学習コストと互換性問題
- ネイティブモジュール開発にはプラットフォーム固有知識が必要
- アプリサイズがネイティブ開発より大きくなる傾向
- プラットフォーム固有UIの完全な再現に制限
- デバッグの複雑さ(JavaScript、Native Bridge、ネイティブコード)
参考ページ
書き方の例
セットアップと基本設定
# React Native CLI のインストール
npm install -g react-native-cli
# 新しいプロジェクトの作成
npx react-native init MyAwesomeApp --template react-native-template-typescript
# プロジェクトディレクトリに移動
cd MyAwesomeApp
# 依存関係のインストール
npm install
# iOS用の CocoaPods 依存関係インストール(macOSのみ)
cd ios && pod install && cd ..
# Android エミュレータでの実行
npx react-native run-android
# iOS シミュレータでの実行(macOSのみ)
npx react-native run-ios
# Metro bundler の手動起動
npx react-native start
# 新規プロジェクト用の必須ライブラリインストール
npm install @react-navigation/native @react-navigation/stack @react-navigation/bottom-tabs
npm install react-native-screens react-native-safe-area-context
npm install react-native-gesture-handler react-native-reanimated
npm install @reduxjs/toolkit react-redux
npm install react-native-async-storage/async-storage
// App.tsx - メインアプリケーションコンポーネント
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { store, persistor } from './src/store/store';
import HomeScreen from './src/screens/HomeScreen';
import ProfileScreen from './src/screens/ProfileScreen';
import LoadingScreen from './src/components/LoadingScreen';
export type RootStackParamList = {
Home: undefined;
Profile: { userId: string };
Settings: undefined;
};
const Stack = createStackNavigator<RootStackParamList>();
const App: React.FC = () => {
return (
<Provider store={store}>
<PersistGate loading={<LoadingScreen />} persistor={persistor}>
<SafeAreaProvider>
<NavigationContainer>
<Stack.Navigator
initialRouteName="Home"
screenOptions={{
headerStyle: {
backgroundColor: '#6200EE',
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
}}
>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: 'ホーム' }}
/>
<Stack.Screen
name="Profile"
component={ProfileScreen}
options={({ route }) => ({
title: `プロフィール: ${route.params.userId}`
})}
/>
</Stack.Navigator>
</NavigationContainer>
</SafeAreaProvider>
</PersistGate>
</Provider>
);
};
export default App;
認証とユーザー管理
// src/services/AuthService.ts - 認証サービス
import AsyncStorage from '@react-native-async-storage/async-storage';
import { GoogleSignin, statusCodes } from '@react-native-google-signin/google-signin';
import { LoginManager, AccessToken } from 'react-native-fbsdk-next';
import auth, { FirebaseAuthTypes } from '@react-native-firebase/auth';
export interface User {
id: string;
email: string;
name: string;
avatar?: string;
provider: 'email' | 'google' | 'facebook' | 'apple';
emailVerified: boolean;
createdAt: Date;
lastLoginAt: Date;
}
export interface LoginCredentials {
email: string;
password: string;
}
export interface RegisterCredentials extends LoginCredentials {
name: string;
confirmPassword: string;
}
class AuthService {
private static instance: AuthService;
private currentUser: User | null = null;
private constructor() {
this.initializeGoogleSignIn();
this.setupAuthStateListener();
}
public static getInstance(): AuthService {
if (!AuthService.instance) {
AuthService.instance = new AuthService();
}
return AuthService.instance;
}
// Google Sign-In の初期化
private initializeGoogleSignIn(): void {
GoogleSignin.configure({
webClientId: 'your-web-client-id.googleusercontent.com',
offlineAccess: true,
hostedDomain: '',
forceCodeForRefreshToken: true,
});
}
// 認証状態リスナーの設定
private setupAuthStateListener(): void {
auth().onAuthStateChanged(async (firebaseUser) => {
if (firebaseUser) {
await this.handleUserSession(firebaseUser);
} else {
this.currentUser = null;
await AsyncStorage.removeItem('user_token');
}
});
}
// ユーザーセッション処理
private async handleUserSession(firebaseUser: FirebaseAuthTypes.User): Promise<void> {
try {
const idToken = await firebaseUser.getIdToken();
await AsyncStorage.setItem('user_token', idToken);
this.currentUser = {
id: firebaseUser.uid,
email: firebaseUser.email!,
name: firebaseUser.displayName || 'Unknown User',
avatar: firebaseUser.photoURL || undefined,
provider: this.getAuthProvider(firebaseUser),
emailVerified: firebaseUser.emailVerified,
createdAt: firebaseUser.metadata.creationTime ? new Date(firebaseUser.metadata.creationTime) : new Date(),
lastLoginAt: firebaseUser.metadata.lastSignInTime ? new Date(firebaseUser.metadata.lastSignInTime) : new Date(),
};
} catch (error) {
console.error('Handle user session error:', error);
}
}
// プロバイダー判定
private getAuthProvider(user: FirebaseAuthTypes.User): User['provider'] {
const providerId = user.providerData[0]?.providerId;
switch (providerId) {
case 'google.com':
return 'google';
case 'facebook.com':
return 'facebook';
case 'apple.com':
return 'apple';
default:
return 'email';
}
}
// メール・パスワード登録
async registerWithEmail(credentials: RegisterCredentials): Promise<User> {
try {
if (credentials.password !== credentials.confirmPassword) {
throw new Error('パスワードが一致しません');
}
if (credentials.password.length < 8) {
throw new Error('パスワードは8文字以上である必要があります');
}
const userCredential = await auth().createUserWithEmailAndPassword(
credentials.email,
credentials.password
);
await userCredential.user.updateProfile({
displayName: credentials.name,
});
await userCredential.user.sendEmailVerification();
return this.currentUser!;
} catch (error: any) {
console.error('Register error:', error);
throw new Error(this.getFirebaseErrorMessage(error.code));
}
}
// メール・パスワードログイン
async loginWithEmail(credentials: LoginCredentials): Promise<User> {
try {
await auth().signInWithEmailAndPassword(credentials.email, credentials.password);
return this.currentUser!;
} catch (error: any) {
console.error('Login error:', error);
throw new Error(this.getFirebaseErrorMessage(error.code));
}
}
// Google ログイン
async loginWithGoogle(): Promise<User> {
try {
await GoogleSignin.hasPlayServices();
const { idToken } = await GoogleSignin.signIn();
const googleCredential = auth.GoogleAuthProvider.credential(idToken);
await auth().signInWithCredential(googleCredential);
return this.currentUser!;
} catch (error: any) {
console.error('Google login error:', error);
if (error.code === statusCodes.SIGN_IN_CANCELLED) {
throw new Error('ログインがキャンセルされました');
} else if (error.code === statusCodes.IN_PROGRESS) {
throw new Error('ログイン処理中です');
} else if (error.code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) {
throw new Error('Google Play Services が利用できません');
}
throw new Error('Googleログインに失敗しました');
}
}
// Facebook ログイン
async loginWithFacebook(): Promise<User> {
try {
const result = await LoginManager.logInWithPermissions(['public_profile', 'email']);
if (result.isCancelled) {
throw new Error('ログインがキャンセルされました');
}
const data = await AccessToken.getCurrentAccessToken();
if (!data) {
throw new Error('Facebook アクセストークンの取得に失敗しました');
}
const facebookCredential = auth.FacebookAuthProvider.credential(data.accessToken);
await auth().signInWithCredential(facebookCredential);
return this.currentUser!;
} catch (error) {
console.error('Facebook login error:', error);
throw error;
}
}
// パスワードリセット
async resetPassword(email: string): Promise<void> {
try {
await auth().sendPasswordResetEmail(email);
} catch (error: any) {
console.error('Password reset error:', error);
throw new Error(this.getFirebaseErrorMessage(error.code));
}
}
// メール確認の再送信
async resendEmailVerification(): Promise<void> {
try {
const user = auth().currentUser;
if (user && !user.emailVerified) {
await user.sendEmailVerification();
}
} catch (error) {
console.error('Resend email verification error:', error);
throw error;
}
}
// ログアウト
async logout(): Promise<void> {
try {
await GoogleSignin.signOut();
await LoginManager.logOut();
await auth().signOut();
this.currentUser = null;
await AsyncStorage.removeItem('user_token');
} catch (error) {
console.error('Logout error:', error);
throw error;
}
}
// 現在のユーザー取得
getCurrentUser(): User | null {
return this.currentUser;
}
// ログイン状態確認
isLoggedIn(): boolean {
return this.currentUser !== null && auth().currentUser !== null;
}
// Firebaseエラーメッセージの変換
private getFirebaseErrorMessage(errorCode: string): string {
const errorMessages: { [key: string]: string } = {
'auth/user-not-found': 'ユーザーが見つかりません',
'auth/wrong-password': 'パスワードが間違っています',
'auth/email-already-in-use': 'このメールアドレスは既に使用されています',
'auth/weak-password': 'パスワードが弱すぎます',
'auth/invalid-email': 'メールアドレスの形式が正しくありません',
'auth/too-many-requests': 'リクエストが多すぎます。しばらく待ってから再試行してください',
'auth/network-request-failed': 'ネットワークエラーが発生しました',
};
return errorMessages[errorCode] || '認証エラーが発生しました';
}
}
export default AuthService.getInstance();
バックエンド統合とAPIアクセス
// src/services/ApiService.ts - APIサービス
import AsyncStorage from '@react-native-async-storage/async-storage';
export interface ApiConfig {
baseURL: string;
timeout: number;
retryAttempts: number;
retryDelay: number;
}
export interface ApiResponse<T> {
data: T;
status: number;
message?: string;
errors?: string[];
}
export interface PaginatedResponse<T> {
data: T[];
pagination: {
page: number;
limit: number;
total: number;
pages: number;
};
}
class ApiService {
private config: ApiConfig;
private authToken: string | null = null;
constructor(config: ApiConfig) {
this.config = config;
this.loadAuthToken();
}
// 認証トークンの読み込み
private async loadAuthToken(): Promise<void> {
try {
this.authToken = await AsyncStorage.getItem('user_token');
} catch (error) {
console.error('Load auth token error:', error);
}
}
// 認証トークンの設定
public setAuthToken(token: string | null): void {
this.authToken = token;
}
// リクエストヘッダーの構築
private getHeaders(): Record<string, string> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
if (this.authToken) {
headers['Authorization'] = `Bearer ${this.authToken}`;
}
return headers;
}
// HTTP リクエストの実行
private async executeRequest<T>(
url: string,
options: RequestInit,
attempt: number = 1
): Promise<ApiResponse<T>> {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
const response = await fetch(`${this.config.baseURL}${url}`, {
...options,
headers: {
...this.getHeaders(),
...options.headers,
},
signal: controller.signal,
});
clearTimeout(timeoutId);
const responseData = await response.json();
if (!response.ok) {
throw new Error(responseData.message || `HTTP Error: ${response.status}`);
}
return {
data: responseData.data || responseData,
status: response.status,
message: responseData.message,
};
} catch (error: any) {
console.error(`API Request failed (attempt ${attempt}):`, error);
// リトライロジック
if (attempt < this.config.retryAttempts && this.shouldRetry(error)) {
await this.delay(this.config.retryDelay * attempt);
return this.executeRequest<T>(url, options, attempt + 1);
}
throw error;
}
}
// リトライ判定
private shouldRetry(error: any): boolean {
return (
error.name === 'AbortError' ||
error.message.includes('Network request failed') ||
error.message.includes('500') ||
error.message.includes('502') ||
error.message.includes('503')
);
}
// 遅延処理
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// GET リクエスト
async get<T>(url: string, params?: Record<string, any>): Promise<ApiResponse<T>> {
let requestUrl = url;
if (params) {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
searchParams.append(key, String(value));
}
});
requestUrl += `?${searchParams.toString()}`;
}
return this.executeRequest<T>(requestUrl, {
method: 'GET',
});
}
// POST リクエスト
async post<T>(url: string, data?: any): Promise<ApiResponse<T>> {
return this.executeRequest<T>(url, {
method: 'POST',
body: data ? JSON.stringify(data) : undefined,
});
}
// PUT リクエスト
async put<T>(url: string, data?: any): Promise<ApiResponse<T>> {
return this.executeRequest<T>(url, {
method: 'PUT',
body: data ? JSON.stringify(data) : undefined,
});
}
// PATCH リクエスト
async patch<T>(url: string, data?: any): Promise<ApiResponse<T>> {
return this.executeRequest<T>(url, {
method: 'PATCH',
body: data ? JSON.stringify(data) : undefined,
});
}
// DELETE リクエスト
async delete<T>(url: string): Promise<ApiResponse<T>> {
return this.executeRequest<T>(url, {
method: 'DELETE',
});
}
// ファイルアップロード
async uploadFile<T>(
url: string,
file: {
uri: string;
type: string;
name: string;
},
additionalData?: Record<string, string>
): Promise<ApiResponse<T>> {
const formData = new FormData();
formData.append('file', {
uri: file.uri,
type: file.type,
name: file.name,
} as any);
if (additionalData) {
Object.entries(additionalData).forEach(([key, value]) => {
formData.append(key, value);
});
}
const headers = this.getHeaders();
delete headers['Content-Type']; // Let the browser set the content-type for FormData
return this.executeRequest<T>(url, {
method: 'POST',
body: formData,
headers,
});
}
}
// デフォルト設定
const defaultConfig: ApiConfig = {
baseURL: __DEV__ ? 'http://localhost:3000/api' : 'https://api.yourapp.com',
timeout: 10000,
retryAttempts: 3,
retryDelay: 1000,
};
export const apiService = new ApiService(defaultConfig);
// 具体的なAPIエンドポイント
export interface Post {
id: string;
title: string;
content: string;
authorId: string;
authorName: string;
createdAt: string;
updatedAt: string;
likes: number;
comments: number;
tags: string[];
}
export interface User {
id: string;
email: string;
name: string;
avatar?: string;
bio?: string;
followers: number;
following: number;
}
export class PostApiService {
// 投稿一覧取得(ページネーション付き)
static async getPosts(page: number = 1, limit: number = 10): Promise<PaginatedResponse<Post>> {
const response = await apiService.get<PaginatedResponse<Post>>('/posts', {
page,
limit,
});
return response.data;
}
// 投稿詳細取得
static async getPost(postId: string): Promise<Post> {
const response = await apiService.get<Post>(`/posts/${postId}`);
return response.data;
}
// 投稿作成
static async createPost(postData: {
title: string;
content: string;
tags: string[];
}): Promise<Post> {
const response = await apiService.post<Post>('/posts', postData);
return response.data;
}
// 投稿更新
static async updatePost(postId: string, updates: Partial<Post>): Promise<Post> {
const response = await apiService.put<Post>(`/posts/${postId}`, updates);
return response.data;
}
// 投稿削除
static async deletePost(postId: string): Promise<void> {
await apiService.delete(`/posts/${postId}`);
}
// 投稿いいね
static async likePost(postId: string): Promise<void> {
await apiService.post(`/posts/${postId}/like`);
}
// 投稿いいね解除
static async unlikePost(postId: string): Promise<void> {
await apiService.delete(`/posts/${postId}/like`);
}
// 投稿検索
static async searchPosts(query: string, tags?: string[]): Promise<Post[]> {
const response = await apiService.get<Post[]>('/posts/search', {
q: query,
tags: tags?.join(','),
});
return response.data;
}
}
export class UserApiService {
// ユーザープロフィール取得
static async getUserProfile(userId: string): Promise<User> {
const response = await apiService.get<User>(`/users/${userId}`);
return response.data;
}
// プロフィール更新
static async updateProfile(updates: Partial<User>): Promise<User> {
const response = await apiService.put<User>('/users/profile', updates);
return response.data;
}
// アバター画像アップロード
static async uploadAvatar(imageUri: string): Promise<{ avatarUrl: string }> {
const response = await apiService.uploadFile<{ avatarUrl: string }>(
'/users/avatar',
{
uri: imageUri,
type: 'image/jpeg',
name: 'avatar.jpg',
}
);
return response.data;
}
// フォロー
static async followUser(userId: string): Promise<void> {
await apiService.post(`/users/${userId}/follow`);
}
// アンフォロー
static async unfollowUser(userId: string): Promise<void> {
await apiService.delete(`/users/${userId}/follow`);
}
// フォロワー一覧
static async getFollowers(userId: string): Promise<User[]> {
const response = await apiService.get<User[]>(`/users/${userId}/followers`);
return response.data;
}
// フォロー中ユーザー一覧
static async getFollowing(userId: string): Promise<User[]> {
const response = await apiService.get<User[]>(`/users/${userId}/following`);
return response.data;
}
}
プッシュ通知とメッセージング
// src/services/NotificationService.ts - プッシュ通知サービス
import PushNotification from 'react-native-push-notification';
import PushNotificationIOS from '@react-native-community/push-notification-ios';
import { Platform, PermissionsAndroid, Alert } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import messaging from '@react-native-firebase/messaging';
export interface LocalNotification {
id?: number;
title: string;
message: string;
date?: Date;
data?: any;
actions?: string[];
category?: string;
}
export interface RemoteNotificationData {
title?: string;
body?: string;
data?: { [key: string]: string };
badge?: number;
sound?: string;
}
class NotificationService {
private static instance: NotificationService;
private isInitialized = false;
private fcmToken: string | null = null;
private constructor() {}
public static getInstance(): NotificationService {
if (!NotificationService.instance) {
NotificationService.instance = new NotificationService();
}
return NotificationService.instance;
}
// 通知サービスの初期化
async initialize(): Promise<void> {
if (this.isInitialized) return;
try {
// 権限リクエスト
await this.requestPermissions();
// FCM初期化
await this.initializeFirebaseMessaging();
// ローカル通知設定
this.configureLocalNotifications();
// 通知ハンドラー設定
this.setupNotificationHandlers();
this.isInitialized = true;
console.log('Notification service initialized successfully');
} catch (error) {
console.error('Notification service initialization failed:', error);
throw error;
}
}
// 権限リクエスト
private async requestPermissions(): Promise<void> {
try {
if (Platform.OS === 'ios') {
const authStatus = await messaging().requestPermission();
const enabled =
authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
authStatus === messaging.AuthorizationStatus.PROVISIONAL;
if (!enabled) {
Alert.alert(
'通知許可',
'プッシュ通知を受信するには、設定から通知を許可してください。',
[
{ text: 'キャンセル', style: 'cancel' },
{ text: '設定を開く', onPress: () => this.openAppSettings() },
]
);
}
} else if (Platform.OS === 'android') {
if (Platform.Version >= 33) {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS
);
if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
Alert.alert(
'通知許可',
'プッシュ通知を受信するには通知を許可してください。'
);
}
}
}
} catch (error) {
console.error('Request permissions error:', error);
}
}
// Firebase Cloud Messaging 初期化
private async initializeFirebaseMessaging(): Promise<void> {
try {
// FCMトークン取得
this.fcmToken = await messaging().getToken();
console.log('FCM Token:', this.fcmToken);
// トークンをサーバーに送信
await this.registerToken(this.fcmToken);
// トークン更新リスナー
messaging().onTokenRefresh(async (token) => {
console.log('FCM Token refreshed:', token);
this.fcmToken = token;
await this.registerToken(token);
});
// フォアグラウンドメッセージハンドラー
messaging().onMessage(async (remoteMessage) => {
console.log('Foreground notification received:', remoteMessage);
this.handleForegroundNotification(remoteMessage);
});
// バックグラウンド・終了状態からの通知タップハンドラー
messaging().onNotificationOpenedApp((remoteMessage) => {
console.log('Notification opened app:', remoteMessage);
this.handleNotificationAction(remoteMessage);
});
// アプリ起動時の初期通知チェック
const initialNotification = await messaging().getInitialNotification();
if (initialNotification) {
console.log('App opened from notification:', initialNotification);
this.handleNotificationAction(initialNotification);
}
} catch (error) {
console.error('Firebase messaging initialization error:', error);
}
}
// ローカル通知設定
private configureLocalNotifications(): void {
PushNotification.configure({
// 通知受信時のコールバック
onNotification: (notification) => {
console.log('Local notification received:', notification);
if (notification.userInteraction) {
this.handleLocalNotificationAction(notification);
}
// iOS用の完了通知
if (Platform.OS === 'ios') {
notification.finish(PushNotificationIOS.FetchResult.NoData);
}
},
// 通知登録エラー
onRegistrationError: (err) => {
console.error('Push notification registration error:', err);
},
// iOS用権限リクエスト
requestPermissions: Platform.OS === 'ios',
// 通知タップ時の処理
onAction: (notification) => {
console.log('Notification action:', notification);
this.handleNotificationAction(notification);
},
// 通知チャンネル設定(Android)
channelId: 'default-channel-id',
});
// Android通知チャンネル作成
if (Platform.OS === 'android') {
PushNotification.createChannel(
{
channelId: 'default-channel-id',
channelName: 'Default Channel',
channelDescription: 'デフォルト通知チャンネル',
playSound: true,
soundName: 'default',
importance: 4,
vibrate: true,
},
(created) => console.log('Notification channel created:', created)
);
}
}
// 通知ハンドラー設定
private setupNotificationHandlers(): void {
// アプリ状態変更時の処理
if (Platform.OS === 'ios') {
PushNotificationIOS.addEventListener('notification', this.handleIOSNotification);
PushNotificationIOS.addEventListener('localNotification', this.handleIOSLocalNotification);
}
}
// iOS通知ハンドラー
private handleIOSNotification = (notification: any): void => {
console.log('iOS notification:', notification);
this.handleNotificationAction(notification);
};
// iOSローカル通知ハンドラー
private handleIOSLocalNotification = (notification: any): void => {
console.log('iOS local notification:', notification);
this.handleLocalNotificationAction(notification);
};
// フォアグラウンド通知処理
private handleForegroundNotification(remoteMessage: any): void {
const { notification, data } = remoteMessage;
// フォアグラウンドでもローカル通知として表示
PushNotification.localNotification({
title: notification?.title || 'New Message',
message: notification?.body || 'You have a new message',
playSound: true,
soundName: 'default',
userInfo: data,
channelId: 'default-channel-id',
});
}
// 通知アクション処理
private handleNotificationAction(notification: any): void {
const { data } = notification;
// 画面遷移やアクション実行
if (data?.screen) {
console.log(`Navigate to screen: ${data.screen}`);
// ナビゲーション処理をここに実装
}
if (data?.action) {
console.log(`Execute action: ${data.action}`);
// アクション処理をここに実装
}
}
// ローカル通知アクション処理
private handleLocalNotificationAction(notification: any): void {
console.log('Local notification action:', notification);
// ローカル通知固有の処理
}
// FCMトークンをサーバーに登録
private async registerToken(token: string): Promise<void> {
try {
await AsyncStorage.setItem('fcm_token', token);
// APIサーバーにトークンを送信
// await apiService.post('/users/fcm-token', { token });
console.log('FCM token registered successfully');
} catch (error) {
console.error('Register token error:', error);
}
}
// ローカル通知送信
sendLocalNotification(notification: LocalNotification): void {
const notificationConfig: any = {
title: notification.title,
message: notification.message,
playSound: true,
soundName: 'default',
userInfo: notification.data,
channelId: 'default-channel-id',
};
if (notification.id) {
notificationConfig.id = notification.id;
}
if (notification.date) {
notificationConfig.date = notification.date;
PushNotification.localNotificationSchedule(notificationConfig);
} else {
PushNotification.localNotification(notificationConfig);
}
}
// スケジュール通知
scheduleNotification(notification: LocalNotification, date: Date): void {
this.sendLocalNotification({
...notification,
date,
});
}
// 通知キャンセル
cancelNotification(notificationId: number): void {
PushNotification.cancelLocalNotifications({ id: notificationId.toString() });
}
// 全通知キャンセル
cancelAllNotifications(): void {
PushNotification.cancelAllLocalNotifications();
}
// 通知バッジクリア(iOS)
clearBadge(): void {
if (Platform.OS === 'ios') {
PushNotificationIOS.setApplicationIconBadgeNumber(0);
}
}
// 通知バッジ設定(iOS)
setBadge(count: number): void {
if (Platform.OS === 'ios') {
PushNotificationIOS.setApplicationIconBadgeNumber(count);
}
}
// トピック購読
async subscribeToTopic(topic: string): Promise<void> {
try {
await messaging().subscribeToTopic(topic);
console.log(`Subscribed to topic: ${topic}`);
} catch (error) {
console.error('Subscribe to topic error:', error);
}
}
// トピック購読解除
async unsubscribeFromTopic(topic: string): Promise<void> {
try {
await messaging().unsubscribeFromTopic(topic);
console.log(`Unsubscribed from topic: ${topic}`);
} catch (error) {
console.error('Unsubscribe from topic error:', error);
}
}
// アプリ設定画面を開く
private openAppSettings(): void {
if (Platform.OS === 'ios') {
PushNotificationIOS.requestPermissions();
} else {
// Android設定画面への遷移は react-native-app-settings などを使用
}
}
// FCMトークン取得
getFCMToken(): string | null {
return this.fcmToken;
}
// 通知許可状態確認
async getPermissionStatus(): Promise<string> {
if (Platform.OS === 'ios') {
const authStatus = await messaging().hasPermission();
switch (authStatus) {
case messaging.AuthorizationStatus.AUTHORIZED:
return 'authorized';
case messaging.AuthorizationStatus.DENIED:
return 'denied';
case messaging.AuthorizationStatus.NOT_DETERMINED:
return 'not_determined';
case messaging.AuthorizationStatus.PROVISIONAL:
return 'provisional';
default:
return 'unknown';
}
} else {
return 'granted'; // Android での詳細な状態取得は複雑なため簡略化
}
}
}
export default NotificationService.getInstance();
分析とパフォーマンス監視
// src/services/AnalyticsService.ts - アナリティクスサービス
import analytics from '@react-native-firebase/analytics';
import crashlytics from '@react-native-firebase/crashlytics';
import perf from '@react-native-firebase/perf';
import { Platform } from 'react-native';
import DeviceInfo from 'react-native-device-info';
export interface AnalyticsEvent {
name: string;
parameters?: { [key: string]: any };
timestamp?: Date;
}
export interface UserProperties {
[key: string]: string | number | boolean;
}
export interface CustomMetrics {
[key: string]: number;
}
export interface PerformanceTrace {
name: string;
startTime: number;
metrics?: CustomMetrics;
attributes?: { [key: string]: string };
}
class AnalyticsService {
private static instance: AnalyticsService;
private activeTraces: Map<string, any> = new Map();
private sessionStartTime: number = Date.now();
private isInitialized = false;
private constructor() {}
public static getInstance(): AnalyticsService {
if (!AnalyticsService.instance) {
AnalyticsService.instance = new AnalyticsService();
}
return AnalyticsService.instance;
}
// アナリティクスサービスの初期化
async initialize(): Promise<void> {
if (this.isInitialized) return;
try {
// Analytics有効化
await analytics().setAnalyticsCollectionEnabled(true);
// Crashlytics有効化
await crashlytics().setCrashlyticsCollectionEnabled(true);
// デバイス情報の設定
await this.setDeviceProperties();
// アプリ起動イベント
await this.trackAppStart();
this.isInitialized = true;
console.log('Analytics service initialized successfully');
} catch (error) {
console.error('Analytics service initialization failed:', error);
this.recordError(error as Error);
}
}
// デバイス情報プロパティ設定
private async setDeviceProperties(): Promise<void> {
try {
const deviceInfo = {
platform: Platform.OS,
version: Platform.Version.toString(),
device_model: await DeviceInfo.getModel(),
device_brand: await DeviceInfo.getBrand(),
system_version: await DeviceInfo.getSystemVersion(),
app_version: await DeviceInfo.getVersion(),
build_number: await DeviceInfo.getBuildNumber(),
is_tablet: await DeviceInfo.isTablet(),
total_memory: await DeviceInfo.getTotalMemory(),
};
// Firebase Analyticsにユーザープロパティとして設定
for (const [key, value] of Object.entries(deviceInfo)) {
await analytics().setUserProperty(key, String(value));
}
// Crashlyticsにも設定
for (const [key, value] of Object.entries(deviceInfo)) {
crashlytics().setAttribute(key, String(value));
}
console.log('Device properties set:', deviceInfo);
} catch (error) {
console.error('Set device properties error:', error);
}
}
// アプリ起動トラッキング
private async trackAppStart(): Promise<void> {
try {
const startupTime = Date.now() - this.sessionStartTime;
await this.trackEvent('app_start', {
startup_time: startupTime,
timestamp: new Date().toISOString(),
session_id: this.generateSessionId(),
});
// パフォーマンストレース
const trace = perf().newTrace('app_startup');
trace.putMetric('startup_time_ms', startupTime);
await trace.start();
await trace.stop();
} catch (error) {
console.error('Track app start error:', error);
}
}
// カスタムイベントトラッキング
async trackEvent(eventName: string, parameters?: { [key: string]: any }): Promise<void> {
try {
const eventParams = {
...parameters,
timestamp: new Date().toISOString(),
session_id: this.generateSessionId(),
};
await analytics().logEvent(eventName, eventParams);
console.log(`Event tracked: ${eventName}`, eventParams);
} catch (error) {
console.error('Track event error:', error);
this.recordError(error as Error);
}
}
// 画面表示トラッキング
async trackScreenView(screenName: string, screenClass?: string, additionalData?: any): Promise<void> {
try {
await analytics().logScreenView({
screen_name: screenName,
screen_class: screenClass || screenName,
});
// 追加のカスタムイベント
await this.trackEvent('screen_view', {
screen_name: screenName,
screen_class: screenClass || screenName,
...additionalData,
});
console.log(`Screen view tracked: ${screenName}`);
} catch (error) {
console.error('Track screen view error:', error);
}
}
// ユーザープロパティ設定
async setUserProperties(properties: UserProperties): Promise<void> {
try {
for (const [key, value] of Object.entries(properties)) {
await analytics().setUserProperty(key, String(value));
crashlytics().setAttribute(key, String(value));
}
console.log('User properties set:', properties);
} catch (error) {
console.error('Set user properties error:', error);
}
}
// ユーザーID設定
async setUserId(userId: string): Promise<void> {
try {
await analytics().setUserId(userId);
await crashlytics().setUserId(userId);
console.log(`User ID set: ${userId}`);
} catch (error) {
console.error('Set user ID error:', error);
}
}
// ECイベントトラッキング
async trackPurchase(
transactionId: string,
value: number,
currency: string,
items: any[]
): Promise<void> {
try {
await analytics().logEvent('purchase', {
transaction_id: transactionId,
value,
currency,
items,
});
// 個別アイテムイベント
for (const item of items) {
await analytics().logEvent('select_item', {
item_id: item.item_id,
item_name: item.item_name,
item_category: item.item_category,
value: item.price,
currency,
});
}
console.log('Purchase tracked:', { transactionId, value, currency });
} catch (error) {
console.error('Track purchase error:', error);
}
}
// ユーザーエンゲージメントトラッキング
async trackUserEngagement(action: string, duration?: number, additionalData?: any): Promise<void> {
try {
const parameters: { [key: string]: any } = {
action,
...additionalData
};
if (duration !== undefined) {
parameters.engagement_time_msec = duration;
}
await analytics().logEvent('user_engagement', parameters);
console.log('User engagement tracked:', parameters);
} catch (error) {
console.error('Track user engagement error:', error);
}
}
// パフォーマンストレース開始
async startPerformanceTrace(traceName: string): Promise<boolean> {
try {
const trace = perf().newTrace(traceName);
await trace.start();
this.activeTraces.set(traceName, {
trace,
startTime: Date.now(),
});
console.log(`Performance trace started: ${traceName}`);
return true;
} catch (error) {
console.error('Start performance trace error:', error);
return false;
}
}
// パフォーマンストレース停止
async stopPerformanceTrace(
traceName: string,
metrics?: CustomMetrics,
attributes?: { [key: string]: string }
): Promise<void> {
try {
const traceData = this.activeTraces.get(traceName);
if (!traceData) {
console.warn(`Performance trace not found: ${traceName}`);
return;
}
const { trace } = traceData;
const duration = Date.now() - traceData.startTime;
// メトリクス追加
if (metrics) {
for (const [key, value] of Object.entries(metrics)) {
trace.putMetric(key, value);
}
}
// 属性追加
if (attributes) {
for (const [key, value] of Object.entries(attributes)) {
trace.putAttribute(key, value);
}
}
// 実行時間メトリック
trace.putMetric('duration_ms', duration);
await trace.stop();
this.activeTraces.delete(traceName);
console.log(`Performance trace stopped: ${traceName}, duration: ${duration}ms`);
} catch (error) {
console.error('Stop performance trace error:', error);
}
}
// 非同期処理のパフォーマンス測定
async measureAsync<T>(traceName: string, fn: () => Promise<T>): Promise<T> {
await this.startPerformanceTrace(traceName);
try {
const result = await fn();
await this.stopPerformanceTrace(traceName, { success: 1 });
return result;
} catch (error) {
await this.stopPerformanceTrace(traceName, { success: 0, error: 1 });
throw error;
}
}
// エラー記録(Crashlytics)
recordError(error: Error, additionalInfo?: { [key: string]: any }): void {
try {
if (additionalInfo) {
for (const [key, value] of Object.entries(additionalInfo)) {
crashlytics().setAttribute(key, String(value));
}
}
crashlytics().recordError(error);
console.log('Error recorded to Crashlytics:', error.message);
} catch (err) {
console.error('Record error to Crashlytics failed:', err);
}
}
// カスタムログ
log(message: string, level: 'debug' | 'info' | 'warning' | 'error' = 'info'): void {
try {
crashlytics().log(`[${level.toUpperCase()}] ${message}`);
console.log(`Crashlytics log: ${message}`);
} catch (error) {
console.error('Crashlytics log error:', error);
}
}
// カスタムキー設定
setCustomKeys(keys: { [key: string]: any }): void {
try {
for (const [key, value] of Object.entries(keys)) {
crashlytics().setAttribute(key, String(value));
}
console.log('Custom keys set for crash reports:', keys);
} catch (error) {
console.error('Set custom keys error:', error);
}
}
// ネットワークリクエストトラッキング
async trackNetworkRequest(
url: string,
method: string,
statusCode: number,
duration: number,
responseSize?: number
): Promise<void> {
try {
await this.trackEvent('network_request', {
url,
method,
status_code: statusCode,
duration_ms: duration,
response_size: responseSize || 0,
success: statusCode >= 200 && statusCode < 300 ? 1 : 0,
});
} catch (error) {
console.error('Track network request error:', error);
}
}
// セッション時間計算
getSessionDuration(): number {
return Date.now() - this.sessionStartTime;
}
// セッションID生成
private generateSessionId(): string {
return `session_${this.sessionStartTime}_${Math.random().toString(36).substr(2, 9)}`;
}
// アプリ終了時のクリーンアップ
async cleanup(): Promise<void> {
try {
// アクティブなトレースを停止
for (const [traceName] of this.activeTraces) {
await this.stopPerformanceTrace(traceName);
}
// セッション終了イベント
await this.trackEvent('session_end', {
session_duration: this.getSessionDuration(),
});
console.log('Analytics service cleanup completed');
} catch (error) {
console.error('Analytics cleanup error:', error);
}
}
// テスト用クラッシュ(開発環境のみ)
testCrash(): void {
if (__DEV__) {
crashlytics().crash();
}
}
}
export default AnalyticsService.getInstance();
// 便利なヘルパー関数
export const performanceMonitor = {
// API呼び出しの測定
async measureApiCall<T>(apiName: string, apiCall: () => Promise<T>): Promise<T> {
const analyticsService = AnalyticsService.getInstance();
return analyticsService.measureAsync(`api_${apiName}`, apiCall);
},
// 画面レンダリング時間の測定
async measureScreenRender(screenName: string, renderFunction: () => Promise<void>): Promise<void> {
const analyticsService = AnalyticsService.getInstance();
return analyticsService.measureAsync(`screen_render_${screenName}`, renderFunction);
},
// データベース操作の測定
async measureDatabaseOperation<T>(operation: string, dbCall: () => Promise<T>): Promise<T> {
const analyticsService = AnalyticsService.getInstance();
return analyticsService.measureAsync(`db_${operation}`, dbCall);
},
};
デプロイメントと設定
# metro.config.js - Metro bundler 設定
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
const defaultConfig = getDefaultConfig(__dirname);
const config = {
resolver: {
assetExts: [...defaultConfig.resolver.assetExts, 'bin'],
sourceExts: [...defaultConfig.resolver.sourceExts, 'svg'],
},
transformer: {
babelTransformerPath: require.resolve('react-native-svg-transformer'),
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
watchFolders: [
// 追加の監視フォルダがあれば設定
],
};
module.exports = mergeConfig(defaultConfig, config);
// package.json - Build & Deploy Scripts
{
"scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios",
"start": "react-native start",
"test": "jest",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"build:android": "cd android && ./gradlew assembleRelease",
"build:android:bundle": "cd android && ./gradlew bundleRelease",
"build:ios": "cd ios && xcodebuild -workspace MyAwesomeApp.xcworkspace -scheme MyAwesomeApp -configuration Release -destination generic/platform=iOS -archivePath build/MyAwesomeApp.xcarchive archive",
"build:ios:simulator": "cd ios && xcodebuild -workspace MyAwesomeApp.xcworkspace -scheme MyAwesomeApp -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 14' build",
"clean": "npx react-native-clean-project",
"clean:android": "cd android && ./gradlew clean",
"clean:ios": "cd ios && xcodebuild clean",
"postinstall": "cd ios && pod install",
"analyze:bundle": "npx react-native-bundle-visualizer",
"type-check": "tsc --noEmit",
"test:coverage": "jest --coverage",
"test:e2e": "detox test",
"test:e2e:build": "detox build",
"deploy:android": "cd android && ./gradlew assembleRelease && echo 'APK built successfully'",
"deploy:ios": "cd ios && xcodebuild -exportArchive -archivePath build/MyAwesomeApp.xcarchive -exportPath build -exportOptionsPlist ExportOptions.plist"
}
}
# release.sh - リリース自動化スクリプト
#!/bin/bash
# React Native アプリケーション リリーススクリプト
set -e
echo "🚀 React Native アプリケーション リリース開始"
# バージョン情報の取得
VERSION=$(node -p "require('./package.json').version")
echo "📱 アプリバージョン: $VERSION"
# 環境チェック
echo "🔍 環境チェック中..."
# Node.js バージョンチェック
NODE_VERSION=$(node -v)
echo "Node.js バージョン: $NODE_VERSION"
# React Native CLI チェック
if ! command -v react-native &> /dev/null; then
echo "❌ React Native CLI がインストールされていません"
exit 1
fi
# 依存関係のインストール
echo "📦 依存関係をインストール中..."
npm ci
# TypeScript 型チェック
echo "🔍 TypeScript 型チェック中..."
npm run type-check
# ESLint チェック
echo "🔍 ESLint チェック中..."
npm run lint
# テスト実行
echo "🧪 テスト実行中..."
npm run test
# Metro bundler キャッシュクリア
echo "🧹 Metro キャッシュクリア中..."
npx react-native start --reset-cache &
sleep 5
kill %1
# Android ビルド
if [[ "$1" == "android" || "$1" == "both" ]]; then
echo "🤖 Android ビルド開始..."
# Android クリーン
cd android
./gradlew clean
# リリースビルド
./gradlew assembleRelease
# APK サイズチェック
APK_SIZE=$(du -h app/build/outputs/apk/release/app-release.apk | cut -f1)
echo "📱 APK サイズ: $APK_SIZE"
# APK ファイル移動
cp app/build/outputs/apk/release/app-release.apk ../builds/android/
cd ..
echo "✅ Android ビルド完了"
fi
# iOS ビルド(macOSのみ)
if [[ "$1" == "ios" || "$1" == "both" ]] && [[ "$OSTYPE" == "darwin"* ]]; then
echo "🍎 iOS ビルド開始..."
# CocoaPods 更新
cd ios
pod install --repo-update
# アーカイブ作成
xcodebuild -workspace MyAwesomeApp.xcworkspace \
-scheme MyAwesomeApp \
-configuration Release \
-destination generic/platform=iOS \
-archivePath build/MyAwesomeApp.xcarchive \
archive
# IPA エクスポート
xcodebuild -exportArchive \
-archivePath build/MyAwesomeApp.xcarchive \
-exportPath build \
-exportOptionsPlist ExportOptions.plist
# IPA ファイル移動
cp build/MyAwesomeApp.ipa ../builds/ios/
cd ..
echo "✅ iOS ビルド完了"
fi
# バンドル分析
echo "📊 バンドル分析中..."
npx react-native-bundle-visualizer --platform android --dev false --output analysis/android-bundle-analysis.html
npx react-native-bundle-visualizer --platform ios --dev false --output analysis/ios-bundle-analysis.html
# リリースノート生成
echo "📝 リリースノート生成中..."
cat > "builds/release-notes-v$VERSION.md" << EOF
# Release Notes v$VERSION
## 📱 Application Information
- Version: $VERSION
- Build Date: $(date)
- React Native Version: $(npx react-native --version)
- Node.js Version: $NODE_VERSION
## 📦 Build Artifacts
- Android APK: builds/android/app-release.apk
- iOS IPA: builds/ios/MyAwesomeApp.ipa
## 🧪 Quality Assurance
- ✅ TypeScript Type Check Passed
- ✅ ESLint Check Passed
- ✅ Unit Tests Passed
- ✅ Bundle Analysis Completed
## 📊 Bundle Analysis
- Android Bundle Analysis: analysis/android-bundle-analysis.html
- iOS Bundle Analysis: analysis/ios-bundle-analysis.html
---
Generated automatically by release script.
EOF
echo "🎉 リリース完了!"
echo "📄 リリースノート: builds/release-notes-v$VERSION.md"
echo "📱 ビルドファイル: builds/ ディレクトリを確認してください"
# 成功時の通知(オプション)
if command -v osascript &> /dev/null; then
osascript -e "display notification \"React Native アプリのリリースが完了しました!\" with title \"🎉 ビルド成功\""
fi