update doc themes

This commit is contained in:
2025-11-19 19:18:39 +07:00
parent 7cb35efd30
commit 1d5b29e4a7

View File

@@ -2,11 +2,49 @@
## Tổng quan
Hệ thống theme đã được cấu hình để hỗ trợ Light Mode, Dark Mode và System Mode (tự động theo hệ thống). Theme được lưu trữ trong AsyncStorage và sẽ được khôi phục khi khởi động lại ứng dụng.
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.
## Cấu trúc Theme
## Kiến trúc Theme System
### 1. Colors Configuration (`constants/theme.ts`)
### 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 = {
@@ -17,11 +55,16 @@ export const Colors = {
backgroundSecondary: "#f5f5f5",
surface: "#ffffff",
surfaceSecondary: "#f8f9fa",
tint: "#0a7ea4",
primary: "#007AFF",
secondary: "#5AC8FA",
success: "#34C759",
warning: "#FF9500",
warning: "#ff6600",
error: "#FF3B30",
icon: "#687076",
border: "#C6C6C8",
separator: "#E5E5E7",
card: "#ffffff",
// ... more colors
},
dark: {
@@ -31,107 +74,110 @@ export const Colors = {
backgroundSecondary: "#1C1C1E",
surface: "#1C1C1E",
surfaceSecondary: "#2C2C2E",
tint: "#fff",
primary: "#0A84FF",
secondary: "#64D2FF",
success: "#30D158",
warning: "#FF9F0A",
warning: "#ff6600",
error: "#FF453A",
icon: "#8E8E93",
border: "#38383A",
separator: "#38383A",
card: "#1C1C1E",
// ... more colors
},
};
export type ColorName = keyof typeof Colors.light;
```
### 2. Theme Context (`hooks/use-theme-context.tsx`)
### 3. Setup trong App (`app/_layout.tsx`)
Cung cấp theme state và functions cho toàn bộ app:
Theme Provider phải wrap toàn bộ app:
```typescript
interface ThemeContextType {
themeMode: ThemeMode; // 'light' | 'dark' | 'system'
colorScheme: ColorScheme; // 'light' | 'dark'
colors: typeof Colors.light;
setThemeMode: (mode: ThemeMode) => Promise<void>;
getColor: (colorName: ColorName) => string;
```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. Sử dụng Themed Components
### 1. useThemeContext (Core Hook)
```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="default">Regular Text</ThemedText>
</ThemedView>
);
}
```
### 2. Sử dụng Theme Hook
Hook chính để access theme state:
```tsx
import { useThemeContext } from "@/hooks/use-theme-context";
function MyComponent() {
const { colors, colorScheme, setThemeMode } = useThemeContext();
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 }}>Current theme: {colorScheme}</Text>
<Text style={{ color: colors.text }}>
Mode: {themeMode}, Scheme: {colorScheme}
</Text>
</View>
);
}
```
### 3. Sử dụng App Theme Hook (Recommended)
### 2. useColorScheme Hook
Alias để lấy colorScheme nhanh:
```tsx
import { useAppTheme } from "@/hooks/use-app-theme";
import { useColorScheme } from "@/hooks/use-theme-context";
function MyComponent() {
const { colors, styles, utils } = useAppTheme();
const colorScheme = useColorScheme(); // 'light' | 'dark'
return (
<View style={styles.container}>
<TouchableOpacity style={styles.primaryButton}>
<Text style={styles.primaryButtonText}>Button</Text>
</TouchableOpacity>
<View
style={[
styles.surface,
{
backgroundColor: utils.getOpacityColor("primary", 0.1),
},
]}
>
<Text style={{ color: colors.text }}>
Theme is {utils.isDark ? "Dark" : "Light"}
</Text>
</View>
</View>
);
return <Text>Current theme: {colorScheme}</Text>;
}
```
### 4. Sử dụng useThemeColor Hook
**⚠️ 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() {
// Override colors for specific themes
// Với override
const backgroundColor = useThemeColor(
{ light: "#ffffff", dark: "#1C1C1E" },
"surface"
);
// Không override, dùng color từ theme
const textColor = useThemeColor({}, "text");
return (
@@ -142,9 +188,84 @@ function MyComponent() {
}
```
## Theme Toggle Component
**Cách hoạt động:**
Sử dụng `ThemeToggle` component để cho phép user chọn theme:
```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";
@@ -158,6 +279,8 @@ function SettingsScreen() {
}
```
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
@@ -165,25 +288,25 @@ const { styles } = useAppTheme();
// Container styles
styles.container; // Flex 1 container với background
styles.surface; // Card surface với padding
styles.card; // Card với shadow và border radius
styles.surface; // Surface với padding 16, borderRadius 12
styles.card; // Card với shadow, elevation
// Button styles
styles.primaryButton; // Primary button style
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
styles.textInput; // Text input với border, padding
// Status styles
styles.successContainer; // Success status container
styles.warningContainer; // Warning status container
styles.errorContainer; // Error status container
styles.successContainer; // Success background với border
styles.warningContainer; // Warning background với border
styles.errorContainer; // Error background với border
// Utility
styles.separator; // Line separator
styles.separator; // 1px line separator
```
## Theme Utilities
@@ -191,44 +314,189 @@ styles.separator; // Line separator
```typescript
const { utils } = useAppTheme();
utils.isDark; // boolean - kiểm tra dark mode
utils.isLight; // boolean - kiểm tra light mode
utils.toggleTheme(); // function - toggle giữa light/dark
utils.getOpacityColor(colorName, opacity); // Tạo màu với opacity
// 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)
```
## Lưu trữ Theme Preference
## Luồng hoạt động của Theme System
Theme preference được tự động lưu trong AsyncStorage với key `'theme_mode'`. Khi app khởi động, theme sẽ được khôi phục từ storage.
```
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 `useAppTheme`** thay vì access colors trực tiếp
2. **Sử dụng pre-defined styles** từ `useAppTheme().styles`
3. **Kiểm tra theme** bằng `utils.isDark` thay vì check colorScheme
4. **Sử dụng opacity colors** cho backgrounds: `utils.getOpacityColor('primary', 0.1)`
5. **Tận dụng ThemedText và ThemedView** cho các component đơn giản
1. **Sử dụng hooks đúng context:**
## Migration từ theme cũ
- `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
Nếu bạn đang sử dụng theme cũ:
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
// Cũ
// Old ❌
import { useColorScheme } from "react-native";
const colorScheme = useColorScheme();
const backgroundColor = colorScheme === "dark" ? "#000" : "#fff";
// Mới
// New ✅
import { useAppTheme } from "@/hooks/use-app-theme";
const { colors } = useAppTheme();
const backgroundColor = colors.background;
```
## Troubleshooting
```tsx
// Old ❌
const [theme, setTheme] = useState("light");
1. **Theme không được lưu**: Kiểm tra AsyncStorage permissions
2. **Flash khi khởi động**: ThemeProvider sẽ chờ load theme trước khi render
3. **Colors không đúng**: Đảm bảo component được wrap trong ThemeProvider
// New ✅
import { useThemeContext } from "@/hooks/use-theme-context";
const { themeMode, setThemeMode } = useThemeContext();
```
## Examples
## Debug Logs
Xem `components/theme-example.tsx` để biết các cách sử dụng theme khác nhau.
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
```