Skip to content

全局状态管理

在完成了收藏和搜索功能后,我们需要进一步优化应用的状态管理。在这一节中,我们将学习如何使用 Redux Toolkit 实现全局状态管理,提升应用的性能和可维护性。

1. Redux Toolkit 回顾

Redux Toolkit 是官方推荐的 Redux 开发工具集,它简化了 Redux 的使用,提供了以下核心功能:

  1. configureStore - 简化 store 的创建
  2. createSlice - 简化 reducer 的创建
  3. createAsyncThunk - 处理异步操作
  4. createEntityAdapter - 管理规范化的数据

2. 优化全局状态结构

让我们优化我们的全局状态结构,使其更加清晰和可维护:

2.1 重构 store.js

js
// src/redux/store.js
import { configureStore } from '@reduxjs/toolkit';
import newsReducer from './slices/newsSlice';
import searchReducer from './slices/searchSlice';
import favoritesReducer from './slices/favoritesSlice';
import userReducer from './slices/userSlice';
import settingsReducer from './slices/settingsSlice';

export const store = configureStore({
  reducer: {
    news: newsReducer,
    search: searchReducer,
    favorites: favoritesReducer,
    user: userReducer,
    settings: settingsReducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: false,
    }),
});

export default store;

2.2 创建用户状态管理

js
// src/redux/slices/userSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import AsyncStorage from '@react-native-async-storage/async-storage';

const USER_KEY = 'news_app_user';

// 异步加载用户信息
export const loadUser = createAsyncThunk(
  'user/loadUser',
  async (_, { rejectWithValue }) => {
    try {
      const userStr = await AsyncStorage.getItem(USER_KEY);
      return userStr ? JSON.parse(userStr) : null;
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

// 异步保存用户信息
const saveUserToStorage = async (user) => {
  try {
    await AsyncStorage.setItem(USER_KEY, JSON.stringify(user));
  } catch (error) {
    console.error('保存用户信息失败:', error);
  }
};

const userSlice = createSlice({
  name: 'user',
  initialState: {
    isLoggedIn: false,
    userInfo: null,
    loading: false,
    error: null,
  },
  reducers: {
    login: (state, action) => {
      state.isLoggedIn = true;
      state.userInfo = action.payload;
      saveUserToStorage(action.payload);
    },
    logout: (state) => {
      state.isLoggedIn = false;
      state.userInfo = null;
      AsyncStorage.removeItem(USER_KEY);
    },
    updateUserInfo: (state, action) => {
      state.userInfo = { ...state.userInfo, ...action.payload };
      saveUserToStorage(state.userInfo);
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(loadUser.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(loadUser.fulfilled, (state, action) => {
        state.loading = false;
        if (action.payload) {
          state.isLoggedIn = true;
          state.userInfo = action.payload;
        }
      })
      .addCase(loadUser.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload;
      });
  },
});

export const { login, logout, updateUserInfo } = userSlice.actions;

export default userSlice.reducer;

2.3 创建设置状态管理

js
// src/redux/slices/settingsSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import AsyncStorage from '@react-native-async-storage/async-storage';

const SETTINGS_KEY = 'news_app_settings';

// 异步加载设置
export const loadSettings = createAsyncThunk(
  'settings/loadSettings',
  async (_, { rejectWithValue }) => {
    try {
      const settingsStr = await AsyncStorage.getItem(SETTINGS_KEY);
      return settingsStr ? JSON.parse(settingsStr) : {};
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

// 异步保存设置
const saveSettingsToStorage = async (settings) => {
  try {
    await AsyncStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
  } catch (error) {
    console.error('保存设置失败:', error);
  }
};

const settingsSlice = createSlice({
  name: 'settings',
  initialState: {
    theme: 'light', // light, dark, system
    country: 'us',
    categories: ['general', 'business', 'technology', 'sports', 'entertainment', 'health', 'science'],
    notifications: true,
    autoRefresh: true,
    refreshInterval: 300000, // 5分钟
    loading: false,
    error: null,
  },
  reducers: {
    setTheme: (state, action) => {
      state.theme = action.payload;
      saveSettingsToStorage(state);
    },
    setCountry: (state, action) => {
      state.country = action.payload;
      saveSettingsToStorage(state);
    },
    toggleCategory: (state, action) => {
      const category = action.payload;
      const index = state.categories.indexOf(category);
      if (index > -1) {
        state.categories.splice(index, 1);
      } else {
        state.categories.push(category);
      }
      saveSettingsToStorage(state);
    },
    setCategories: (state, action) => {
      state.categories = action.payload;
      saveSettingsToStorage(state);
    },
    toggleNotifications: (state) => {
      state.notifications = !state.notifications;
      saveSettingsToStorage(state);
    },
    toggleAutoRefresh: (state) => {
      state.autoRefresh = !state.autoRefresh;
      saveSettingsToStorage(state);
    },
    setRefreshInterval: (state, action) => {
      state.refreshInterval = action.payload;
      saveSettingsToStorage(state);
    },
    updateSettings: (state, action) => {
      state = { ...state, ...action.payload };
      saveSettingsToStorage(state);
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(loadSettings.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(loadSettings.fulfilled, (state, action) => {
        state.loading = false;
        state = { ...state, ...action.payload };
      })
      .addCase(loadSettings.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload;
      });
  },
});

export const { 
  setTheme, 
  setCountry, 
  toggleCategory, 
  setCategories, 
  toggleNotifications, 
  toggleAutoRefresh, 
  setRefreshInterval, 
  updateSettings 
} = settingsSlice.actions;

export default settingsSlice.reducer;

3. 创建设置页面

js
// src/screens/SettingsScreen.js
import React, { useEffect } from 'react';
import { View, Text, StyleSheet, Switch, TouchableOpacity, ScrollView, Alert } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigation } from '@react-navigation/native';
import { Ionicons } from '@expo/vector-icons';
import { 
  loadSettings, 
  setTheme, 
  setCountry, 
  toggleCategory, 
  toggleNotifications, 
  toggleAutoRefresh, 
  setRefreshInterval 
} from '../redux/slices/settingsSlice';
import { logout } from '../redux/slices/userSlice';

const categories = [
  { id: 'general', name: '综合' },
  { id: 'business', name: '商业' },
  { id: 'entertainment', name: '娱乐' },
  { id: 'health', name: '健康' },
  { id: 'science', name: '科技' },
  { id: 'sports', name: '体育' },
  { id: 'technology', name: '技术' },
];

const countries = [
  { code: 'us', name: '美国' },
  { code: 'cn', name: '中国' },
  { code: 'uk', name: '英国' },
  { code: 'jp', name: '日本' },
  { code: 'de', name: '德国' },
  { code: 'fr', name: '法国' },
];

const refreshIntervals = [
  { value: 60000, label: '1分钟' },
  { value: 300000, label: '5分钟' },
  { value: 600000, label: '10分钟' },
  { value: 1800000, label: '30分钟' },
  { value: 3600000, label: '1小时' },
];

const SettingsScreen = () => {
  const dispatch = useDispatch();
  const navigation = useNavigation();
  const { 
    theme, 
    country, 
    categories: selectedCategories, 
    notifications, 
    autoRefresh, 
    refreshInterval 
  } = useSelector(state => state.settings);
  const { isLoggedIn } = useSelector(state => state.user);

  useEffect(() => {
    dispatch(loadSettings());
  }, [dispatch]);

  const handleLogout = () => {
    Alert.alert(
      '退出登录',
      '确定要退出登录吗?',
      [
        { text: '取消', style: 'cancel' },
        {
          text: '退出',
          style: 'destructive',
          onPress: () => dispatch(logout()),
        },
      ],
      { cancelable: true }
    );
  };

  const handleThemeChange = (newTheme) => {
    dispatch(setTheme(newTheme));
  };

  const handleCountryChange = (newCountry) => {
    dispatch(setCountry(newCountry));
  };

  return (
    <ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>外观</Text>
        <View style={styles.settingItem}>
          <Text style={styles.settingLabel}>主题</Text>
          <View style={styles.themeOptions}>
            <TouchableOpacity 
              style={[styles.themeOption, theme === 'light' && styles.selectedThemeOption]}
              onPress={() => handleThemeChange('light')}
            >
              <Text style={[styles.themeText, theme === 'light' && styles.selectedThemeText]}>浅色</Text>
            </TouchableOpacity>
            <TouchableOpacity 
              style={[styles.themeOption, theme === 'dark' && styles.selectedThemeOption]}
              onPress={() => handleThemeChange('dark')}
            >
              <Text style={[styles.themeText, theme === 'dark' && styles.selectedThemeText]}>深色</Text>
            </TouchableOpacity>
            <TouchableOpacity 
              style={[styles.themeOption, theme === 'system' && styles.selectedThemeOption]}
              onPress={() => handleThemeChange('system')}
            >
              <Text style={[styles.themeText, theme === 'system' && styles.selectedThemeText]}>系统</Text>
            </TouchableOpacity>
          </View>
        </View>
      </View>

      <View style={styles.section}>
        <Text style={styles.sectionTitle}>新闻设置</Text>
        <View style={styles.settingItem}>
          <Text style={styles.settingLabel}>国家/地区</Text>
          <View style={styles.countryOptions}>
            {countries.map((item) => (
              <TouchableOpacity
                key={item.code}
                style={[styles.countryOption, country === item.code && styles.selectedCountryOption]}
                onPress={() => handleCountryChange(item.code)}
              >
                <Text style={[styles.countryText, country === item.code && styles.selectedCountryText]}>
                  {item.name}
                </Text>
              </TouchableOpacity>
            ))}
          </View>
        </View>

        <View style={styles.settingItem}>
          <Text style={styles.settingLabel}>新闻分类</Text>
          <View style={styles.categoryOptions}>
            {categories.map((category) => (
              <TouchableOpacity
                key={category.id}
                style={[
                  styles.categoryOption,
                  selectedCategories.includes(category.id) && styles.selectedCategoryOption
                ]}
                onPress={() => dispatch(toggleCategory(category.id))}
              >
                <Text style={[
                  styles.categoryText,
                  selectedCategories.includes(category.id) && styles.selectedCategoryText
                ]}
                numberOfLines={1}
              >
                {category.name}
              </Text>
            ))}
          </View>
        </View>

        <View style={styles.settingItem}>
          <Text style={styles.settingLabel}>自动刷新</Text>
          <Switch
            value={autoRefresh}
            onValueChange={toggleAutoRefresh}
            trackColor={{ false: '#E0E0E0', true: '#007AFF' }}
            thumbColor={autoRefresh ? '#FFFFFF' : '#F4F3F4'}
          />
        </View>

        {autoRefresh && (
          <View style={styles.settingItem}>
            <Text style={styles.settingLabel}>刷新间隔</Text>
            <View style={styles.intervalOptions}>
              {refreshIntervals.map((interval) => (
                <TouchableOpacity
                  key={interval.value}
                  style={[
                    styles.intervalOption,
                    refreshInterval === interval.value && styles.selectedIntervalOption
                  ]}
                  onPress={() => dispatch(setRefreshInterval(interval.value))}
                >
                  <Text style={[
                    styles.intervalText,
                    refreshInterval === interval.value && styles.selectedIntervalText
                  ]}
                >
                  {interval.label}
                </Text>
              ))}
            </View>
          </View>
        )}
      </View>

      <View style={styles.section}>
        <Text style={styles.sectionTitle}>通知</Text>
        <View style={styles.settingItem}>
          <Text style={styles.settingLabel}>推送通知</Text>
          <Switch
            value={notifications}
            onValueChange={toggleNotifications}
            trackColor={{ false: '#E0E0E0', true: '#007AFF' }}
            thumbColor={notifications ? '#FFFFFF' : '#F4F3F4'}
          />
        </View>
      </View>

      <View style={styles.section}>
        <Text style={styles.sectionTitle}>账户</Text>
        {isLoggedIn ? (
          <TouchableOpacity style={styles.settingItem} onPress={handleLogout}>
            <Text style={styles.settingLabel}>退出登录</Text>
            <Ionicons name="log-out-outline" size={20} color="#FF3B30" />
          </TouchableOpacity>
        ) : (
          <TouchableOpacity 
            style={styles.settingItem}
            onPress={() => navigation.navigate('Login')}
          >
            <Text style={styles.settingLabel}>登录/注册</Text>
            <Ionicons name="chevron-forward" size={20} color="#999" />
          </TouchableOpacity>
        )}
      </View>

      <View style={styles.section}>
        <Text style={styles.sectionTitle}>关于</Text>
        <View style={styles.settingItem}>
          <Text style={styles.settingLabel}>版本</Text>
          <Text style={styles.versionText}>1.0.0</Text>
        </View>
        <TouchableOpacity style={styles.settingItem}>
          <Text style={styles.settingLabel}>隐私政策</Text>
          <Ionicons name="chevron-forward" size={20} color="#999" />
        </TouchableOpacity>
        <TouchableOpacity style={styles.settingItem}>
          <Text style={styles.settingLabel}>用户协议</Text>
          <Ionicons name="chevron-forward" size={20} color="#999" />
        </TouchableOpacity>
      </View>
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5F5F5',
  },
  section: {
    marginTop: 20,
    backgroundColor: 'white',
    paddingHorizontal: 16,
    paddingVertical: 12,
  },
  sectionTitle: {
    fontSize: 14,
    fontWeight: '600',
    color: '#999',
    marginBottom: 12,
    textTransform: 'uppercase',
  },
  settingItem: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingVertical: 12,
    borderBottomWidth: 1,
    borderBottomColor: '#F0F0F0',
  },
  settingLabel: {
    fontSize: 16,
    color: '#333',
  },
  themeOptions: {
    flexDirection: 'row',
  },
  themeOption: {
    paddingHorizontal: 12,
    paddingVertical: 6,
    borderRadius: 16,
    backgroundColor: '#F0F0F0',
    marginLeft: 8,
  },
  selectedThemeOption: {
    backgroundColor: '#007AFF',
  },
  themeText: {
    fontSize: 14,
    color: '#333',
  },
  selectedThemeText: {
    color: 'white',
  },
  countryOptions: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    maxWidth: 200,
  },
  countryOption: {
    paddingHorizontal: 12,
    paddingVertical: 6,
    borderRadius: 16,
    backgroundColor: '#F0F0F0',
    marginLeft: 8,
    marginBottom: 8,
  },
  selectedCountryOption: {
    backgroundColor: '#007AFF',
  },
  countryText: {
    fontSize: 14,
    color: '#333',
  },
  selectedCountryText: {
    color: 'white',
  },
  categoryOptions: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    maxWidth: 200,
  },
  categoryOption: {
    paddingHorizontal: 12,
    paddingVertical: 6,
    borderRadius: 16,
    backgroundColor: '#F0F0F0',
    marginLeft: 8,
    marginBottom: 8,
  },
  selectedCategoryOption: {
    backgroundColor: '#007AFF',
  },
  categoryText: {
    fontSize: 14,
    color: '#333',
  },
  selectedCategoryText: {
    color: 'white',
  },
  intervalOptions: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    maxWidth: 200,
  },
  intervalOption: {
    paddingHorizontal: 12,
    paddingVertical: 6,
    borderRadius: 16,
    backgroundColor: '#F0F0F0',
    marginLeft: 8,
    marginBottom: 8,
  },
  selectedIntervalOption: {
    backgroundColor: '#007AFF',
  },
  intervalText: {
    fontSize: 14,
    color: '#333',
  },
  selectedIntervalText: {
    color: 'white',
  },
  versionText: {
    fontSize: 14,
    color: '#999',
  },
});

export default SettingsScreen;

4. 更新导航,添加设置页面

js
// src/navigation/AppNavigator.js
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
import HomeScreen from '../screens/HomeScreen';
import NewsDetailScreen from '../screens/NewsDetailScreen';
import FavoritesScreen from '../screens/FavoritesScreen';
import SearchScreen from '../screens/SearchScreen';
import SettingsScreen from '../screens/SettingsScreen';
import LoginScreen from '../screens/LoginScreen';

const Stack = createStackNavigator();
const Tab = createBottomTabNavigator();

const HomeStack = () => {
  return (
    <Stack.Navigator
      screenOptions={{
        headerShown: false,
      }}
    >
      <Stack.Screen name="HomeMain" component={HomeScreen} />
      <Stack.Screen name="NewsDetail" component={NewsDetailScreen} />
      <Stack.Screen name="Login" component={LoginScreen} />
    </Stack.Navigator>
  );
};

const AppNavigator = () => {
  return (
    <NavigationContainer>
      <Tab.Navigator
        screenOptions={({ route }) => ({
          headerShown: false,
          tabBarIcon: ({ focused, color, size }) => {
            let iconName;

            if (route.name === 'Home') {
              iconName = focused ? 'home' : 'home-outline';
            } else if (route.name === 'Search') {
              iconName = focused ? 'search' : 'search-outline';
            } else if (route.name === 'Favorites') {
              iconName = focused ? 'heart' : 'heart-outline';
            } else if (route.name === 'Settings') {
              iconName = focused ? 'settings' : 'settings-outline';
            }

            return <Ionicons name={iconName} size={size} color={color} />;
          },
          tabBarActiveTintColor: '#007AFF',
          tabBarInactiveTintColor: 'gray',
        })}
      >
        <Tab.Screen name="Home" component={HomeStack} options={{ title: '首页' }} />
        <Tab.Screen name="Search" component={SearchScreen} options={{ title: '搜索' }} />
        <Tab.Screen name="Favorites" component={FavoritesScreen} options={{ title: '收藏' }} />
        <Tab.Screen name="Settings" component={SettingsScreen} options={{ title: '设置' }} />
      </Tab.Navigator>
    </NavigationContainer>
  );
};

export default AppNavigator;

5. 创建登录页面

js
// src/screens/LoginScreen.js
import React, { useState } from 'react';
import { View, Text, StyleSheet, TextInput, TouchableOpacity, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
import { useDispatch } from 'react-redux';
import { useNavigation } from '@react-navigation/native';
import { login } from '../redux/slices/userSlice';

const LoginScreen = () => {
  const dispatch = useDispatch();
  const navigation = useNavigation();
  const [isLogin, setIsLogin] = useState(true);
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [name, setName] = useState('');
  const [loading, setLoading] = useState(false);

  const handleSubmit = () => {
    setLoading(true);
    // 模拟登录/注册
    setTimeout(() => {
      const userInfo = {
        id: '1',
        name: name || '用户',
        email: email,
        avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
      };
      dispatch(login(userInfo));
      navigation.goBack();
      setLoading(false);
    }, 1000);
  };

  return (
    <KeyboardAvoidingView 
      style={styles.container}
      behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
    >
      <ScrollView 
        contentContainerStyle={styles.scrollContent}
        showsVerticalScrollIndicator={false}
      >
        <View style={styles.header}>
          <Text style={styles.title}>{isLogin ? '登录' : '注册'}</Text>
          <Text style={styles.subtitle}>
            {isLogin ? '欢迎回来' : '创建新账户'}
          </Text>
        </View>

        <View style={styles.form}>
          {!isLogin && (
            <View style={styles.inputContainer}>
              <Text style={styles.inputLabel}>姓名</Text>
              <TextInput
                style={styles.input}
                placeholder="请输入姓名"
                placeholderTextColor="#999"
                value={name}
                onChangeText={setName}
              />
            </View>
          )}

          <View style={styles.inputContainer}>
            <Text style={styles.inputLabel}>邮箱</Text>
            <TextInput
              style={styles.input}
              placeholder="请输入邮箱"
              placeholderTextColor="#999"
              value={email}
              onChangeText={setEmail}
              keyboardType="email-address"
              autoCapitalize="none"
            />
          </View>

          <View style={styles.inputContainer}>
            <Text style={styles.inputLabel}>密码</Text>
            <TextInput
              style={styles.input}
              placeholder="请输入密码"
              placeholderTextColor="#999"
              value={password}
              onChangeText={setPassword}
              secureTextEntry
            />
          </View>

          <TouchableOpacity 
            style={styles.submitButton}
            onPress={handleSubmit}
            disabled={loading}
          >
            <Text style={styles.submitButtonText}>
              {loading ? '处理中...' : isLogin ? '登录' : '注册'}
            </Text>
          </TouchableOpacity>

          <View style={styles.switchContainer}>
            <Text style={styles.switchText}>
              {isLogin ? '还没有账户?' : '已有账户?'}
            </Text>
            <TouchableOpacity onPress={() => setIsLogin(!isLogin)}>
              <Text style={styles.switchLink}>
                {isLogin ? '立即注册' : '立即登录'}
              </Text>
            </TouchableOpacity>
          </View>
        </View>
      </ScrollView>
    </KeyboardAvoidingView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'white',
  },
  scrollContent: {
    flexGrow: 1,
    paddingHorizontal: 24,
    paddingVertical: 32,
  },
  header: {
    marginTop: 40,
    marginBottom: 40,
  },
  title: {
    fontSize: 32,
    fontWeight: 'bold',
    color: '#333',
    marginBottom: 8,
  },
  subtitle: {
    fontSize: 16,
    color: '#999',
  },
  form: {
    flex: 1,
  },
  inputContainer: {
    marginBottom: 20,
  },
  inputLabel: {
    fontSize: 14,
    fontWeight: '500',
    color: '#333',
    marginBottom: 8,
  },
  input: {
    borderWidth: 1,
    borderColor: '#E0E0E0',
    borderRadius: 8,
    paddingHorizontal: 16,
    paddingVertical: 12,
    fontSize: 16,
    color: '#333',
  },
  submitButton: {
    backgroundColor: '#007AFF',
    borderRadius: 8,
    paddingVertical: 14,
    alignItems: 'center',
    marginTop: 24,
  },
  submitButtonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '600',
  },
  switchContainer: {
    flexDirection: 'row',
    justifyContent: 'center',
    marginTop: 24,
  },
  switchText: {
    fontSize: 14,
    color: '#999',
  },
  switchLink: {
    fontSize: 14,
    color: '#007AFF',
    marginLeft: 4,
  },
});

export default LoginScreen;

6. 实现主题切换

6.1 创建主题上下文

js
// src/contexts/ThemeContext.js
import React, { createContext, useState, useContext, useEffect } from 'react';
import { useColorScheme } from 'react-native';
import { useSelector } from 'react-redux';

const ThemeContext = createContext();

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

export const ThemeProvider = ({ children }) => {
  const systemColorScheme = useColorScheme();
  const { theme: themeSetting } = useSelector(state => state.settings);
  
  const [isDarkMode, setIsDarkMode] = useState(
    themeSetting === 'system' ? systemColorScheme === 'dark' : themeSetting === 'dark'
  );

  useEffect(() => {
    if (themeSetting === 'system') {
      setIsDarkMode(systemColorScheme === 'dark');
    } else {
      setIsDarkMode(themeSetting === 'dark');
    }
  }, [themeSetting, systemColorScheme]);

  const theme = {
    colors: {
      primary: '#4F46E5',
      secondary: '#10B981',
      background: isDarkMode ? '#121212' : '#F3F4F6',
      surface: isDarkMode ? '#1E1E1E' : '#FFFFFF',
      text: isDarkMode ? '#FFFFFF' : '#1F2937',
      textSecondary: isDarkMode ? '#A0AEC0' : '#6B7280',
      border: isDarkMode ? '#2D3748' : '#E5E7EB',
      error: '#EF4444',
      success: '#10B981',
      warning: '#F59E0B',
    },
    spacing: {
      xs: 4,
      sm: 8,
      md: 16,
      lg: 24,
      xl: 32,
    },
    borderRadius: {
      sm: 4,
      md: 8,
      lg: 12,
      xl: 16,
    },
    shadows: {
      sm: {
        shadowColor: '#000',
        shadowOffset: {
          width: 0,
          height: 1,
        },
        shadowOpacity: 0.05,
        shadowRadius: 2,
        elevation: 2,
      },
      md: {
        shadowColor: '#000',
        shadowOffset: {
          width: 0,
          height: 2,
        },
        shadowOpacity: 0.1,
        shadowRadius: 4,
        elevation: 4,
      },
    },
  };

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

6.2 更新 App.js,添加主题提供者

js
// App.js
import React, { useEffect } from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Provider, useDispatch } from 'react-redux';
import { StatusBar } from 'expo-status-bar';
import { store } from './src/redux/store';
import AppNavigator from './src/navigation/AppNavigator';
import { ThemeProvider, useTheme } from './src/contexts/ThemeContext';
import { loadSettings } from './src/redux/slices/settingsSlice';
import { loadFavorites } from './src/redux/slices/favoritesSlice';
import { loadUser } from './src/redux/slices/userSlice';

// 初始化应用数据
const AppInitializer = () => {
  const dispatch = useDispatch();
  
  useEffect(() => {
    // 加载设置
    dispatch(loadSettings());
    // 加载收藏
    dispatch(loadFavorites());
    // 加载用户信息
    dispatch(loadUser());
  }, [dispatch]);

  return null;
};

// 主题化的应用组件
const ThemedApp = () => {
  const { isDarkMode } = useTheme();
  
  return (
    <>
      <StatusBar style={isDarkMode ? 'light' : 'dark'} />
      <AppNavigator />
      <AppInitializer />
    </>
  );
};

export default function App() {
  return (
    <Provider store={store}>
      <SafeAreaProvider>
        <ThemeProvider>
          <ThemedApp />
        </ThemeProvider>
      </SafeAreaProvider>
    </Provider>
  );
}

7. 优化性能

7.1 使用 useSelector 的缓存功能

js
// src/components/NewsList.js
import { useSelector } from 'react-redux';

// 优化前
const { headlines, loading, error } = useSelector(state => state.news);

// 优化后
const headlines = useSelector(state => state.news.headlines);
const loading = useSelector(state => state.news.loading);
const error = useSelector(state => state.news.error);

7.2 使用 useCallback 和 useMemo

js
// src/components/NewsList.js
import { useCallback, useMemo } from 'react';

// 使用 useCallback 缓存回调函数
const handleCategoryChange = useCallback((categoryId) => {
  dispatch(setCategory(categoryId));
}, [dispatch]);

// 使用 useMemo 缓存计算结果
const filteredHeadlines = useMemo(() => {
  return headlines.filter(item => item.title.includes(searchQuery));
}, [headlines, searchQuery]);

7.3 使用 createEntityAdapter 管理规范化数据

js
// src/redux/slices/newsSlice.js
import { createEntityAdapter } from '@reduxjs/toolkit';

const newsAdapter = createEntityAdapter({
  selectId: (news) => news.url,
  sortComparer: (a, b) => new Date(b.publishedAt) - new Date(a.publishedAt),
});

const initialState = newsAdapter.getInitialState({
  loading: false,
  error: null,
  selectedCategory: 'general',
  selectedCountry: 'us',
});

const newsSlice = createSlice({
  name: 'news',
  initialState,
  reducers: {
    // 其他 reducers
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchTopHeadlines.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchTopHeadlines.fulfilled, (state, action) => {
        state.loading = false;
        newsAdapter.setAll(state, action.payload);
      })
      .addCase(fetchTopHeadlines.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload;
      });
  },
});

// 导出选择器
export const {
  selectAll: selectAllNews,
  selectById: selectNewsById,
  selectIds: selectNewsIds,
} = newsAdapter.getSelectors((state) => state.news);

8. 总结

通过本章节的学习,我们掌握了如何使用 Redux Toolkit 实现全局状态管理,包括:

  1. 如何优化全局状态结构
  2. 如何创建用户状态管理
  3. 如何创建设置状态管理
  4. 如何实现主题切换
  5. 如何优化性能

这些技能将帮助我们构建更加健壮、可维护的 React Native 应用。

9. 下一步

接下来,我们将学习如何打包和发布应用到应用商店,完成整个开发流程。

© 2026 编程马·菜鸟教程 版权所有