@ranimontagna/agent-toolkit 0.1.5 → 0.1.6
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.
- package/README.md +42 -8
- package/package.json +1 -1
- package/skills/frontend/react/react-patterns/LICENSE +21 -0
- package/skills/frontend/react/react-patterns/NOTICE.md +11 -0
- package/skills/frontend/react/react-patterns/SKILL.md +341 -0
- package/skills/frontend/react/react-performance/LICENSE +21 -0
- package/skills/frontend/react/react-performance/NOTICE.md +11 -0
- package/skills/frontend/react/react-performance/SKILL.md +574 -0
- package/skills/frontend/react/react-testing/LICENSE +21 -0
- package/skills/frontend/react/react-testing/NOTICE.md +11 -0
- package/skills/frontend/react/react-testing/SKILL.md +423 -0
- package/skills/frontend/react-native/react-native-expert/LICENSE +21 -0
- package/skills/frontend/react-native/react-native-expert/NOTICE.md +11 -0
- package/skills/frontend/react-native/react-native-expert/SKILL.md +187 -0
- package/skills/frontend/react-native/react-native-expert/references/expo-router.md +187 -0
- package/skills/frontend/react-native/react-native-expert/references/list-optimization.md +204 -0
- package/skills/frontend/react-native/react-native-expert/references/platform-handling.md +188 -0
- package/skills/frontend/react-native/react-native-expert/references/project-structure.md +171 -0
- package/skills/frontend/react-native/react-native-expert/references/storage-hooks.md +173 -0
- package/skills/frontend/react-native/react-native-unistyles-v3/LICENSE +21 -0
- package/skills/frontend/react-native/react-native-unistyles-v3/NOTICE.md +11 -0
- package/skills/frontend/react-native/react-native-unistyles-v3/SKILL.md +159 -0
- package/skills/frontend/react-native/react-native-unistyles-v3/references/api-reference.md +495 -0
- package/skills/frontend/react-native/react-native-unistyles-v3/references/common-issues.md +389 -0
- package/skills/frontend/react-native/react-native-unistyles-v3/references/setup-guide.md +217 -0
- package/skills/frontend/react-native/react-native-unistyles-v3/references/styling-patterns.md +705 -0
- package/skills/frontend/react-native/react-native-unistyles-v3/references/third-party-integration.md +318 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Expo Router
|
|
2
|
+
|
|
3
|
+
## Project Structure
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
app/
|
|
7
|
+
├── _layout.tsx # Root layout
|
|
8
|
+
├── index.tsx # Home (/)
|
|
9
|
+
├── +not-found.tsx # 404 page
|
|
10
|
+
├── (tabs)/ # Tab group
|
|
11
|
+
│ ├── _layout.tsx # Tab bar config
|
|
12
|
+
│ ├── index.tsx # First tab
|
|
13
|
+
│ └── profile.tsx # Profile tab
|
|
14
|
+
├── (auth)/ # Auth group (no tabs)
|
|
15
|
+
│ ├── _layout.tsx
|
|
16
|
+
│ ├── login.tsx
|
|
17
|
+
│ └── register.tsx
|
|
18
|
+
├── settings/
|
|
19
|
+
│ ├── _layout.tsx # Stack layout
|
|
20
|
+
│ ├── index.tsx # Settings main
|
|
21
|
+
│ └── notifications.tsx
|
|
22
|
+
└── details/[id].tsx # Dynamic route
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Root Layout
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// app/_layout.tsx
|
|
29
|
+
import { Stack } from 'expo-router';
|
|
30
|
+
import { ThemeProvider } from '@react-navigation/native';
|
|
31
|
+
|
|
32
|
+
export default function RootLayout() {
|
|
33
|
+
return (
|
|
34
|
+
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
|
35
|
+
<Stack screenOptions={{ headerShown: false }}>
|
|
36
|
+
<Stack.Screen name="(tabs)" />
|
|
37
|
+
<Stack.Screen name="(auth)" />
|
|
38
|
+
<Stack.Screen
|
|
39
|
+
name="details/[id]"
|
|
40
|
+
options={{ presentation: 'modal' }}
|
|
41
|
+
/>
|
|
42
|
+
</Stack>
|
|
43
|
+
</ThemeProvider>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Tab Layout
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// app/(tabs)/_layout.tsx
|
|
52
|
+
import { Tabs } from 'expo-router';
|
|
53
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
54
|
+
|
|
55
|
+
export default function TabLayout() {
|
|
56
|
+
return (
|
|
57
|
+
<Tabs
|
|
58
|
+
screenOptions={{
|
|
59
|
+
tabBarActiveTintColor: '#007AFF',
|
|
60
|
+
headerShown: true,
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
<Tabs.Screen
|
|
64
|
+
name="index"
|
|
65
|
+
options={{
|
|
66
|
+
title: 'Home',
|
|
67
|
+
tabBarIcon: ({ color, size }) => (
|
|
68
|
+
<Ionicons name="home" color={color} size={size} />
|
|
69
|
+
),
|
|
70
|
+
}}
|
|
71
|
+
/>
|
|
72
|
+
<Tabs.Screen
|
|
73
|
+
name="profile"
|
|
74
|
+
options={{
|
|
75
|
+
title: 'Profile',
|
|
76
|
+
tabBarIcon: ({ color, size }) => (
|
|
77
|
+
<Ionicons name="person" color={color} size={size} />
|
|
78
|
+
),
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
81
|
+
</Tabs>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Navigation
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { router, useLocalSearchParams, Link } from 'expo-router';
|
|
90
|
+
|
|
91
|
+
// Programmatic navigation
|
|
92
|
+
router.push('/details/123'); // Push to stack
|
|
93
|
+
router.replace('/home'); // Replace current
|
|
94
|
+
router.back(); // Go back
|
|
95
|
+
router.canGoBack(); // Check if can go back
|
|
96
|
+
|
|
97
|
+
// With params
|
|
98
|
+
router.push({
|
|
99
|
+
pathname: '/details/[id]',
|
|
100
|
+
params: { id: '123', title: 'Item' },
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Link component
|
|
104
|
+
<Link href="/profile" asChild>
|
|
105
|
+
<Pressable>
|
|
106
|
+
<Text>Go to Profile</Text>
|
|
107
|
+
</Pressable>
|
|
108
|
+
</Link>
|
|
109
|
+
|
|
110
|
+
// Reading params
|
|
111
|
+
function DetailsScreen() {
|
|
112
|
+
const { id, title } = useLocalSearchParams<{ id: string; title?: string }>();
|
|
113
|
+
return <Text>Details for {id}</Text>;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Protected Routes
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// app/(auth)/_layout.tsx
|
|
121
|
+
import { Redirect, Stack } from 'expo-router';
|
|
122
|
+
import { useAuth } from '@/hooks/useAuth';
|
|
123
|
+
|
|
124
|
+
export default function AuthLayout() {
|
|
125
|
+
const { user, isLoading } = useAuth();
|
|
126
|
+
|
|
127
|
+
if (isLoading) {
|
|
128
|
+
return <LoadingScreen />;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (user) {
|
|
132
|
+
return <Redirect href="/(tabs)" />;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return <Stack screenOptions={{ headerShown: false }} />;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// app/(tabs)/_layout.tsx
|
|
139
|
+
export default function TabLayout() {
|
|
140
|
+
const { user, isLoading } = useAuth();
|
|
141
|
+
|
|
142
|
+
if (isLoading) {
|
|
143
|
+
return <LoadingScreen />;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!user) {
|
|
147
|
+
return <Redirect href="/(auth)/login" />;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return <Tabs>...</Tabs>;
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Deep Linking
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
// app.json
|
|
158
|
+
{
|
|
159
|
+
"expo": {
|
|
160
|
+
"scheme": "myapp",
|
|
161
|
+
"web": {
|
|
162
|
+
"bundler": "metro"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// Handle: myapp://details/123
|
|
170
|
+
// app/details/[id].tsx handles automatically
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Quick Reference
|
|
174
|
+
|
|
175
|
+
| Component | Purpose |
|
|
176
|
+
|-----------|---------|
|
|
177
|
+
| `<Stack>` | Stack navigator |
|
|
178
|
+
| `<Tabs>` | Tab navigator |
|
|
179
|
+
| `<Drawer>` | Drawer navigator |
|
|
180
|
+
| `<Link>` | Declarative navigation |
|
|
181
|
+
|
|
182
|
+
| router method | Behavior |
|
|
183
|
+
|---------------|----------|
|
|
184
|
+
| `push()` | Add to stack |
|
|
185
|
+
| `replace()` | Replace current |
|
|
186
|
+
| `back()` | Go back |
|
|
187
|
+
| `dismissAll()` | Dismiss modals |
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# List Optimization
|
|
2
|
+
|
|
3
|
+
## Optimized FlatList
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { FlatList, ListRenderItem } from 'react-native';
|
|
7
|
+
import { memo, useCallback } from 'react';
|
|
8
|
+
|
|
9
|
+
interface Item {
|
|
10
|
+
id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
subtitle: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Memoized list item
|
|
16
|
+
const ListItem = memo(function ListItem({
|
|
17
|
+
item,
|
|
18
|
+
onPress
|
|
19
|
+
}: {
|
|
20
|
+
item: Item;
|
|
21
|
+
onPress: (id: string) => void;
|
|
22
|
+
}) {
|
|
23
|
+
return (
|
|
24
|
+
<Pressable onPress={() => onPress(item.id)} style={styles.item}>
|
|
25
|
+
<Text style={styles.title}>{item.title}</Text>
|
|
26
|
+
<Text style={styles.subtitle}>{item.subtitle}</Text>
|
|
27
|
+
</Pressable>
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
function OptimizedList({ data }: { data: Item[] }) {
|
|
32
|
+
// Memoize callbacks
|
|
33
|
+
const handlePress = useCallback((id: string) => {
|
|
34
|
+
console.log('Selected:', id);
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
const renderItem: ListRenderItem<Item> = useCallback(
|
|
38
|
+
({ item }) => <ListItem item={item} onPress={handlePress} />,
|
|
39
|
+
[handlePress]
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const keyExtractor = useCallback((item: Item) => item.id, []);
|
|
43
|
+
|
|
44
|
+
// Fixed height for getItemLayout
|
|
45
|
+
const getItemLayout = useCallback(
|
|
46
|
+
(_: any, index: number) => ({
|
|
47
|
+
length: ITEM_HEIGHT,
|
|
48
|
+
offset: ITEM_HEIGHT * index,
|
|
49
|
+
index,
|
|
50
|
+
}),
|
|
51
|
+
[]
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<FlatList
|
|
56
|
+
data={data}
|
|
57
|
+
renderItem={renderItem}
|
|
58
|
+
keyExtractor={keyExtractor}
|
|
59
|
+
getItemLayout={getItemLayout}
|
|
60
|
+
// Performance props
|
|
61
|
+
removeClippedSubviews
|
|
62
|
+
maxToRenderPerBatch={10}
|
|
63
|
+
windowSize={5}
|
|
64
|
+
initialNumToRender={10}
|
|
65
|
+
updateCellsBatchingPeriod={50}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const ITEM_HEIGHT = 72;
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## SectionList
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { SectionList } from 'react-native';
|
|
77
|
+
|
|
78
|
+
interface Section {
|
|
79
|
+
title: string;
|
|
80
|
+
data: Item[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function GroupedList({ sections }: { sections: Section[] }) {
|
|
84
|
+
const renderSectionHeader = useCallback(
|
|
85
|
+
({ section }: { section: Section }) => (
|
|
86
|
+
<View style={styles.sectionHeader}>
|
|
87
|
+
<Text style={styles.sectionTitle}>{section.title}</Text>
|
|
88
|
+
</View>
|
|
89
|
+
),
|
|
90
|
+
[]
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<SectionList
|
|
95
|
+
sections={sections}
|
|
96
|
+
renderItem={renderItem}
|
|
97
|
+
renderSectionHeader={renderSectionHeader}
|
|
98
|
+
keyExtractor={keyExtractor}
|
|
99
|
+
stickySectionHeadersEnabled
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Pull to Refresh
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
function RefreshableList({ data, onRefresh }: Props) {
|
|
109
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
110
|
+
|
|
111
|
+
const handleRefresh = useCallback(async () => {
|
|
112
|
+
setRefreshing(true);
|
|
113
|
+
await onRefresh();
|
|
114
|
+
setRefreshing(false);
|
|
115
|
+
}, [onRefresh]);
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<FlatList
|
|
119
|
+
data={data}
|
|
120
|
+
renderItem={renderItem}
|
|
121
|
+
refreshControl={
|
|
122
|
+
<RefreshControl
|
|
123
|
+
refreshing={refreshing}
|
|
124
|
+
onRefresh={handleRefresh}
|
|
125
|
+
tintColor="#007AFF"
|
|
126
|
+
/>
|
|
127
|
+
}
|
|
128
|
+
/>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Infinite Scroll
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
function InfiniteList() {
|
|
137
|
+
const [data, setData] = useState<Item[]>([]);
|
|
138
|
+
const [loading, setLoading] = useState(false);
|
|
139
|
+
const [hasMore, setHasMore] = useState(true);
|
|
140
|
+
|
|
141
|
+
const loadMore = useCallback(async () => {
|
|
142
|
+
if (loading || !hasMore) return;
|
|
143
|
+
|
|
144
|
+
setLoading(true);
|
|
145
|
+
const newItems = await fetchMoreItems(data.length);
|
|
146
|
+
|
|
147
|
+
if (newItems.length === 0) {
|
|
148
|
+
setHasMore(false);
|
|
149
|
+
} else {
|
|
150
|
+
setData(prev => [...prev, ...newItems]);
|
|
151
|
+
}
|
|
152
|
+
setLoading(false);
|
|
153
|
+
}, [data.length, loading, hasMore]);
|
|
154
|
+
|
|
155
|
+
const renderFooter = useCallback(() => {
|
|
156
|
+
if (!loading) return null;
|
|
157
|
+
return <ActivityIndicator style={styles.loader} />;
|
|
158
|
+
}, [loading]);
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<FlatList
|
|
162
|
+
data={data}
|
|
163
|
+
renderItem={renderItem}
|
|
164
|
+
onEndReached={loadMore}
|
|
165
|
+
onEndReachedThreshold={0.5}
|
|
166
|
+
ListFooterComponent={renderFooter}
|
|
167
|
+
/>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## FlashList (Alternative)
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { FlashList } from '@shopify/flash-list';
|
|
176
|
+
|
|
177
|
+
function FastList({ data }: { data: Item[] }) {
|
|
178
|
+
return (
|
|
179
|
+
<FlashList
|
|
180
|
+
data={data}
|
|
181
|
+
renderItem={renderItem}
|
|
182
|
+
estimatedItemSize={72}
|
|
183
|
+
keyExtractor={keyExtractor}
|
|
184
|
+
/>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Quick Reference
|
|
190
|
+
|
|
191
|
+
| Prop | Purpose |
|
|
192
|
+
|------|---------|
|
|
193
|
+
| `removeClippedSubviews` | Unmount off-screen items |
|
|
194
|
+
| `maxToRenderPerBatch` | Items per render batch |
|
|
195
|
+
| `windowSize` | Render window multiplier |
|
|
196
|
+
| `initialNumToRender` | Initial items to render |
|
|
197
|
+
| `getItemLayout` | Skip measurement (fixed height) |
|
|
198
|
+
|
|
199
|
+
| Optimization | When |
|
|
200
|
+
|--------------|------|
|
|
201
|
+
| `memo()` | All list items |
|
|
202
|
+
| `useCallback` | renderItem, keyExtractor |
|
|
203
|
+
| `getItemLayout` | Fixed height items |
|
|
204
|
+
| `FlashList` | Very large lists |
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Platform Handling
|
|
2
|
+
|
|
3
|
+
## Platform.select
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { Platform, StyleSheet } from 'react-native';
|
|
7
|
+
|
|
8
|
+
const styles = StyleSheet.create({
|
|
9
|
+
card: {
|
|
10
|
+
padding: 16,
|
|
11
|
+
borderRadius: 12,
|
|
12
|
+
backgroundColor: '#fff',
|
|
13
|
+
...Platform.select({
|
|
14
|
+
ios: {
|
|
15
|
+
shadowColor: '#000',
|
|
16
|
+
shadowOffset: { width: 0, height: 2 },
|
|
17
|
+
shadowOpacity: 0.1,
|
|
18
|
+
shadowRadius: 8,
|
|
19
|
+
},
|
|
20
|
+
android: {
|
|
21
|
+
elevation: 4,
|
|
22
|
+
},
|
|
23
|
+
}),
|
|
24
|
+
},
|
|
25
|
+
text: {
|
|
26
|
+
fontFamily: Platform.select({
|
|
27
|
+
ios: 'Helvetica Neue',
|
|
28
|
+
android: 'Roboto',
|
|
29
|
+
}),
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Platform.OS
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { Platform } from 'react-native';
|
|
38
|
+
|
|
39
|
+
function MyComponent() {
|
|
40
|
+
const isIOS = Platform.OS === 'ios';
|
|
41
|
+
const isAndroid = Platform.OS === 'android';
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<View>
|
|
45
|
+
{isIOS && <IOSOnlyComponent />}
|
|
46
|
+
<Text>{isAndroid ? 'Android' : 'iOS'}</Text>
|
|
47
|
+
</View>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Platform-Specific Files
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
components/
|
|
56
|
+
├── Button.tsx # Shared logic
|
|
57
|
+
├── Button.ios.tsx # iOS-specific
|
|
58
|
+
└── Button.android.tsx # Android-specific
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// Import resolves to correct platform file
|
|
63
|
+
import Button from './components/Button';
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## SafeAreaView
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { SafeAreaView, StyleSheet } from 'react-native';
|
|
70
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
71
|
+
|
|
72
|
+
// Method 1: SafeAreaView component
|
|
73
|
+
function Screen() {
|
|
74
|
+
return (
|
|
75
|
+
<SafeAreaView style={styles.container}>
|
|
76
|
+
<Content />
|
|
77
|
+
</SafeAreaView>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Method 2: useSafeAreaInsets hook (more control)
|
|
82
|
+
function CustomHeader() {
|
|
83
|
+
const insets = useSafeAreaInsets();
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<View style={[styles.header, { paddingTop: insets.top }]}>
|
|
87
|
+
<Text>Header</Text>
|
|
88
|
+
</View>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Method 3: SafeAreaProvider context
|
|
93
|
+
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
|
94
|
+
|
|
95
|
+
function App() {
|
|
96
|
+
return (
|
|
97
|
+
<SafeAreaProvider>
|
|
98
|
+
<Navigation />
|
|
99
|
+
</SafeAreaProvider>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## KeyboardAvoidingView
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { KeyboardAvoidingView, Platform } from 'react-native';
|
|
108
|
+
|
|
109
|
+
function FormScreen() {
|
|
110
|
+
return (
|
|
111
|
+
<KeyboardAvoidingView
|
|
112
|
+
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
113
|
+
style={{ flex: 1 }}
|
|
114
|
+
keyboardVerticalOffset={Platform.select({ ios: 88, android: 0 })}
|
|
115
|
+
>
|
|
116
|
+
<ScrollView>
|
|
117
|
+
<TextInput placeholder="Name" />
|
|
118
|
+
<TextInput placeholder="Email" />
|
|
119
|
+
</ScrollView>
|
|
120
|
+
</KeyboardAvoidingView>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## StatusBar
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { StatusBar, Platform } from 'react-native';
|
|
129
|
+
|
|
130
|
+
function Screen() {
|
|
131
|
+
return (
|
|
132
|
+
<>
|
|
133
|
+
<StatusBar
|
|
134
|
+
barStyle={Platform.OS === 'ios' ? 'dark-content' : 'light-content'}
|
|
135
|
+
backgroundColor={Platform.OS === 'android' ? '#000' : undefined}
|
|
136
|
+
/>
|
|
137
|
+
<Content />
|
|
138
|
+
</>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Android Back Button
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { useEffect } from 'react';
|
|
147
|
+
import { BackHandler, Platform } from 'react-native';
|
|
148
|
+
|
|
149
|
+
function useBackHandler(handler: () => boolean) {
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (Platform.OS !== 'android') return;
|
|
152
|
+
|
|
153
|
+
const subscription = BackHandler.addEventListener(
|
|
154
|
+
'hardwareBackPress',
|
|
155
|
+
handler
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
return () => subscription.remove();
|
|
159
|
+
}, [handler]);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Usage
|
|
163
|
+
function Screen() {
|
|
164
|
+
useBackHandler(() => {
|
|
165
|
+
if (hasUnsavedChanges) {
|
|
166
|
+
showDiscardAlert();
|
|
167
|
+
return true; // Prevent default back
|
|
168
|
+
}
|
|
169
|
+
return false; // Allow default back
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Quick Reference
|
|
175
|
+
|
|
176
|
+
| API | Purpose |
|
|
177
|
+
|-----|---------|
|
|
178
|
+
| `Platform.OS` | Get platform ('ios' / 'android') |
|
|
179
|
+
| `Platform.select()` | Platform-specific values |
|
|
180
|
+
| `Platform.Version` | OS version number |
|
|
181
|
+
| `.ios.tsx` / `.android.tsx` | Platform-specific files |
|
|
182
|
+
|
|
183
|
+
| Component | Purpose |
|
|
184
|
+
|-----------|---------|
|
|
185
|
+
| `SafeAreaView` | Avoid notch/home indicator |
|
|
186
|
+
| `KeyboardAvoidingView` | Keyboard handling |
|
|
187
|
+
| `StatusBar` | Status bar styling |
|
|
188
|
+
| `BackHandler` | Android back button |
|