expo-mobile-app-rule

安装量: 66
排名: #11550

安装

npx skills add https://github.com/oimiragieo/agent-studio --skill expo-mobile-app-rule

Expo Mobile App Rule Skill Navigation with Expo Router File-Based Routing

Expo Router uses the file system for navigation:

app/ _layout.tsx # Root layout index.tsx # Home screen (/) (tabs)/ # Tab navigator group _layout.tsx # Tab layout home.tsx # /home profile.tsx # /profile user/ [id].tsx # Dynamic route /user/:id modal.tsx # Can be presented as modal

Root Layout // app/_layout.tsx import { Stack } from 'expo-router';

export default function RootLayout() { return ( ); }

Tab Navigation // app/(tabs)/_layout.tsx import { Tabs } from 'expo-router'; import { Ionicons } from '@expo/vector-icons';

export default function TabLayout() { return ( ( ), }} /> ( ), }} /> ); }

Navigation Methods import { useRouter, useLocalSearchParams, Link } from 'expo-router';

function MyComponent() { const router = useRouter(); const params = useLocalSearchParams();

return ( <> {/ Declarative navigation /} <Link href="/profile">Go to Profile</Link> <Link href={{ pathname: '/user/[id]', params: { id: '123' } }}> View User </Link>

  {/* Imperative navigation */}
  <Button onPress={() => router.push('/profile')} title="Push" />
  <Button onPress={() => router.replace('/home')} title="Replace" />
  <Button onPress={() => router.back()} title="Go Back" />
</>

); }

Dynamic Routes // app/user/[id].tsx import { useLocalSearchParams } from 'expo-router';

export default function UserScreen() { const { id } = useLocalSearchParams<{ id: string }>();

return User ID: {id}; }

Protected Routes // app/_layout.tsx import { useAuth } from '@/hooks/useAuth'; import { Redirect, Slot } from 'expo-router';

export default function AppLayout() { const { user, loading } = useAuth();

if (loading) { return ; }

if (!user) { return ; }

return ; }

State Management Context API for Global State // contexts/AppContext.tsx import { createContext, useContext, useState, ReactNode } from 'react';

interface AppState { user: User | null; theme: 'light' | 'dark'; setUser: (user: User | null) => void; setTheme: (theme: 'light' | 'dark') => void; }

const AppContext = createContext(undefined);

export function AppProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null); const [theme, setTheme] = useState<'light' | 'dark'>('light');

return ( {children} ); }

export const useApp = () => { const context = useContext(AppContext); if (!context) { throw new Error('useApp must be used within AppProvider'); } return context; };

Redux Toolkit (for Complex State) // store/slices/userSlice.ts import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface UserState { currentUser: User | null; loading: boolean; }

const userSlice = createSlice({ name: 'user', initialState: { currentUser: null, loading: false } as UserState, reducers: { setUser: (state, action: PayloadAction) => { state.currentUser = action.payload; }, setLoading: (state, action: PayloadAction) => { state.loading = action.payload; }, }, });

export const { setUser, setLoading } = userSlice.actions; export default userSlice.reducer;

Zustand (Lightweight Alternative) // store/useStore.ts import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import AsyncStorage from '@react-native-async-storage/async-storage';

interface AppStore { user: User | null; theme: 'light' | 'dark'; setUser: (user: User | null) => void; toggleTheme: () => void; }

export const useStore = create()( persist( set => ({ user: null, theme: 'light', setUser: user => set({ user }), toggleTheme: () => set(state => ({ theme: state.theme === 'light' ? 'dark' : 'light', })), }), { name: 'app-storage', storage: createJSONStorage(() => AsyncStorage), } ) );

Offline Support AsyncStorage for Local Data import AsyncStorage from '@react-native-async-storage/async-storage';

// Save data await AsyncStorage.setItem('user', JSON.stringify(user));

// Load data const userData = await AsyncStorage.getItem('user'); const user = userData ? JSON.parse(userData) : null;

// Remove data await AsyncStorage.removeItem('user');

// Clear all await AsyncStorage.clear();

SQLite for Complex Offline Data import * as SQLite from 'expo-sqlite';

class DatabaseService { private db: SQLite.SQLiteDatabase | null = null;

async init() { this.db = await SQLite.openDatabaseAsync('myapp.db');

await this.db.execAsync(`
  CREATE TABLE IF NOT EXISTS messages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    text TEXT NOT NULL,
    timestamp INTEGER,
    synced INTEGER DEFAULT 0
  );
`);

}

async saveMessage(text: string) { await this.db?.runAsync( 'INSERT INTO messages (text, timestamp) VALUES (?, ?)', text, Date.now() ); }

async getUnsynced() { return await this.db?.getAllAsync('SELECT * FROM messages WHERE synced = 0'); }

async markSynced(id: number) { await this.db?.runAsync('UPDATE messages SET synced = 1 WHERE id = ?', id); } }

Network State Detection import NetInfo from '@react-native-community/netinfo'; import { useEffect, useState } from 'react';

function useOnlineStatus() { const [isOnline, setIsOnline] = useState(true);

useEffect(() => { const unsubscribe = NetInfo.addEventListener((state) => { setIsOnline(state.isConnected ?? false); });

return () => unsubscribe();

}, []);

return isOnline; }

// Usage function MyScreen() { const isOnline = useOnlineStatus();

return ( {!isOnline && } {/ Rest of content /} ); }

Offline-First Data Sync import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

function useOfflineData() { const queryClient = useQueryClient(); const isOnline = useOnlineStatus();

const { data } = useQuery({ queryKey: ['items'], queryFn: fetchItems, enabled: isOnline, // Use cached data when offline staleTime: Infinity, });

const mutation = useMutation({ mutationFn: createItem, onMutate: async newItem => { // Optimistic update await queryClient.cancelQueries({ queryKey: ['items'] }); const previous = queryClient.getQueryData(['items']);

  queryClient.setQueryData(['items'], (old: any) => [...old, newItem]);

  // Save to local storage if offline
  if (!isOnline) {
    await saveToQueue(newItem);
  }

  return { previous };
},
onError: (err, newItem, context) => {
  // Rollback on error
  queryClient.setQueryData(['items'], context?.previous);
},

});

// Sync queue when online useEffect(() => { if (isOnline) { syncQueue(); } }, [isOnline]);

return { data, mutation }; }

Push Notifications Setup with Expo Notifications import * as Notifications from 'expo-notifications'; import * as Device from 'expo-device'; import Constants from 'expo-constants'; import { Platform } from 'react-native';

// Configure notification handler Notifications.setNotificationHandler({ handleNotification: async () => ({ shouldShowAlert: true, shouldPlaySound: true, shouldSetBadge: true, }), });

async function registerForPushNotifications() { if (!Device.isDevice) { alert('Push notifications only work on physical devices'); return; }

const { status: existingStatus } = await Notifications.getPermissionsAsync();

let finalStatus = existingStatus;

if (existingStatus !== 'granted') { const { status } = await Notifications.requestPermissionsAsync(); finalStatus = status; }

if (finalStatus !== 'granted') { return; }

const projectId = Constants.expoConfig?.extra?.eas?.projectId;

const token = await Notifications.getExpoPushTokenAsync({ projectId });

// Send token to your backend await sendTokenToBackend(token.data);

// Android-specific channel setup if (Platform.OS === 'android') { Notifications.setNotificationChannelAsync('default', { name: 'default', importance: Notifications.AndroidImportance.MAX, vibrationPattern: [0, 250, 250, 250], lightColor: '#FF231F7C', }); }

return token.data; }

Handling Notifications import { useEffect, useRef } from 'react';

function useNotifications() { const notificationListener = useRef(); const responseListener = useRef();

useEffect(() => { // Foreground notification handler notificationListener.current = Notifications.addNotificationReceivedListener(notification => { console.log('Notification received:', notification); });

// User interaction handler
responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
  const data = response.notification.request.content.data;

  // Navigate based on notification data
  if (data.screen) {
    router.push(data.screen as any);
  }
});

return () => {
  if (notificationListener.current) {
    Notifications.removeNotificationSubscription(notificationListener.current);
  }
  if (responseListener.current) {
    Notifications.removeNotificationSubscription(responseListener.current);
  }
};

}, []); }

Local Notifications async function scheduleNotification() { await Notifications.scheduleNotificationAsync({ content: { title: 'Reminder', body: 'Time to check your tasks!', data: { screen: '/tasks' }, }, trigger: { seconds: 60, // Or use specific date // date: new Date(Date.now() + 60 * 60 * 1000), // Or repeating // repeats: true, }, }); }

// Cancel notification const identifier = await scheduleNotification(); await Notifications.cancelScheduledNotificationAsync(identifier);

// Cancel all await Notifications.cancelAllScheduledNotificationsAsync();

Deep Linking Configure Deep Links // app.json { "expo": { "scheme": "myapp", "ios": { "associatedDomains": ["applinks:myapp.com"] }, "android": { "intentFilters": [ { "action": "VIEW", "autoVerify": true, "data": [ { "scheme": "https", "host": "myapp.com" } ], "category": ["BROWSABLE", "DEFAULT"] } ] } } }

Handle Deep Links in Expo Router // Expo Router handles deep links automatically // myapp://user/123 -> app/user/[id].tsx

// For custom handling: import * as Linking from 'expo-linking';

function useDeepLinking() { useEffect(() => { // Get initial URL (app opened via link) Linking.getInitialURL().then(url => { if (url) { handleDeepLink(url); } });

// Listen for URL changes (app already open)
const subscription = Linking.addEventListener('url', ({ url }) => {
  handleDeepLink(url);
});

return () => subscription.remove();

}, []); }

function handleDeepLink(url: string) { const { path, queryParams } = Linking.parse(url);

// Navigate based on path if (path === 'user') { router.push(/user/${queryParams?.id}); } }

Universal Links (iOS) & App Links (Android) // apple-app-site-association (serve at https://myapp.com/.well-known/) { "applinks": { "apps": [], "details": [ { "appID": "TEAMID.com.company.myapp", "paths": ["/user/", "/post/"] } ] } }

// assetlinks.json (serve at https://myapp.com/.well-known/) [ { "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "com.company.myapp", "sha256_cert_fingerprints": ["FINGERPRINT"] } } ]

Deep Link from Push Notifications // When sending push notification from backend { "to": "ExponentPushToken[xxx]", "title": "New Message", "body": "You have a new message", "data": { "url": "myapp://chat/123" } }

// Handle in app responseListener.current = Notifications.addNotificationResponseReceivedListener((response) => { const url = response.notification.request.content.data.url; if (url) { Linking.openURL(url); } });

Performance Optimization Memoization import { memo, useMemo, useCallback } from 'react';

const ListItem = memo(({ item, onPress }: Props) => ( onPress(item.id)}> {item.title} ));

function MyList({ items }: Props) { const sortedItems = useMemo( () => items.sort((a, b) => a.title.localeCompare(b.title)), [items] );

const handlePress = useCallback((id: string) => { router.push(/item/${id}); }, []);

return ( ( )} keyExtractor={(item) => item.id} /> ); }

Optimized Lists import { FlashList } from '@shopify/flash-list';

} estimatedItemSize={100} // Much faster than FlatList for large lists />

Image Optimization import { Image } from 'expo-image';

Memory Protocol (MANDATORY)

Before starting:

cat .claude/context/memory/learnings.md

After completing: Record any new patterns or exceptions discovered.

ASSUME INTERRUPTION: Your context may reset. If it's not in memory, it didn't happen.

返回排行榜