Files
sgw-owner-app/THEME_GUIDE.md
2025-11-28 16:59:57 +07:00

503 lines
12 KiB
Markdown

# Theme System Documentation
## Tổng quan
Hệ thống theme hỗ trợ **Light Mode**, **Dark Mode****System Mode** (tự động theo hệ thống). Theme preference được lưu trong AsyncStorage và tự động khôi phục khi khởi động lại ứng dụng.
## Kiến trúc Theme System
### 1. Theme Provider (`hooks/use-theme-context.tsx`)
Theme Provider là core của hệ thống theme, quản lý state và đồng bộ với system theme.
**Các tính năng chính:**
- Quản lý `themeMode`: `'light' | 'dark' | 'system'`
- Tự động detect system theme thông qua nhiều nguồn:
- `Appearance.getColorScheme()` - iOS/Android system theme
- `useColorScheme()` hook từ React Native
- `Appearance.addChangeListener()` - listen system theme changes
- `AppState` listener - sync lại khi app active
- Lưu và restore theme preference từ AsyncStorage
- Export ra `colorScheme` cuối cùng: `'light' | 'dark'`
**ThemeContextType:**
```typescript
interface ThemeContextType {
themeMode: ThemeMode; // User's choice: 'light' | 'dark' | 'system'
colorScheme: ColorScheme; // Final theme: 'light' | 'dark'
colors: typeof Colors.light; // Theme colors object
setThemeMode: (mode: ThemeMode) => Promise<void>;
getColor: (colorName: ColorName) => string;
isHydrated: boolean; // AsyncStorage đã load xong
}
```
**Cách hoạt động:**
```typescript
// Xác định colorScheme cuối cùng
const colorScheme: ColorScheme =
themeMode === "system" ? systemScheme : themeMode;
```
### 2. Colors Configuration (`constants/theme.ts`)
Định nghĩa tất cả colors cho light và dark theme:
```typescript
export const Colors = {
light: {
text: "#11181C",
textSecondary: "#687076",
background: "#fff",
backgroundSecondary: "#f5f5f5",
surface: "#ffffff",
surfaceSecondary: "#f8f9fa",
tint: "#0a7ea4",
primary: "#007AFF",
secondary: "#5AC8FA",
success: "#34C759",
warning: "#ff6600",
error: "#FF3B30",
icon: "#687076",
border: "#C6C6C8",
separator: "#E5E5E7",
card: "#ffffff",
// ... more colors
},
dark: {
text: "#ECEDEE",
textSecondary: "#8E8E93",
background: "#000000",
backgroundSecondary: "#1C1C1E",
surface: "#1C1C1E",
surfaceSecondary: "#2C2C2E",
tint: "#fff",
primary: "#0A84FF",
secondary: "#64D2FF",
success: "#30D158",
warning: "#ff6600",
error: "#FF453A",
icon: "#8E8E93",
border: "#38383A",
separator: "#38383A",
card: "#1C1C1E",
// ... more colors
},
};
export type ColorName = keyof typeof Colors.light;
```
### 3. Setup trong App (`app/_layout.tsx`)
Theme Provider phải wrap toàn bộ app:
```tsx
export default function RootLayout() {
return (
<I18nProvider>
<AppThemeProvider>
<AppContent />
</AppThemeProvider>
</I18nProvider>
);
}
function AppContent() {
const { colorScheme } = useThemeContext();
return (
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<Stack>{/* ... routes */}</Stack>
<StatusBar style="auto" />
</ThemeProvider>
);
}
```
## Cách sử dụng Theme
### 1. useThemeContext (Core Hook)
Hook chính để access theme state:
```tsx
import { useThemeContext } from "@/hooks/use-theme-context";
function MyComponent() {
const {
themeMode, // 'light' | 'dark' | 'system'
colorScheme, // 'light' | 'dark'
colors, // Colors object
setThemeMode, // Change theme
getColor, // Get color by name
isHydrated, // AsyncStorage loaded
} = useThemeContext();
return (
<View style={{ backgroundColor: colors.background }}>
<Text style={{ color: colors.text }}>
Mode: {themeMode}, Scheme: {colorScheme}
</Text>
</View>
);
}
```
### 2. useColorScheme Hook
Alias để lấy colorScheme nhanh:
```tsx
import { useColorScheme } from "@/hooks/use-theme-context";
function MyComponent() {
const colorScheme = useColorScheme(); // 'light' | 'dark'
return <Text>Current theme: {colorScheme}</Text>;
}
```
**⚠️ Lưu ý:** `useColorScheme` từ `use-theme-context.tsx`, KHÔNG phải từ `react-native`.
### 3. useThemeColor Hook
Override colors cho specific themes:
```tsx
import { useThemeColor } from "@/hooks/use-theme-color";
function MyComponent() {
// Với override
const backgroundColor = useThemeColor(
{ light: "#ffffff", dark: "#1C1C1E" },
"surface"
);
// Không override, dùng color từ theme
const textColor = useThemeColor({}, "text");
return (
<View style={{ backgroundColor }}>
<Text style={{ color: textColor }}>Text</Text>
</View>
);
}
```
**Cách hoạt động:**
```typescript
// Ưu tiên props override trước, sau đó mới dùng Colors
const colorFromProps = props[colorScheme];
return colorFromProps || Colors[colorScheme][colorName];
```
### 4. useAppTheme Hook (Recommended)
Hook tiện lợi với pre-built styles và utilities:
```tsx
import { useAppTheme } from "@/hooks/use-app-theme";
function MyComponent() {
const { colors, styles, utils } = useAppTheme();
return (
<View style={styles.container}>
<TouchableOpacity style={styles.primaryButton}>
<Text style={styles.primaryButtonText}>Primary Button</Text>
</TouchableOpacity>
<View style={styles.card}>
<Text style={{ color: colors.text }}>
Theme is {utils.isDark ? "Dark" : "Light"}
</Text>
</View>
<View
style={{
backgroundColor: utils.getOpacityColor("primary", 0.1),
}}
>
<Text>Transparent background</Text>
</View>
</View>
);
}
```
### 5. Themed Components
**ThemedView****ThemedText** - Tự động apply theme colors:
```tsx
import { ThemedText } from "@/components/themed-text";
import { ThemedView } from "@/components/themed-view";
function MyComponent() {
return (
<ThemedView>
<ThemedText type="title">Title Text</ThemedText>
<ThemedText type="subtitle">Subtitle</ThemedText>
<ThemedText type="default">Regular Text</ThemedText>
<ThemedText type="link">Link Text</ThemedText>
{/* Override với custom colors */}
<ThemedText lightColor="#000000" darkColor="#FFFFFF">
Custom colored text
</ThemedText>
</ThemedView>
);
}
```
**ThemedText types:**
- `default` - 16px regular
- `title` - 32px bold
- `subtitle` - 20px bold
- `defaultSemiBold` - 16px semibold
- `link` - 16px với color #0a7ea4
### 6. Theme Toggle Component
Component có sẵn để user chọn theme:
```tsx
import { ThemeToggle } from "@/components/theme-toggle";
function SettingsScreen() {
return (
<View>
<ThemeToggle />
</View>
);
}
```
Component này hiển thị 3 options: Light, Dark, System với icons và labels đa ngôn ngữ.
## Available Styles từ useAppTheme
```typescript
const { styles } = useAppTheme();
// Container styles
styles.container; // Flex 1 container với background
styles.surface; // Surface với padding 16, borderRadius 12
styles.card; // Card với shadow, elevation
// Button styles
styles.primaryButton; // Primary button với colors.primary
styles.secondaryButton; // Secondary button với border
styles.primaryButtonText; // White text cho primary button
styles.secondaryButtonText; // Theme text cho secondary button
// Input styles
styles.textInput; // Text input với border, padding
// Status styles
styles.successContainer; // Success background với border
styles.warningContainer; // Warning background với border
styles.errorContainer; // Error background với border
// Utility
styles.separator; // 1px line separator
```
## Theme Utilities
```typescript
const { utils } = useAppTheme();
// Check theme
utils.isDark; // boolean - true nếu dark mode
utils.isLight; // boolean - true nếu light mode
// Toggle theme (ignores system mode)
utils.toggleTheme(); // Switch giữa light ↔ dark
// Get color với opacity
utils.getOpacityColor("primary", 0.1); // rgba(0, 122, 255, 0.1)
```
## Luồng hoạt động của Theme System
```
1. App khởi động
└─→ ThemeProvider mount
├─→ Load saved themeMode từ AsyncStorage ('light'/'dark'/'system')
├─→ Detect systemScheme từ OS
│ ├─→ Appearance.getColorScheme()
│ ├─→ useColorScheme() hook
│ └─→ Appearance.addChangeListener()
└─→ Tính toán colorScheme cuối cùng
└─→ themeMode === 'system' ? systemScheme : themeMode
2. User thay đổi system theme
└─→ Appearance listener fire
└─→ Update systemScheme state
└─→ Nếu themeMode === 'system'
└─→ colorScheme tự động update
└─→ Components re-render với colors mới
3. User chọn theme trong app
└─→ setThemeMode('light'/'dark'/'system')
├─→ Update themeMode state
├─→ Save vào AsyncStorage
└─→ colorScheme update
└─→ Components re-render
4. App về foreground
└─→ AppState listener fire
└─→ Sync lại systemScheme (phòng user đổi system theme khi app background)
```
## Storage
Theme preference được lưu với key: `'theme_mode'`
```typescript
// Tự động xử lý bởi ThemeProvider
await setStorageItem("theme_mode", "light" | "dark" | "system");
const savedMode = await getStorageItem("theme_mode");
```
## Best Practices
1. **Sử dụng hooks đúng context:**
- `useThemeContext()` - Khi cần full control (themeMode, setThemeMode)
- `useColorScheme()` - Chỉ cần biết light/dark
- `useAppTheme()` - Recommended cho UI components (có styles + utils)
- `useThemeColor()` - Khi cần override colors
2. **Sử dụng Themed Components:**
```tsx
// Good ✅
<ThemedView>
<ThemedText>Hello</ThemedText>
</ThemedView>;
// Also good ✅
const { colors } = useAppTheme();
<View style={{ backgroundColor: colors.background }}>
<Text style={{ color: colors.text }}>Hello</Text>
</View>;
```
3. **Tận dụng pre-built styles:**
```tsx
// Good ✅
const { styles } = useAppTheme();
<TouchableOpacity style={styles.primaryButton}>
// Less good ❌
<TouchableOpacity style={{
backgroundColor: colors.primary,
paddingVertical: 14,
borderRadius: 12
}}>
```
4. **Sử dụng opacity colors:**
```tsx
const { utils } = useAppTheme();
<View
style={{
backgroundColor: utils.getOpacityColor("primary", 0.1),
}}
/>;
```
5. **Check theme correctly:**
```tsx
// Good ✅
const { utils } = useAppTheme();
if (utils.isDark) { ... }
// Also good ✅
const { colorScheme } = useThemeContext();
if (colorScheme === 'dark') { ... }
```
## Troubleshooting
### Theme không được lưu
- Kiểm tra AsyncStorage permissions
- Check logs trong console: `[Theme] Failed to save theme mode:`
### Flash màu sắc khi khởi động
- ThemeProvider đã xử lý với `isHydrated` state
- Chờ AsyncStorage load xong trước khi render
### System theme không update
- Check Appearance listener đã register: `[Theme] Registering appearance listener`
- Check logs: `[Theme] System theme changed to: ...`
- iOS: Restart app sau khi đổi system theme
- Android: Cần `expo-system-ui` plugin trong `app.json`
### Colors không đúng
- Đảm bảo app wrapped trong `<AppThemeProvider>`
- Check `colorScheme` trong console logs
- Verify Colors object trong `constants/theme.ts`
## Migration Guide
Nếu đang dùng old theme system:
```tsx
// Old ❌
import { useColorScheme } from "react-native";
const colorScheme = useColorScheme();
const backgroundColor = colorScheme === "dark" ? "#000" : "#fff";
// New ✅
import { useAppTheme } from "@/hooks/use-app-theme";
const { colors } = useAppTheme();
const backgroundColor = colors.background;
```
```tsx
// Old ❌
const [theme, setTheme] = useState("light");
// New ✅
import { useThemeContext } from "@/hooks/use-theme-context";
const { themeMode, setThemeMode } = useThemeContext();
```
## Debug Logs
Enable logs để debug theme issues:
```tsx
// Trong use-theme-context.tsx, uncomment các dòng:
console.log("[Theme] Appearance.getColorScheme():", scheme);
console.log("[Theme] System theme changed to:", newScheme);
console.log("[Theme] Mode:", themeMode);
console.log("[Theme] Derived colorScheme:", colorScheme);
```
```tsx
// Trong use-theme-color.ts:
console.log("Detected theme:", theme); // Đã có sẵn
```
```tsx
// Trong _layout.tsx:
console.log("Color Scheme: ", colorScheme); // Đã có sẵn
```