Khởi tạo ban đầu
This commit is contained in:
502
THEME_GUIDE.md
Normal file
502
THEME_GUIDE.md
Normal file
@@ -0,0 +1,502 @@
|
||||
# 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
|
||||
```
|
||||
Reference in New Issue
Block a user