omgkit 2.0.7 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,756 @@
1
+ ---
2
+ name: mobile-development
3
+ description: Cross-platform mobile development with React Native and Expo including navigation, state management, and native features
4
+ category: mobile
5
+ triggers:
6
+ - mobile development
7
+ - react native
8
+ - expo
9
+ - cross platform
10
+ - mobile app
11
+ - ios android
12
+ - native features
13
+ ---
14
+
15
+ # Mobile Development
16
+
17
+ Build **cross-platform mobile applications** with React Native and Expo. This skill covers component architecture, navigation patterns, state management, and native feature integration.
18
+
19
+ ## Purpose
20
+
21
+ Create production-ready mobile applications:
22
+
23
+ - Build iOS and Android apps from single codebase
24
+ - Implement native navigation patterns
25
+ - Manage application state effectively
26
+ - Access device features (camera, location, notifications)
27
+ - Handle offline-first architecture
28
+ - Optimize performance for mobile devices
29
+
30
+ ## Features
31
+
32
+ ### 1. Expo Project Setup
33
+
34
+ ```bash
35
+ # Create new Expo project
36
+ npx create-expo-app@latest my-app --template tabs
37
+
38
+ # Project structure
39
+ my-app/
40
+ ├── app/ # File-based routing
41
+ │ ├── (tabs)/ # Tab navigation group
42
+ │ │ ├── _layout.tsx # Tab layout
43
+ │ │ ├── index.tsx # Home tab
44
+ │ │ └── profile.tsx # Profile tab
45
+ │ ├── _layout.tsx # Root layout
46
+ │ └── modal.tsx # Modal screen
47
+ ├── components/ # Reusable components
48
+ ├── hooks/ # Custom hooks
49
+ ├── services/ # API services
50
+ ├── store/ # State management
51
+ ├── constants/ # App constants
52
+ └── assets/ # Images, fonts
53
+ ```
54
+
55
+ ```typescript
56
+ // app/_layout.tsx - Root layout with providers
57
+ import { Stack } from 'expo-router';
58
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
59
+ import { GestureHandlerRootView } from 'react-native-gesture-handler';
60
+
61
+ const queryClient = new QueryClient();
62
+
63
+ export default function RootLayout() {
64
+ return (
65
+ <GestureHandlerRootView style={{ flex: 1 }}>
66
+ <QueryClientProvider client={queryClient}>
67
+ <AuthProvider>
68
+ <ThemeProvider>
69
+ <Stack>
70
+ <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
71
+ <Stack.Screen name="modal" options={{ presentation: 'modal' }} />
72
+ </Stack>
73
+ </ThemeProvider>
74
+ </AuthProvider>
75
+ </QueryClientProvider>
76
+ </GestureHandlerRootView>
77
+ );
78
+ }
79
+ ```
80
+
81
+ ### 2. Navigation Patterns
82
+
83
+ ```typescript
84
+ // Tab Navigation with Expo Router
85
+ // app/(tabs)/_layout.tsx
86
+ import { Tabs } from 'expo-router';
87
+ import { Ionicons } from '@expo/vector-icons';
88
+
89
+ export default function TabLayout() {
90
+ const colorScheme = useColorScheme();
91
+
92
+ return (
93
+ <Tabs
94
+ screenOptions={{
95
+ tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
96
+ headerShown: false,
97
+ }}
98
+ >
99
+ <Tabs.Screen
100
+ name="index"
101
+ options={{
102
+ title: 'Home',
103
+ tabBarIcon: ({ color }) => <Ionicons name="home" size={24} color={color} />,
104
+ }}
105
+ />
106
+ <Tabs.Screen
107
+ name="explore"
108
+ options={{
109
+ title: 'Explore',
110
+ tabBarIcon: ({ color }) => <Ionicons name="compass" size={24} color={color} />,
111
+ }}
112
+ />
113
+ <Tabs.Screen
114
+ name="profile"
115
+ options={{
116
+ title: 'Profile',
117
+ tabBarIcon: ({ color }) => <Ionicons name="person" size={24} color={color} />,
118
+ }}
119
+ />
120
+ </Tabs>
121
+ );
122
+ }
123
+
124
+ // Stack Navigation with authentication
125
+ // app/(auth)/_layout.tsx
126
+ import { Stack, Redirect } from 'expo-router';
127
+ import { useAuth } from '@/hooks/useAuth';
128
+
129
+ export default function AuthLayout() {
130
+ const { isAuthenticated, isLoading } = useAuth();
131
+
132
+ if (isLoading) {
133
+ return <LoadingScreen />;
134
+ }
135
+
136
+ if (!isAuthenticated) {
137
+ return <Redirect href="/login" />;
138
+ }
139
+
140
+ return (
141
+ <Stack>
142
+ <Stack.Screen name="dashboard" options={{ title: 'Dashboard' }} />
143
+ <Stack.Screen
144
+ name="settings"
145
+ options={{
146
+ title: 'Settings',
147
+ presentation: 'modal',
148
+ }}
149
+ />
150
+ </Stack>
151
+ );
152
+ }
153
+
154
+ // Deep linking configuration
155
+ // app.json
156
+ {
157
+ "expo": {
158
+ "scheme": "myapp",
159
+ "web": {
160
+ "bundler": "metro"
161
+ },
162
+ "plugins": [
163
+ [
164
+ "expo-router",
165
+ {
166
+ "origin": "https://myapp.com"
167
+ }
168
+ ]
169
+ ]
170
+ }
171
+ }
172
+ ```
173
+
174
+ ### 3. State Management with Zustand
175
+
176
+ ```typescript
177
+ // store/useStore.ts
178
+ import { create } from 'zustand';
179
+ import { persist, createJSONStorage } from 'zustand/middleware';
180
+ import AsyncStorage from '@react-native-async-storage/async-storage';
181
+
182
+ interface User {
183
+ id: string;
184
+ name: string;
185
+ email: string;
186
+ avatar?: string;
187
+ }
188
+
189
+ interface AuthState {
190
+ user: User | null;
191
+ token: string | null;
192
+ isAuthenticated: boolean;
193
+ login: (user: User, token: string) => void;
194
+ logout: () => void;
195
+ updateUser: (updates: Partial<User>) => void;
196
+ }
197
+
198
+ export const useAuthStore = create<AuthState>()(
199
+ persist(
200
+ (set) => ({
201
+ user: null,
202
+ token: null,
203
+ isAuthenticated: false,
204
+
205
+ login: (user, token) => set({
206
+ user,
207
+ token,
208
+ isAuthenticated: true,
209
+ }),
210
+
211
+ logout: () => set({
212
+ user: null,
213
+ token: null,
214
+ isAuthenticated: false,
215
+ }),
216
+
217
+ updateUser: (updates) => set((state) => ({
218
+ user: state.user ? { ...state.user, ...updates } : null,
219
+ })),
220
+ }),
221
+ {
222
+ name: 'auth-storage',
223
+ storage: createJSONStorage(() => AsyncStorage),
224
+ }
225
+ )
226
+ );
227
+
228
+ // Cart store with computed values
229
+ interface CartItem {
230
+ id: string;
231
+ productId: string;
232
+ name: string;
233
+ price: number;
234
+ quantity: number;
235
+ }
236
+
237
+ interface CartState {
238
+ items: CartItem[];
239
+ addItem: (item: Omit<CartItem, 'id'>) => void;
240
+ removeItem: (id: string) => void;
241
+ updateQuantity: (id: string, quantity: number) => void;
242
+ clearCart: () => void;
243
+ total: () => number;
244
+ itemCount: () => number;
245
+ }
246
+
247
+ export const useCartStore = create<CartState>((set, get) => ({
248
+ items: [],
249
+
250
+ addItem: (item) => set((state) => {
251
+ const existing = state.items.find(i => i.productId === item.productId);
252
+ if (existing) {
253
+ return {
254
+ items: state.items.map(i =>
255
+ i.productId === item.productId
256
+ ? { ...i, quantity: i.quantity + item.quantity }
257
+ : i
258
+ ),
259
+ };
260
+ }
261
+ return {
262
+ items: [...state.items, { ...item, id: generateId() }],
263
+ };
264
+ }),
265
+
266
+ removeItem: (id) => set((state) => ({
267
+ items: state.items.filter(i => i.id !== id),
268
+ })),
269
+
270
+ updateQuantity: (id, quantity) => set((state) => ({
271
+ items: quantity > 0
272
+ ? state.items.map(i => i.id === id ? { ...i, quantity } : i)
273
+ : state.items.filter(i => i.id !== id),
274
+ })),
275
+
276
+ clearCart: () => set({ items: [] }),
277
+
278
+ total: () => get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
279
+
280
+ itemCount: () => get().items.reduce((count, item) => count + item.quantity, 0),
281
+ }));
282
+ ```
283
+
284
+ ### 4. API Integration with React Query
285
+
286
+ ```typescript
287
+ // services/api.ts
288
+ import axios from 'axios';
289
+ import { useAuthStore } from '@/store/useStore';
290
+
291
+ const api = axios.create({
292
+ baseURL: process.env.EXPO_PUBLIC_API_URL,
293
+ });
294
+
295
+ // Request interceptor for auth
296
+ api.interceptors.request.use((config) => {
297
+ const token = useAuthStore.getState().token;
298
+ if (token) {
299
+ config.headers.Authorization = `Bearer ${token}`;
300
+ }
301
+ return config;
302
+ });
303
+
304
+ // Response interceptor for errors
305
+ api.interceptors.response.use(
306
+ (response) => response,
307
+ async (error) => {
308
+ if (error.response?.status === 401) {
309
+ useAuthStore.getState().logout();
310
+ }
311
+ return Promise.reject(error);
312
+ }
313
+ );
314
+
315
+ // hooks/useProducts.ts
316
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
317
+
318
+ export function useProducts(categoryId?: string) {
319
+ return useQuery({
320
+ queryKey: ['products', categoryId],
321
+ queryFn: async () => {
322
+ const params = categoryId ? { category: categoryId } : {};
323
+ const { data } = await api.get('/products', { params });
324
+ return data;
325
+ },
326
+ staleTime: 5 * 60 * 1000, // 5 minutes
327
+ });
328
+ }
329
+
330
+ export function useProduct(id: string) {
331
+ return useQuery({
332
+ queryKey: ['product', id],
333
+ queryFn: async () => {
334
+ const { data } = await api.get(`/products/${id}`);
335
+ return data;
336
+ },
337
+ });
338
+ }
339
+
340
+ export function useCreateOrder() {
341
+ const queryClient = useQueryClient();
342
+
343
+ return useMutation({
344
+ mutationFn: async (orderData: CreateOrderInput) => {
345
+ const { data } = await api.post('/orders', orderData);
346
+ return data;
347
+ },
348
+ onSuccess: () => {
349
+ queryClient.invalidateQueries({ queryKey: ['orders'] });
350
+ },
351
+ });
352
+ }
353
+
354
+ // Infinite scroll with pagination
355
+ export function useInfiniteProducts() {
356
+ return useInfiniteQuery({
357
+ queryKey: ['products', 'infinite'],
358
+ queryFn: async ({ pageParam = 1 }) => {
359
+ const { data } = await api.get('/products', {
360
+ params: { page: pageParam, limit: 20 },
361
+ });
362
+ return data;
363
+ },
364
+ getNextPageParam: (lastPage) =>
365
+ lastPage.hasMore ? lastPage.page + 1 : undefined,
366
+ });
367
+ }
368
+ ```
369
+
370
+ ### 5. Native Features
371
+
372
+ ```typescript
373
+ // Camera integration
374
+ import { Camera, CameraType } from 'expo-camera';
375
+ import * as ImagePicker from 'expo-image-picker';
376
+
377
+ export function CameraScreen() {
378
+ const [permission, requestPermission] = Camera.useCameraPermissions();
379
+ const [type, setType] = useState(CameraType.back);
380
+ const cameraRef = useRef<Camera>(null);
381
+
382
+ async function takePicture() {
383
+ if (cameraRef.current) {
384
+ const photo = await cameraRef.current.takePictureAsync({
385
+ quality: 0.8,
386
+ base64: false,
387
+ });
388
+ // Handle photo
389
+ await uploadImage(photo.uri);
390
+ }
391
+ }
392
+
393
+ async function pickImage() {
394
+ const result = await ImagePicker.launchImageLibraryAsync({
395
+ mediaTypes: ImagePicker.MediaTypeOptions.Images,
396
+ allowsEditing: true,
397
+ aspect: [4, 3],
398
+ quality: 0.8,
399
+ });
400
+
401
+ if (!result.canceled) {
402
+ await uploadImage(result.assets[0].uri);
403
+ }
404
+ }
405
+
406
+ if (!permission?.granted) {
407
+ return (
408
+ <View style={styles.container}>
409
+ <Text>Camera permission required</Text>
410
+ <Button title="Grant Permission" onPress={requestPermission} />
411
+ </View>
412
+ );
413
+ }
414
+
415
+ return (
416
+ <View style={styles.container}>
417
+ <Camera style={styles.camera} type={type} ref={cameraRef}>
418
+ <View style={styles.controls}>
419
+ <TouchableOpacity onPress={() => setType(
420
+ type === CameraType.back ? CameraType.front : CameraType.back
421
+ )}>
422
+ <Ionicons name="camera-reverse" size={32} color="white" />
423
+ </TouchableOpacity>
424
+ <TouchableOpacity onPress={takePicture}>
425
+ <View style={styles.captureButton} />
426
+ </TouchableOpacity>
427
+ <TouchableOpacity onPress={pickImage}>
428
+ <Ionicons name="images" size={32} color="white" />
429
+ </TouchableOpacity>
430
+ </View>
431
+ </Camera>
432
+ </View>
433
+ );
434
+ }
435
+
436
+ // Location services
437
+ import * as Location from 'expo-location';
438
+
439
+ export function useLocation() {
440
+ const [location, setLocation] = useState<Location.LocationObject | null>(null);
441
+ const [error, setError] = useState<string | null>(null);
442
+
443
+ useEffect(() => {
444
+ (async () => {
445
+ const { status } = await Location.requestForegroundPermissionsAsync();
446
+ if (status !== 'granted') {
447
+ setError('Location permission denied');
448
+ return;
449
+ }
450
+
451
+ const currentLocation = await Location.getCurrentPositionAsync({
452
+ accuracy: Location.Accuracy.Balanced,
453
+ });
454
+ setLocation(currentLocation);
455
+ })();
456
+ }, []);
457
+
458
+ return { location, error };
459
+ }
460
+
461
+ // Push notifications
462
+ import * as Notifications from 'expo-notifications';
463
+ import * as Device from 'expo-device';
464
+
465
+ Notifications.setNotificationHandler({
466
+ handleNotification: async () => ({
467
+ shouldShowAlert: true,
468
+ shouldPlaySound: true,
469
+ shouldSetBadge: true,
470
+ }),
471
+ });
472
+
473
+ export async function registerForPushNotifications(): Promise<string | null> {
474
+ if (!Device.isDevice) {
475
+ console.log('Push notifications require a physical device');
476
+ return null;
477
+ }
478
+
479
+ const { status: existingStatus } = await Notifications.getPermissionsAsync();
480
+ let finalStatus = existingStatus;
481
+
482
+ if (existingStatus !== 'granted') {
483
+ const { status } = await Notifications.requestPermissionsAsync();
484
+ finalStatus = status;
485
+ }
486
+
487
+ if (finalStatus !== 'granted') {
488
+ return null;
489
+ }
490
+
491
+ const token = await Notifications.getExpoPushTokenAsync({
492
+ projectId: process.env.EXPO_PUBLIC_PROJECT_ID,
493
+ });
494
+
495
+ return token.data;
496
+ }
497
+
498
+ export function usePushNotifications() {
499
+ const [expoPushToken, setExpoPushToken] = useState<string | null>(null);
500
+ const notificationListener = useRef<Notifications.Subscription>();
501
+ const responseListener = useRef<Notifications.Subscription>();
502
+
503
+ useEffect(() => {
504
+ registerForPushNotifications().then(setExpoPushToken);
505
+
506
+ notificationListener.current = Notifications.addNotificationReceivedListener(
507
+ (notification) => {
508
+ console.log('Notification received:', notification);
509
+ }
510
+ );
511
+
512
+ responseListener.current = Notifications.addNotificationResponseReceivedListener(
513
+ (response) => {
514
+ const data = response.notification.request.content.data;
515
+ // Handle notification tap
516
+ if (data.screen) {
517
+ router.push(data.screen);
518
+ }
519
+ }
520
+ );
521
+
522
+ return () => {
523
+ notificationListener.current?.remove();
524
+ responseListener.current?.remove();
525
+ };
526
+ }, []);
527
+
528
+ return { expoPushToken };
529
+ }
530
+ ```
531
+
532
+ ### 6. Performance Optimization
533
+
534
+ ```typescript
535
+ // Optimized FlatList with virtualization
536
+ import { FlashList } from '@shopify/flash-list';
537
+
538
+ interface ProductListProps {
539
+ products: Product[];
540
+ onEndReached: () => void;
541
+ }
542
+
543
+ export function ProductList({ products, onEndReached }: ProductListProps) {
544
+ const renderItem = useCallback(({ item }: { item: Product }) => (
545
+ <ProductCard product={item} />
546
+ ), []);
547
+
548
+ const keyExtractor = useCallback((item: Product) => item.id, []);
549
+
550
+ return (
551
+ <FlashList
552
+ data={products}
553
+ renderItem={renderItem}
554
+ keyExtractor={keyExtractor}
555
+ estimatedItemSize={200}
556
+ onEndReached={onEndReached}
557
+ onEndReachedThreshold={0.5}
558
+ ItemSeparatorComponent={Separator}
559
+ ListEmptyComponent={EmptyState}
560
+ />
561
+ );
562
+ }
563
+
564
+ // Memoized components
565
+ const ProductCard = memo(function ProductCard({ product }: { product: Product }) {
566
+ const navigation = useNavigation();
567
+
568
+ const handlePress = useCallback(() => {
569
+ navigation.navigate('ProductDetail', { id: product.id });
570
+ }, [product.id, navigation]);
571
+
572
+ return (
573
+ <Pressable onPress={handlePress} style={styles.card}>
574
+ <Image
575
+ source={{ uri: product.image }}
576
+ style={styles.image}
577
+ contentFit="cover"
578
+ transition={200}
579
+ cachePolicy="memory-disk"
580
+ />
581
+ <Text style={styles.name}>{product.name}</Text>
582
+ <Text style={styles.price}>${product.price}</Text>
583
+ </Pressable>
584
+ );
585
+ });
586
+
587
+ // Image optimization with expo-image
588
+ import { Image } from 'expo-image';
589
+
590
+ const blurhash = '|rF?hV%2WCj[ayj[a|j[az_NaeWBj@ayfRayfQfQM{M|azj[azf6fQfQIpWXofj[ayj[j[fQayWCoeoeaya}j[ayfQa{oLj?j[WVj[ayayj[fQoff7telephones';
591
+
592
+ export function OptimizedImage({ source, style }) {
593
+ return (
594
+ <Image
595
+ source={source}
596
+ style={style}
597
+ placeholder={blurhash}
598
+ contentFit="cover"
599
+ transition={300}
600
+ cachePolicy="memory-disk"
601
+ />
602
+ );
603
+ }
604
+
605
+ // Skeleton loading
606
+ import { Skeleton } from 'moti/skeleton';
607
+
608
+ export function ProductCardSkeleton() {
609
+ return (
610
+ <View style={styles.card}>
611
+ <Skeleton colorMode="light" width="100%" height={200} />
612
+ <Skeleton colorMode="light" width="80%" height={20} />
613
+ <Skeleton colorMode="light" width="40%" height={16} />
614
+ </View>
615
+ );
616
+ }
617
+ ```
618
+
619
+ ## Use Cases
620
+
621
+ ### 1. E-commerce App
622
+
623
+ ```typescript
624
+ // Complete product screen
625
+ export function ProductScreen() {
626
+ const { id } = useLocalSearchParams();
627
+ const { data: product, isLoading } = useProduct(id as string);
628
+ const addToCart = useCartStore((state) => state.addItem);
629
+
630
+ if (isLoading) return <ProductSkeleton />;
631
+ if (!product) return <NotFound />;
632
+
633
+ return (
634
+ <ScrollView>
635
+ <ImageGallery images={product.images} />
636
+
637
+ <View style={styles.content}>
638
+ <Text style={styles.name}>{product.name}</Text>
639
+ <Text style={styles.price}>${product.price}</Text>
640
+
641
+ <VariantSelector
642
+ variants={product.variants}
643
+ onSelect={setSelectedVariant}
644
+ />
645
+
646
+ <Button
647
+ title="Add to Cart"
648
+ onPress={() => addToCart({
649
+ productId: product.id,
650
+ name: product.name,
651
+ price: product.price,
652
+ quantity: 1,
653
+ })}
654
+ />
655
+
656
+ <Text style={styles.description}>{product.description}</Text>
657
+ </View>
658
+ </ScrollView>
659
+ );
660
+ }
661
+ ```
662
+
663
+ ### 2. Social App with Real-time
664
+
665
+ ```typescript
666
+ // Real-time chat with Socket.io
667
+ import { io } from 'socket.io-client';
668
+
669
+ export function useChatRoom(roomId: string) {
670
+ const [messages, setMessages] = useState<Message[]>([]);
671
+ const socketRef = useRef<Socket>();
672
+
673
+ useEffect(() => {
674
+ const token = useAuthStore.getState().token;
675
+
676
+ socketRef.current = io(process.env.EXPO_PUBLIC_WS_URL!, {
677
+ auth: { token },
678
+ });
679
+
680
+ socketRef.current.emit('join', roomId);
681
+
682
+ socketRef.current.on('message', (message: Message) => {
683
+ setMessages((prev) => [...prev, message]);
684
+ });
685
+
686
+ return () => {
687
+ socketRef.current?.emit('leave', roomId);
688
+ socketRef.current?.disconnect();
689
+ };
690
+ }, [roomId]);
691
+
692
+ const sendMessage = useCallback((content: string) => {
693
+ socketRef.current?.emit('message', { roomId, content });
694
+ }, [roomId]);
695
+
696
+ return { messages, sendMessage };
697
+ }
698
+ ```
699
+
700
+ ## Best Practices
701
+
702
+ ### Do's
703
+
704
+ - **Use Expo for faster development** - Managed workflow handles complexity
705
+ - **Implement offline-first** - Use AsyncStorage and optimistic updates
706
+ - **Optimize images** - Use expo-image with caching
707
+ - **Use FlashList** - Better performance than FlatList
708
+ - **Test on real devices** - Simulators don't show real performance
709
+ - **Handle all permission states** - Request gracefully
710
+
711
+ ### Don'ts
712
+
713
+ - Don't block the JS thread with heavy computations
714
+ - Don't use inline styles in render methods
715
+ - Don't forget to handle keyboard avoiding
716
+ - Don't ignore deep linking setup
717
+ - Don't skip splash screen configuration
718
+ - Don't neglect accessibility
719
+
720
+ ### Performance Checklist
721
+
722
+ ```markdown
723
+ ## Mobile Performance Checklist
724
+
725
+ ### Rendering
726
+ - [ ] Use memo for expensive components
727
+ - [ ] Implement proper list virtualization
728
+ - [ ] Optimize images (size, format, caching)
729
+ - [ ] Avoid inline function props
730
+
731
+ ### State
732
+ - [ ] Split stores by domain
733
+ - [ ] Use selectors for derived state
734
+ - [ ] Persist critical data
735
+ - [ ] Handle loading/error states
736
+
737
+ ### Network
738
+ - [ ] Implement request caching
739
+ - [ ] Use optimistic updates
740
+ - [ ] Handle offline gracefully
741
+ - [ ] Implement retry logic
742
+ ```
743
+
744
+ ## Related Skills
745
+
746
+ - **react** - React fundamentals
747
+ - **typescript** - Type-safe development
748
+ - **frontend-design** - UI/UX patterns
749
+ - **api-architecture** - Backend integration
750
+
751
+ ## Reference Resources
752
+
753
+ - [Expo Documentation](https://docs.expo.dev/)
754
+ - [React Native Docs](https://reactnative.dev/)
755
+ - [React Navigation](https://reactnavigation.org/)
756
+ - [Expo Router](https://docs.expo.dev/router/introduction/)