请添加图片描述

案例项目开源地址:https://atomgit.com/nutpi/wanandroid_rn_openharmony

用户拨动一个开关,整个 App 的颜色就变了。背景从深色变浅色,文字从白色变黑色,所有页面同时生效。

这是怎么做到的?答案是 React Context。

先看效果

在"我的"页面有一个深色模式开关:

<View style={styles.settingRow}>
  <Text style={[styles.settingLabel, {color: theme.text}]}>🌙 深色模式</Text>
  <Switch value={darkMode} onValueChange={setDarkMode} trackColor={{false: '#767577', true: theme.accent}} />
</View>

Switch 组件

Switch 是 React Native 提供的开关组件,就是那种左右滑动的开关按钮。

value={darkMode} 绑定当前状态。darkMode 为 true 时开关打开(显示在右边),为 false 时开关关闭(显示在左边)。

onValueChange={setDarkMode} 绑定状态更新函数。用户拨动开关时,Switch 会调用这个函数,传入新的布尔值。

trackColor 设置开关轨道的颜色。false 是关闭状态的颜色,true 是打开状态的颜色。我们用 theme.accent(强调色)作为打开状态的颜色,让它更醒目。

darkMode 和 setDarkMode 从哪来

const {theme, darkMode, setDarkMode} = useTheme();

useTheme Hook 获取。这个 Hook 返回主题相关的状态和方法,任何组件都可以调用它来获取当前主题。

Context 的基本概念

React 的数据流是单向的:从父组件流向子组件,通过 props 传递。但如果一个数据需要被很多组件使用,一层层传 props 会很麻烦。

Context 提供了一种"跨层级"传递数据的方式。你在顶层设置一个值,任何子组件都可以直接获取,不需要中间组件帮忙传递。

主题就是典型的场景:几乎所有组件都需要知道当前是深色还是浅色模式,用 Context 最合适。

ThemeContext 的定义

import React, {createContext, useContext, useState, ReactNode} from 'react';
import {Theme} from '../types';

interface ThemeContextType {
  darkMode: boolean;
  setDarkMode: (value: boolean) => void;
  theme: Theme;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

接口定义

ThemeContextType 定义了 Context 里有什么内容:

  • darkMode:布尔值,表示当前是否是深色模式
  • setDarkMode:函数,用于切换深色模式
  • theme:主题对象,包含各种颜色值

TypeScript 的接口让代码更清晰,也能在编译时检查类型错误。

createContext

createContext<ThemeContextType | undefined>(undefined) 创建一个 Context 对象。

泛型 <ThemeContextType | undefined> 指定 Context 的值类型。为什么要加 undefined?因为 Context 的默认值是 undefined,在 Provider 外面使用时会是 undefined。

参数 undefined 是默认值。如果组件在 Provider 外面使用 Context,就会得到这个默认值。

ThemeProvider 组件

export const ThemeProvider = ({children}: {children: ReactNode}) => {
  const [darkMode, setDarkMode] = useState(true);

  const theme: Theme = {
    bg: darkMode ? '#0f0f23' : '#f5f5f5',
    card: darkMode ? '#1a1a2e' : '#ffffff',
    text: darkMode ? '#ffffff' : '#333333',
    subText: darkMode ? '#888888' : '#666666',
    border: darkMode ? '#2a2a4a' : '#e0e0e0',
    accent: '#6c5ce7',
    success: '#6bcb77',
    danger: '#ff6b6b',
  };

  return (
    <ThemeContext.Provider value={{darkMode, setDarkMode, theme}}>
      {children}
    </ThemeContext.Provider>
  );
};

Provider 的作用

ThemeContext.Provider 是 Context 的提供者。它包裹的所有子组件都可以访问 Context 的值。

value={{darkMode, setDarkMode, theme}} 设置 Context 的值。这个对象会被传递给所有使用 useTheme 的组件。

children 参数

{children}: {children: ReactNode} 接收子组件。ReactNode 是 React 元素的类型,可以是组件、字符串、数字、null 等。

Provider 把 children 原样渲染出来,只是在外面包了一层 Context。

darkMode 状态

useState(true) 初始值是 true,意味着 App 默认是深色模式。如果想默认浅色,改成 useState(false)

theme 对象的计算

const theme: Theme = {
  bg: darkMode ? '#0f0f23' : '#f5f5f5',
  card: darkMode ? '#1a1a2e' : '#ffffff',
  // ...
};

theme 对象根据 darkMode 的值动态计算。每个颜色都用三元表达式:深色模式用第一个值,浅色模式用第二个值。

这个对象在每次渲染时都会重新计算。当 darkMode 变化时,theme 的值也会变化,使用 theme 的组件就会重新渲染。

颜色的设计

深色模式:

  • bg: '#0f0f23' 深蓝黑色背景,比纯黑更柔和
  • card: '#1a1a2e' 卡片背景比页面背景稍浅,有层次感
  • text: '#ffffff' 白色文字
  • subText: '#888888' 灰色次要文字

浅色模式:

  • bg: '#f5f5f5' 浅灰色背景,比纯白更护眼
  • card: '#ffffff' 白色卡片
  • text: '#333333' 深灰色文字,比纯黑更柔和
  • subText: '#666666' 中灰色次要文字

accentsuccessdanger 在两种模式下保持一致,因为这些是功能色,需要保持辨识度。

useTheme Hook

export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
};

useContext

useContext(ThemeContext) 获取最近的 ThemeContext.Provider 的值。如果组件在 Provider 外面,返回 createContext 时设置的默认值(undefined)。

错误检查

if (!context) 检查是否在 Provider 外面使用。如果是,抛出错误提示开发者。这是一个常见的模式,可以在开发时尽早发现问题。

错误信息 'useTheme must be used within ThemeProvider' 明确告诉开发者问题在哪,怎么解决。

返回值

返回 context,包含 darkModesetDarkModetheme。调用方可以解构获取需要的部分。

在 App 顶层使用 Provider

// App.tsx
import {ThemeProvider} from './src/context/ThemeContext';

const App = () => {
  return (
    <ThemeProvider>
      <AuthProvider>
        {/* 其他组件 */}
      </AuthProvider>
    </ThemeProvider>
  );
};

Provider 的位置

ThemeProvider 要放在最外层,这样所有组件都能访问主题。如果放在里面,外面的组件就访问不到了。

多个 Provider 可以嵌套。我们有 ThemeProvider 和 AuthProvider,它们各自管理不同的状态,互不干扰。

组件中使用主题

const {theme} = useTheme();

<View style={{backgroundColor: theme.bg}}>
  <Text style={{color: theme.text}}>Hello</Text>
</View>

动态样式

style={{backgroundColor: theme.bg}} 把主题颜色应用到样式上。当主题切换时,theme.bg 的值变化,组件重新渲染,背景色就变了。

这种写法把样式写在 JSX 里,适合动态样式。静态样式建议用 StyleSheet.create,性能更好。

混合使用

<View style={[styles.card, {backgroundColor: theme.card, borderColor: theme.border}]}>

style 接受数组,React Native 会合并这些样式。styles.card 是静态样式(圆角、内边距等),后面的对象是动态样式(颜色)。

后面的样式会覆盖前面的同名属性,所以动态样式放后面。

主题切换的完整流程

  1. 用户拨动 Switch 开关
  2. Switch 调用 setDarkMode(newValue)
  3. ThemeProvider 里的 darkMode 状态更新
  4. theme 对象重新计算,颜色值变化
  5. ThemeContext.Provider 的 value 变化
  6. 所有使用 useTheme 的组件收到通知
  7. 这些组件重新渲染,应用新的颜色
  8. 用户看到整个 App 的颜色变了

整个过程是自动的,你只需要调用 setDarkMode,React 和 Context 会处理剩下的事情。

Theme 类型定义

// types/index.ts
export interface Theme {
  bg: string;
  card: string;
  text: string;
  subText: string;
  border: string;
  accent: string;
  success: string;
  danger: string;
}

为什么要定义类型

TypeScript 的类型定义有几个好处:

  1. 编辑器自动补全:输入 theme. 会提示所有可用的颜色
  2. 编译时检查:如果写错了颜色名(比如 theme.txt),编译时就会报错
  3. 代码文档:看类型定义就知道有哪些颜色可用

颜色的命名

用语义化的名字而不是具体颜色名。text 而不是 whitebg 而不是 darkBlue。这样切换主题时不用改名字,只改值就行。

持久化主题设置

当前实现有一个问题:用户切换到浅色模式,关闭 App 再打开,又变回深色了。因为 darkMode 的初始值是 true。

如果要持久化,可以用 AsyncStorage:

import AsyncStorage from '@react-native-async-storage/async-storage';

export const ThemeProvider = ({children}: {children: ReactNode}) => {
  const [darkMode, setDarkMode] = useState(true);

  useEffect(() => {
    AsyncStorage.getItem('darkMode').then(value => {
      if (value !== null) {
        setDarkMode(value === 'true');
      }
    });
  }, []);

  const handleSetDarkMode = (value: boolean) => {
    setDarkMode(value);
    AsyncStorage.setItem('darkMode', String(value));
  };

  // ...
};

读取保存的设置

useEffect 在组件挂载时执行,从 AsyncStorage 读取之前保存的设置。AsyncStorage.getItem 返回字符串,需要转换成布尔值。

保存设置

handleSetDarkMode 包装了 setDarkMode,在更新状态的同时保存到 AsyncStorage。

我们的项目没有实现持久化,保持简单。如果你的项目需要,可以参考上面的代码。

完整的 ThemeContext 代码

import React, {createContext, useContext, useState, ReactNode} from 'react';
import {Theme} from '../types';

interface ThemeContextType {
  darkMode: boolean;
  setDarkMode: (value: boolean) => void;
  theme: Theme;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

export const ThemeProvider = ({children}: {children: ReactNode}) => {
  const [darkMode, setDarkMode] = useState(true);

  const theme: Theme = {
    bg: darkMode ? '#0f0f23' : '#f5f5f5',
    card: darkMode ? '#1a1a2e' : '#ffffff',
    text: darkMode ? '#ffffff' : '#333333',
    subText: darkMode ? '#888888' : '#666666',
    border: darkMode ? '#2a2a4a' : '#e0e0e0',
    accent: '#6c5ce7',
    success: '#6bcb77',
    danger: '#ff6b6b',
  };

  return (
    <ThemeContext.Provider value={{darkMode, setDarkMode, theme}}>
      {children}
    </ThemeContext.Provider>
  );
};

export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
};

Context 是 React 状态管理的基础,理解了它,Redux、MobX 这些库也更容易理解。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐