Initial commit
This commit is contained in:
14
.claude-plugin/plugin.json
Normal file
14
.claude-plugin/plugin.json
Normal file
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# mobile-development
|
||||||
|
|
||||||
|
Mobile development patterns for React Native and Flutter with offline-first architecture
|
||||||
81
agents/mobile-app-builder.md
Normal file
81
agents/mobile-app-builder.md
Normal file
@@ -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<UserNotifier, User?>((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
|
||||||
553
commands/mobile-patterns.md
Normal file
553
commands/mobile-patterns.md
Normal file
@@ -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<RootStackParamList>();
|
||||||
|
const Tab = createBottomTabNavigator();
|
||||||
|
|
||||||
|
function TabNavigator() {
|
||||||
|
return (
|
||||||
|
<Tab.Navigator>
|
||||||
|
<Tab.Screen name="Home" component={HomeScreen} />
|
||||||
|
<Tab.Screen name="Profile" component={ProfileScreen} />
|
||||||
|
<Tab.Screen name="Settings" component={SettingsScreen} />
|
||||||
|
</Tab.Navigator>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppNavigator() {
|
||||||
|
return (
|
||||||
|
<NavigationContainer>
|
||||||
|
<Stack.Navigator>
|
||||||
|
<Stack.Screen name="Main" component={TabNavigator} />
|
||||||
|
<Stack.Screen name="Details" component={DetailsScreen} />
|
||||||
|
</Stack.Navigator>
|
||||||
|
</NavigationContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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<void>;
|
||||||
|
logout: () => void;
|
||||||
|
updateProfile: (data: Partial<User>) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUserStore = create<UserStore>()(
|
||||||
|
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<User> getUser(String id) async {
|
||||||
|
// API call
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<User> updateUser(String id, Map<String, dynamic> data) async {
|
||||||
|
// API call
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Providers
|
||||||
|
final userRepositoryProvider = Provider((ref) => UserRepository());
|
||||||
|
|
||||||
|
final userProvider = FutureProvider.family<User, String>((ref, id) async {
|
||||||
|
final repository = ref.watch(userRepositoryProvider);
|
||||||
|
return repository.getUser(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notifier for mutable state
|
||||||
|
class UserNotifier extends StateNotifier<AsyncValue<User>> {
|
||||||
|
UserNotifier(this.repository) : super(const AsyncValue.loading());
|
||||||
|
|
||||||
|
final UserRepository repository;
|
||||||
|
|
||||||
|
Future<void> loadUser(String id) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
state = await AsyncValue.guard(() => repository.getUser(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateUser(String id, Map<String, dynamic> data) async {
|
||||||
|
state = await AsyncValue.guard(() => repository.updateUser(id, data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final userNotifierProvider = StateNotifierProvider<UserNotifier, AsyncValue<User>>((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<List<User>> getAllUsers() => select(users).get();
|
||||||
|
|
||||||
|
Stream<List<User>> watchAllUsers() => select(users).watch();
|
||||||
|
|
||||||
|
Future<User> getUserById(int id) =>
|
||||||
|
(select(users)..where((u) => u.id.equals(id))).getSingle();
|
||||||
|
|
||||||
|
Future<int> createUser(UsersCompanion user) => into(users).insert(user);
|
||||||
|
|
||||||
|
Future<bool> updateUser(User user) => update(users).replace(user);
|
||||||
|
|
||||||
|
Future<int> 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<void> 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<List<User>> 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 (
|
||||||
|
<View>
|
||||||
|
<Text>{user.name}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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
|
||||||
|
<FlatList
|
||||||
|
data={items}
|
||||||
|
renderItem={({ item }) => <ItemCard item={item} />}
|
||||||
|
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(
|
||||||
|
<LoginScreen onLogin={mockLogin} />
|
||||||
|
);
|
||||||
|
|
||||||
|
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**
|
||||||
49
plugin.lock.json
Normal file
49
plugin.lock.json
Normal file
@@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user