commit 4d7019579207f9b395b6c459c6949693f14e186c Author: Zhongwei Li Date: Sat Nov 29 18:21:31 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..565cadd --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,14 @@ +{ + "name": "mobile-development", + "description": "Mobile development patterns for React Native and Flutter with offline-first architecture", + "version": "1.0.0", + "author": { + "name": "Brock" + }, + "agents": [ + "./agents" + ], + "commands": [ + "./commands" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e1ae43e --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# mobile-development + +Mobile development patterns for React Native and Flutter with offline-first architecture diff --git a/agents/mobile-app-builder.md b/agents/mobile-app-builder.md new file mode 100644 index 0000000..9b9d23e --- /dev/null +++ b/agents/mobile-app-builder.md @@ -0,0 +1,81 @@ +# Mobile App Builder Agent + +You are an autonomous agent specialized in building mobile applications with React Native and Flutter using offline-first architecture and modern best practices. + +## Your Mission + +Build production-ready mobile applications that work seamlessly offline, provide excellent UX, and follow platform-specific guidelines. + +## Core Responsibilities + +### 1. Set Up Mobile Project +- Initialize React Native or Flutter project +- Configure navigation +- Set up state management (Zustand/Riverpod) +- Configure offline storage +- Set up build tooling + +### 2. Implement Offline-First Architecture +- Local database with Drift/SQLite/AsyncStorage +- Data synchronization strategy +- Conflict resolution +- Queue failed requests +- Background sync + +### 3. Build UI Components +- Platform-specific components +- Responsive layouts +- Dark mode support +- Accessibility +- Animations + +### 4. Implement State Management + +React Native: +```typescript +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +const useStore = create(persist( + (set) => ({ + user: null, + login: (user) => set({ user }), + }), + { name: 'app-storage' } +)); +``` + +Flutter: +```dart +final userProvider = StateNotifierProvider((ref) { + return UserNotifier(); +}); +``` + +### 5. Handle Navigation +- Stack navigation +- Tab navigation +- Deep linking +- Authentication flows + +### 6. Optimize Performance +- List virtualization +- Image caching +- Memoization +- Code splitting + +### 7. Testing +- Unit tests +- Widget/component tests +- Integration tests +- E2E tests + +## Deliverables + +1. Fully functional mobile app +2. Offline-first data layer +3. Responsive UI +4. Navigation setup +5. Testing suite +6. Build configuration +7. Deployment guide diff --git a/commands/mobile-patterns.md b/commands/mobile-patterns.md new file mode 100644 index 0000000..cba54db --- /dev/null +++ b/commands/mobile-patterns.md @@ -0,0 +1,553 @@ +# Mobile Development Patterns + +Comprehensive mobile development patterns for React Native and Flutter with offline-first architecture and best practices. + +## React Native Patterns + +### Project Structure +``` +src/ +├── components/ # Reusable UI components +├── screens/ # Screen components +├── navigation/ # Navigation configuration +├── services/ # API and business logic +├── state/ # State management +├── hooks/ # Custom hooks +├── utils/ # Utility functions +└── types/ # TypeScript types +``` + +### Navigation with React Navigation + +```typescript +// navigation/AppNavigator.tsx +import { NavigationContainer } from '@react-navigation/native'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; + +type RootStackParamList = { + Home: undefined; + Profile: { userId: string }; + Settings: undefined; +}; + +const Stack = createNativeStackNavigator(); +const Tab = createBottomTabNavigator(); + +function TabNavigator() { + return ( + + + + + + ); +} + +export function AppNavigator() { + return ( + + + + + + + ); +} +``` + +### State Management with Zustand + +```typescript +import { create } from 'zustand'; +import { persist, createJSONStorage } from 'zustand/middleware'; +import AsyncStorage from '@react-native-async-storage/async-storage'; + +interface User { + id: string; + name: string; + email: string; +} + +interface UserStore { + user: User | null; + isLoading: boolean; + error: string | null; + login: (email: string, password: string) => Promise; + logout: () => void; + updateProfile: (data: Partial) => Promise; +} + +export const useUserStore = create()( + persist( + (set, get) => ({ + user: null, + isLoading: false, + error: null, + + login: async (email, password) => { + set({ isLoading: true, error: null }); + try { + const user = await authService.login(email, password); + set({ user, isLoading: false }); + } catch (error) { + set({ error: error.message, isLoading: false }); + } + }, + + logout: () => { + set({ user: null }); + }, + + updateProfile: async (data) => { + const currentUser = get().user; + if (!currentUser) return; + + set({ isLoading: true }); + try { + const updated = await userService.update(currentUser.id, data); + set({ user: updated, isLoading: false }); + } catch (error) { + set({ error: error.message, isLoading: false }); + } + }, + }), + { + name: 'user-storage', + storage: createJSONStorage(() => AsyncStorage), + } + ) +); +``` + +### Offline-First with React Query + +```typescript +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { persistQueryClient } from '@tanstack/react-query-persist-client'; +import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import NetInfo from '@react-native-community/netinfo'; + +// Setup persistence +const asyncStoragePersister = createAsyncStoragePersister({ + storage: AsyncStorage, +}); + +persistQueryClient({ + queryClient, + persister: asyncStoragePersister, +}); + +// Custom hook for online status +export function useOnlineStatus() { + const [isOnline, setIsOnline] = useState(true); + + useEffect(() => { + return NetInfo.addEventListener(state => { + setIsOnline(state.isConnected ?? false); + }); + }, []); + + return isOnline; +} + +// Offline-capable query +export function usePosts() { + const isOnline = useOnlineStatus(); + + return useQuery({ + queryKey: ['posts'], + queryFn: fetchPosts, + staleTime: 5 * 60 * 1000, // 5 minutes + cacheTime: 24 * 60 * 60 * 1000, // 24 hours + enabled: isOnline, + refetchOnMount: isOnline, + }); +} + +// Offline-capable mutation with queue +export function useCreatePost() { + const queryClient = useQueryClient(); + const isOnline = useOnlineStatus(); + + return useMutation({ + mutationFn: createPost, + onMutate: async (newPost) => { + // Cancel outgoing refetches + await queryClient.cancelQueries({ queryKey: ['posts'] }); + + // Snapshot previous value + const previousPosts = queryClient.getQueryData(['posts']); + + // Optimistically update + queryClient.setQueryData(['posts'], (old: any) => [...old, newPost]); + + return { previousPosts }; + }, + onError: (err, newPost, context) => { + // Rollback on error + queryClient.setQueryData(['posts'], context?.previousPosts); + + if (!isOnline) { + // Queue for later + queueMutation('createPost', newPost); + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['posts'] }); + }, + }); +} +``` + +## Flutter Patterns + +### Project Structure +``` +lib/ +├── main.dart +├── app/ +│ ├── routes.dart +│ └── theme.dart +├── features/ +│ ├── auth/ +│ │ ├── models/ +│ │ ├── providers/ +│ │ ├── screens/ +│ │ └── widgets/ +│ └── home/ +├── core/ +│ ├── services/ +│ ├── utils/ +│ └── constants/ +└── shared/ + └── widgets/ +``` + +### Navigation with GoRouter + +```dart +import 'package:go_router/go_router.dart'; + +final router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const HomeScreen(), + routes: [ + GoRoute( + path: 'profile/:id', + builder: (context, state) { + final id = state.pathParameters['id']!; + return ProfileScreen(userId: id); + }, + ), + ], + ), + GoRoute( + path: '/settings', + builder: (context, state) => const SettingsScreen(), + ), + ], + redirect: (context, state) { + final isLoggedIn = authService.isAuthenticated; + final isAuthRoute = state.matchedLocation.startsWith('/auth'); + + if (!isLoggedIn && !isAuthRoute) { + return '/auth/login'; + } + if (isLoggedIn && isAuthRoute) { + return '/'; + } + return null; + }, +); + +// Usage +void main() { + runApp(MaterialApp.router( + routerConfig: router, + )); +} +``` + +### State Management with Riverpod + +```dart +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +// Model +class User { + final String id; + final String name; + final String email; + + User({required this.id, required this.name, required this.email}); +} + +// Repository +class UserRepository { + Future getUser(String id) async { + // API call + } + + Future updateUser(String id, Map data) async { + // API call + } +} + +// Providers +final userRepositoryProvider = Provider((ref) => UserRepository()); + +final userProvider = FutureProvider.family((ref, id) async { + final repository = ref.watch(userRepositoryProvider); + return repository.getUser(id); +}); + +// Notifier for mutable state +class UserNotifier extends StateNotifier> { + UserNotifier(this.repository) : super(const AsyncValue.loading()); + + final UserRepository repository; + + Future loadUser(String id) async { + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() => repository.getUser(id)); + } + + Future updateUser(String id, Map data) async { + state = await AsyncValue.guard(() => repository.updateUser(id, data)); + } +} + +final userNotifierProvider = StateNotifierProvider>((ref) { + return UserNotifier(ref.watch(userRepositoryProvider)); +}); + +// Usage in widget +class ProfileScreen extends ConsumerWidget { + final String userId; + + const ProfileScreen({required this.userId}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final userAsync = ref.watch(userProvider(userId)); + + return userAsync.when( + data: (user) => Text(user.name), + loading: () => CircularProgressIndicator(), + error: (error, stack) => Text('Error: $error'), + ); + } +} +``` + +### Offline-First with Drift (SQLite) + +```dart +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; + +// Define tables +class Users extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get name => text().withLength(min: 1, max: 100)(); + TextColumn get email => text()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); +} + +class Posts extends Table { + IntColumn get id => integer().autoIncrement()(); + IntColumn get userId => integer().references(Users, #id)(); + TextColumn get title => text()(); + TextColumn get content => text()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); +} + +// Database +@DriftDatabase(tables: [Users, Posts]) +class AppDatabase extends _$AppDatabase { + AppDatabase() : super(_openConnection()); + + @override + int get schemaVersion => 1; + + static LazyDatabase _openConnection() { + return LazyDatabase(() async { + final dbFolder = await getApplicationDocumentsDirectory(); + final file = File(path.join(dbFolder.path, 'app.db')); + return NativeDatabase(file); + }); + } + + // Queries + Future> getAllUsers() => select(users).get(); + + Stream> watchAllUsers() => select(users).watch(); + + Future getUserById(int id) => + (select(users)..where((u) => u.id.equals(id))).getSingle(); + + Future createUser(UsersCompanion user) => into(users).insert(user); + + Future updateUser(User user) => update(users).replace(user); + + Future deleteUser(int id) => + (delete(users)..where((u) => u.id.equals(id))).go(); +} + +// Service with sync +class UserService { + final AppDatabase db; + final ApiClient api; + + UserService(this.db, this.api); + + Future syncUsers() async { + try { + final remoteUsers = await api.fetchUsers(); + + await db.transaction(() async { + for (final user in remoteUsers) { + await db.into(db.users).insertOnConflictUpdate( + UsersCompanion( + id: Value(user.id), + name: Value(user.name), + email: Value(user.email), + ), + ); + } + }); + } catch (e) { + print('Sync failed: $e'); + } + } + + Stream> watchUsers() { + // Start sync in background + syncUsers(); + // Return live data from local DB + return db.watchAllUsers(); + } +} +``` + +## Performance Optimization + +### React Native + +```typescript +// Use React.memo for component optimization +export const UserCard = React.memo(({ user }: { user: User }) => { + return ( + + {user.name} + + ); +}); + +// Use useMemo for expensive calculations +const sortedUsers = useMemo(() => { + return users.sort((a, b) => a.name.localeCompare(b.name)); +}, [users]); + +// Use useCallback for function memoization +const handlePress = useCallback(() => { + navigation.navigate('Details', { id: user.id }); +}, [user.id]); + +// FlatList optimization + } + keyExtractor={item => item.id} + removeClippedSubviews={true} + maxToRenderPerBatch={10} + windowSize={21} + getItemLayout={(data, index) => ({ + length: ITEM_HEIGHT, + offset: ITEM_HEIGHT * index, + index, + })} +/> +``` + +### Flutter + +```dart +// Use const constructors +const Text('Hello'); + +// ListView optimization +ListView.builder( + itemCount: items.length, + itemBuilder: (context, index) { + return ItemCard(item: items[index]); + }, + cacheExtent: 100, +); + +// Image caching +CachedNetworkImage( + imageUrl: user.avatarUrl, + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => Icon(Icons.error), +); +``` + +## Testing + +### React Native Testing Library + +```typescript +import { render, fireEvent, waitFor } from '@testing-library/react-native'; + +describe('LoginScreen', () => { + it('should submit form with valid credentials', async () => { + const mockLogin = jest.fn(); + const { getByPlaceholderText, getByText } = render( + + ); + + fireEvent.changeText(getByPlaceholderText('Email'), 'test@example.com'); + fireEvent.changeText(getByPlaceholderText('Password'), 'password123'); + fireEvent.press(getByText('Login')); + + await waitFor(() => { + expect(mockLogin).toHaveBeenCalledWith('test@example.com', 'password123'); + }); + }); +}); +``` + +### Flutter Widget Tests + +```dart +testWidgets('Counter increments', (WidgetTester tester) async { + await tester.pumpWidget(MyApp()); + + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); +}); +``` + +## Best Practices + +1. **Use TypeScript/Dart for type safety** +2. **Implement offline-first architecture** +3. **Optimize list rendering** +4. **Use memo/const for performance** +5. **Handle loading and error states** +6. **Implement proper navigation** +7. **Test on real devices** +8. **Monitor app performance** +9. **Handle permissions properly** +10. **Follow platform guidelines** diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..ac5681a --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,49 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:Dieshen/claude_marketplace:plugins/mobile-development", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "325f293b98d10c02fc7c9a4621156c2c370ffac2", + "treeHash": "e1223a9967b89d7bced186d7e84ae918395932b0d2a5b0c6f1e6db33460b63ca", + "generatedAt": "2025-11-28T10:10:24.266146Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "mobile-development", + "description": "Mobile development patterns for React Native and Flutter with offline-first architecture", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "6c316eb0b09a4c3187f9b70cda4e926c64529efb61cb6909f2baea7be0046255" + }, + { + "path": "agents/mobile-app-builder.md", + "sha256": "721da79ae7616cb1a912f3e89a086f8de6f9b14d7ea485ff6f2d3741cdfc1824" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "5e1159aa55ce207461c6ffe7624c951c10b5da27636aa07503a9d57e3bd85337" + }, + { + "path": "commands/mobile-patterns.md", + "sha256": "bd57309710324b1dd2f744f1478dfb932322e20f249d9746961d1685809128e5" + } + ], + "dirSha256": "e1223a9967b89d7bced186d7e84ae918395932b0d2a5b0c6f1e6db33460b63ca" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file