Expo template with pre-built components and patterns.
Update app.json:
{
"expo": {
"name": "App Name",
"slug": "appname",
"icon": "./assets/icon.png",
"android": {
"package": "com.vandam.appname"
}
}
}Delete /android folder before first build (regenerates with your config).
bunx expo run:android- Build and runbun start- Start dev serverbun run sync-version- Sync version from app.json to package.json + build.gradlebun run generate-icon- Generate app icon from first letter of app name
Workflow at .github/workflows/build.yml builds APK and creates release:
- Triggered manually via
workflow_dispatch - Builds production APK using EAS
- Creates GitHub release with changelog
Requires EXPO_TOKEN secret in repo settings.
| Component | Purpose |
|---|---|
ContentContainer |
Page wrapper with header, handles background color |
StyledText |
Theme-aware text with custom font |
StyledButton |
Button with haptic feedback |
HapticPressable |
Pressable with haptic feedback |
Header |
Top bar with title, back button, icons |
Navbar |
Bottom tab navigation |
CustomScrollView |
FlatList with custom scroll indicator |
ToggleSwitch |
On/off toggle |
SelectorButton |
Shows label + value, navigates to options page |
OptionsSelector |
Full-page picker for selecting from options |
Always use n() for sizes - normalizes across screen densities:
import { n } from "@/utils/scaling";
const styles = StyleSheet.create({
container: { padding: n(16) },
text: { fontSize: n(18) },
icon: { width: n(24), height: n(24) }
});To add a new tab:
- Create screen file
app/(tabs)/search.tsx - Add to
TABS_CONFIGinapp/(tabs)/_layout.tsx:
export const TABS_CONFIG: ReadonlyArray<TabConfigItem> = [
{ name: "Home", screenName: "index", iconName: "home" },
{ name: "Search", screenName: "search", iconName: "search" }, // new
{ name: "Settings", screenName: "settings", iconName: "settings" },
] as const;- Add
<Tabs.Screen>entry:
<Tabs.Screen name="index" />
<Tabs.Screen name="search" /> // new
<Tabs.Screen name="settings" />Icons: Use MaterialIcons names.
Settings use nested routes:
app/(tabs)/settings.tsx → Main settings page
app/settings/customise.tsx → Customise options
app/settings/display-mode.tsx → Options page (example)
Use SelectorButton + OptionsSelector for option pickers:
// In settings page
<SelectorButton
label="Display Mode"
value={currentValue}
href="/settings/display-mode"
/>
// In options page (app/settings/display-mode.tsx)
<OptionsSelector
title="Display Mode"
options={[{ label: "Standard", value: "standard" }, ...]}
selectedValue={displayMode}
onSelect={(value) => setDisplayMode(value)}
/>For destructive actions, use the confirm screen pattern:
router.push({
pathname: "/confirm",
params: {
title: "Clear Cache",
message: "Are you sure?",
confirmText: "Clear",
action: "clearCache",
returnPath: "/(tabs)/settings",
},
});
// Handle confirmation in useEffect
useEffect(() => {
if (params.confirmed === "true" && params.action === "clearCache") {
router.setParams({ confirmed: undefined, action: undefined });
// Do the action
}
}, [params.confirmed, params.action]);Wrapped in app/_layout.tsx:
InvertColorsContext- Theme toggle (black/white), persists to AsyncStorageDisplayModeContext- Example setting context (seeapp/settings/display-mode.tsx)HapticContext-useHaptic()returns function to trigger feedback
Use: const { invertColors } = useInvertColors();
- Use
n()for all numeric style values - Use
buninstead of npm - Minimize
useEffect- see You Might Not Need an Effect - Readable code > comments