Files
SeaGateway-App/THEME_GUIDE.md
2025-11-19 19:18:39 +07:00

12 KiB

Theme System Documentation

Tổng quan

Hệ thống theme hỗ trợ Light Mode, Dark ModeSystem 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:

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:

// 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:

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:

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:

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:

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:

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:

// Ưu tiên props override trước, sau đó mới dùng Colors
const colorFromProps = props[colorScheme];
return colorFromProps || Colors[colorScheme][colorName];

Hook tiện lợi với pre-built styles và utilities:

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

ThemedViewThemedText - Tự động apply theme colors:

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:

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

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

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'

// 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:

    // 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:

    // 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:

    const { utils } = useAppTheme();
    <View
      style={{
        backgroundColor: utils.getOpacityColor("primary", 0.1),
      }}
    />;
    
  5. Check theme correctly:

    // 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:

// 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;
// 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:

// 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);
// Trong use-theme-color.ts:
console.log("Detected theme:", theme); // Đã có sẵn
// Trong _layout.tsx:
console.log("Color Scheme: ", colorScheme); // Đã có sẵn