503 lines
12 KiB
Markdown
503 lines
12 KiB
Markdown
# Theme System Documentation
|
|
|
|
## Tổng quan
|
|
|
|
Hệ thống theme hỗ trợ **Light Mode**, **Dark Mode** và **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** và **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
|
|
```
|