Skip to content

项目1:简易待办清单(Todo App)- 本地数据存储

1. 本地数据存储概述

在 React Native 应用中,本地数据存储是一个重要的功能,它允许应用在设备上持久化存储数据,即使应用被关闭或重启后,数据仍然保留。对于待办清单应用来说,本地数据存储尤为重要,因为用户希望他们的待办事项能够被保存,而不是每次打开应用都重新开始。

在本章节中,我们将:

  • 完善本地数据存储功能
  • 优化存储结构
  • 添加数据备份和恢复功能
  • 处理存储异常

2. 存储方案选择

React Native 提供了多种本地存储方案,我们可以根据应用的需求选择合适的方案:

存储方案适用场景优点缺点
AsyncStorage简单的键值对存储使用简单,API 友好存储容量有限,不适合大量数据
Realm复杂数据结构高性能,支持复杂查询集成复杂,包大小增加
SQLite关系型数据支持 SQL 查询,适合结构化数据学习成本较高,需要管理数据库
文件系统大文件存储适合存储图片、视频等大文件操作复杂,需要管理文件路径

对于待办清单应用,我们选择使用 AsyncStorage,因为:

  1. 待办事项数据结构简单,适合键值对存储
  2. 存储量通常不大,AsyncStorage 足够满足需求
  3. API 简单易用,开发效率高
  4. 跨平台兼容,无需针对不同平台编写不同代码

3. 完善存储工具

我们已经在项目结构搭建中创建了基本的存储工具,现在将进一步完善它:

javascript
// utils/storage.js
import AsyncStorage from '@react-native-async-storage/async-storage';

const STORAGE_KEY = '@TodoApp:todos';
const SETTINGS_KEY = '@TodoApp:settings';

export const storage = {
  // 存储待办事项
  saveTodos: async (todos) => {
    try {
      const jsonValue = JSON.stringify(todos);
      await AsyncStorage.setItem(STORAGE_KEY, jsonValue);
      return true;
    } catch (error) {
      console.error('保存待办事项失败:', error);
      return false;
    }
  },

  // 获取待办事项
  getTodos: async () => {
    try {
      const jsonValue = await AsyncStorage.getItem(STORAGE_KEY);
      return jsonValue != null ? JSON.parse(jsonValue) : [];
    } catch (error) {
      console.error('获取待办事项失败:', error);
      return [];
    }
  },

  // 清除待办事项
  clearTodos: async () => {
    try {
      await AsyncStorage.removeItem(STORAGE_KEY);
      return true;
    } catch (error) {
      console.error('清除待办事项失败:', error);
      return false;
    }
  },

  // 存储应用设置
  saveSettings: async (settings) => {
    try {
      const jsonValue = JSON.stringify(settings);
      await AsyncStorage.setItem(SETTINGS_KEY, jsonValue);
      return true;
    } catch (error) {
      console.error('保存设置失败:', error);
      return false;
    }
  },

  // 获取应用设置
  getSettings: async () => {
    try {
      const jsonValue = await AsyncStorage.getItem(SETTINGS_KEY);
      return jsonValue != null ? JSON.parse(jsonValue) : {
        theme: 'light',
        autoSync: false,
        notifications: true
      };
    } catch (error) {
      console.error('获取设置失败:', error);
      return {
        theme: 'light',
        autoSync: false,
        notifications: true
      };
    }
  },

  // 导出数据
  exportData: async () => {
    try {
      const todos = await storage.getTodos();
      const settings = await storage.getSettings();
      return {
        todos,
        settings,
        exportDate: new Date().toISOString()
      };
    } catch (error) {
      console.error('导出数据失败:', error);
      return null;
    }
  },

  // 导入数据
  importData: async (data) => {
    try {
      if (data.todos) {
        await storage.saveTodos(data.todos);
      }
      if (data.settings) {
        await storage.saveSettings(data.settings);
      }
      return true;
    } catch (error) {
      console.error('导入数据失败:', error);
      return false;
    }
  },

  // 清除所有数据
  clearAll: async () => {
    try {
      await AsyncStorage.multiRemove([STORAGE_KEY, SETTINGS_KEY]);
      return true;
    } catch (error) {
      console.error('清除所有数据失败:', error);
      return false;
    }
  },

  // 获取存储使用情况
  getStorageInfo: async () => {
    try {
      const keys = await AsyncStorage.getAllKeys();
      let totalSize = 0;
      
      for (const key of keys) {
        const value = await AsyncStorage.getItem(key);
        if (value) {
          totalSize += value.length;
        }
      }
      
      return {
        keyCount: keys.length,
        totalSize: totalSize,
        totalSizeFormatted: `${(totalSize / 1024).toFixed(2)} KB`
      };
    } catch (error) {
      console.error('获取存储信息失败:', error);
      return null;
    }
  }
};

4. 实现数据备份和恢复功能

为了防止数据丢失,我们可以添加数据备份和恢复功能,允许用户将待办事项导出为文件或从文件导入:

4.1 安装必要的依赖

bash
# 安装文件系统和分享相关的依赖
npx expo install expo-file-system expo-sharing expo-document-picker

4.2 创建数据备份和恢复工具

javascript
// utils/backup.js
import * as FileSystem from 'expo-file-system';
import * as Sharing from 'expo-sharing';
import * as DocumentPicker from 'expo-document-picker';
import { storage } from './storage';

export const backup = {
  // 备份数据到文件
  backupToFile: async () => {
    try {
      // 导出数据
      const data = await storage.exportData();
      if (!data) {
        throw new Error('导出数据失败');
      }

      // 创建备份文件
      const backupContent = JSON.stringify(data, null, 2);
      const fileName = `todo-backup-${new Date().toISOString().split('T')[0]}.json`;
      const fileUri = `${FileSystem.documentDirectory}${fileName}`;

      // 写入文件
      await FileSystem.writeAsStringAsync(fileUri, backupContent);

      // 分享文件
      if (await Sharing.isAvailableAsync()) {
        await Sharing.shareAsync(fileUri);
        return true;
      } else {
        throw new Error('设备不支持分享功能');
      }
    } catch (error) {
      console.error('备份失败:', error);
      return false;
    }
  },

  // 从文件恢复数据
  restoreFromFile: async () => {
    try {
      // 选择文件
      const result = await DocumentPicker.getDocumentAsync({
        type: 'application/json',
        copyToCacheDirectory: true
      });

      if (result.canceled) {
        return false;
      }

      // 读取文件内容
      const fileUri = result.assets[0].uri;
      const backupContent = await FileSystem.readAsStringAsync(fileUri);
      const data = JSON.parse(backupContent);

      // 导入数据
      const success = await storage.importData(data);
      return success;
    } catch (error) {
      console.error('恢复失败:', error);
      return false;
    }
  }
};

5. 集成到应用中

现在,我们将把这些功能集成到待办清单应用中:

5.1 添加设置页面

javascript
// screens/SettingsScreen.js
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Alert, Switch } from 'react-native';
import { storage } from '../utils/storage';
import { backup } from '../utils/backup';
import { globalStyles } from '../styles/global';

const SettingsScreen = ({ navigation }) => {
  const [settings, setSettings] = useState({
    theme: 'light',
    autoSync: false,
    notifications: true
  });
  const [storageInfo, setStorageInfo] = useState(null);

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

  const loadSettings = async () => {
    const savedSettings = await storage.getSettings();
    setSettings(savedSettings);
  };

  const loadStorageInfo = async () => {
    const info = await storage.getStorageInfo();
    setStorageInfo(info);
  };

  const handleSettingChange = async (key, value) => {
    const newSettings = { ...settings, [key]: value };
    setSettings(newSettings);
    await storage.saveSettings(newSettings);
  };

  const handleBackup = async () => {
    const success = await backup.backupToFile();
    if (success) {
      Alert.alert('成功', '数据备份成功');
    } else {
      Alert.alert('失败', '数据备份失败');
    }
  };

  const handleRestore = async () => {
    Alert.alert(
      '确认恢复',
      '恢复数据将覆盖当前所有待办事项,确定要继续吗?',
      [
        {
          text: '取消',
          style: 'cancel',
        },
        {
          text: '确定',
          onPress: async () => {
            const success = await backup.restoreFromFile();
            if (success) {
              Alert.alert('成功', '数据恢复成功,请重启应用');
            } else {
              Alert.alert('失败', '数据恢复失败');
            }
          },
        },
      ],
      { cancelable: true }
    );
  };

  const handleClearAll = () => {
    Alert.alert(
      '确认清除',
      '确定要清除所有数据吗?此操作不可恢复。',
      [
        {
          text: '取消',
          style: 'cancel',
        },
        {
          text: '清除',
          style: 'destructive',
          onPress: async () => {
            const success = await storage.clearAll();
            if (success) {
              Alert.alert('成功', '所有数据已清除');
              loadStorageInfo();
            } else {
              Alert.alert('失败', '清除数据失败');
            }
          },
        },
      ],
      { cancelable: true }
    );
  };

  return (
    <View style={globalStyles.container}>
      <Text style={globalStyles.title}>设置</Text>

      <View style={styles.section}>
        <Text style={styles.sectionTitle}>存储信息</Text>
        {storageInfo && (
          <View style={styles.infoContainer}>
            <Text style={styles.infoText}>存储键数量: {storageInfo.keyCount}</Text>
            <Text style={styles.infoText}>总大小: {storageInfo.totalSizeFormatted}</Text>
          </View>
        )}
      </View>

      <View style={styles.section}>
        <Text style={styles.sectionTitle}>应用设置</Text>
        
        <View style={styles.settingItem}>
          <Text style={styles.settingLabel}>深色模式</Text>
          <Switch
            value={settings.theme === 'dark'}
            onValueChange={(value) => handleSettingChange('theme', value ? 'dark' : 'light')}
          />
        </View>

        <View style={styles.settingItem}>
          <Text style={styles.settingLabel}>自动同步</Text>
          <Switch
            value={settings.autoSync}
            onValueChange={(value) => handleSettingChange('autoSync', value)}
          />
        </View>

        <View style={styles.settingItem}>
          <Text style={styles.settingLabel}>通知提醒</Text>
          <Switch
            value={settings.notifications}
            onValueChange={(value) => handleSettingChange('notifications', value)}
          />
        </View>
      </View>

      <View style={styles.section}>
        <Text style={styles.sectionTitle}>数据管理</Text>
        
        <TouchableOpacity style={styles.button} onPress={handleBackup}>
          <Text style={styles.buttonText}>备份数据</Text>
        </TouchableOpacity>

        <TouchableOpacity style={styles.button} onPress={handleRestore}>
          <Text style={styles.buttonText}>恢复数据</Text>
        </TouchableOpacity>

        <TouchableOpacity style={[styles.button, styles.dangerButton]} onPress={handleClearAll}>
          <Text style={[styles.buttonText, styles.dangerText]}>清除所有数据</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  section: {
    backgroundColor: '#fff',
    borderRadius: 8,
    padding: 15,
    marginBottom: 20,
    shadowColor: '#000',
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.1,
    shadowRadius: 3.84,
    elevation: 5,
  },
  sectionTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    marginBottom: 15,
  },
  infoContainer: {
    backgroundColor: '#f5f5f5',
    padding: 10,
    borderRadius: 5,
  },
  infoText: {
    fontSize: 14,
    marginBottom: 5,
  },
  settingItem: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingVertical: 10,
    borderBottomWidth: 1,
    borderBottomColor: '#f0f0f0',
  },
  settingLabel: {
    fontSize: 16,
  },
  button: {
    backgroundColor: '#007AFF',
    padding: 12,
    borderRadius: 5,
    alignItems: 'center',
    marginBottom: 10,
  },
  buttonText: {
    color: '#fff',
    fontWeight: 'bold',
  },
  dangerButton: {
    backgroundColor: '#ff3b30',
  },
  dangerText: {
    color: '#fff',
  },
});

export default SettingsScreen;

5.2 更新导航配置

javascript
// App.js
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
import TodoListScreen from './screens/TodoListScreen';
import SettingsScreen from './screens/SettingsScreen';

const Tab = createBottomTabNavigator();

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

            if (route.name === 'TodoList') {
              iconName = focused ? 'list' : 'list-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="TodoList" component={TodoListScreen} options={{ title: '待办清单' }} />
        <Tab.Screen name="Settings" component={SettingsScreen} options={{ title: '设置' }} />
      </Tab.Navigator>
    </NavigationContainer>
  );
};

export default App;

5.3 安装导航依赖

bash
# 安装导航相关依赖
npx expo install @react-navigation/native @react-navigation/bottom-tabs react-native-screens react-native-safe-area-context react-native-gesture-handler

# 安装图标库
npx expo install @expo/vector-icons

6. 错误处理和异常情况

为了提高应用的稳定性,我们需要处理可能出现的异常情况:

6.1 存储异常处理

javascript
// utils/storage.js - 增强错误处理
import AsyncStorage from '@react-native-async-storage/async-storage';

const STORAGE_KEY = '@TodoApp:todos';
const SETTINGS_KEY = '@TodoApp:settings';

// 重试机制
const withRetry = async (fn, retries = 3, delay = 1000) => {
  try {
    return await fn();
  } catch (error) {
    if (retries > 0) {
      console.warn(`操作失败,${retries} 秒后重试:`, error);
      await new Promise(resolve => setTimeout(resolve, delay));
      return withRetry(fn, retries - 1, delay);
    }
    throw error;
  }
};

export const storage = {
  // 存储待办事项
  saveTodos: async (todos) => {
    try {
      return await withRetry(async () => {
        const jsonValue = JSON.stringify(todos);
        await AsyncStorage.setItem(STORAGE_KEY, jsonValue);
        return true;
      });
    } catch (error) {
      console.error('保存待办事项失败:', error);
      // 可以在这里添加错误上报
      return false;
    }
  },

  // 其他方法类似...
};

6.2 数据验证

javascript
// utils/validator.js
// 数据验证工具
export const validator = {
  // 验证待办事项数据
  validateTodos: (todos) => {
    if (!Array.isArray(todos)) {
      return false;
    }
    
    return todos.every(todo => {
      return (
        typeof todo === 'object' &&
        todo !== null &&
        typeof todo.id === 'string' &&
        typeof todo.text === 'string' &&
        typeof todo.completed === 'boolean'
      );
    });
  },

  // 验证设置数据
  validateSettings: (settings) => {
    return (
      typeof settings === 'object' &&
      settings !== null &&
      (settings.theme === 'light' || settings.theme === 'dark') &&
      typeof settings.autoSync === 'boolean' &&
      typeof settings.notifications === 'boolean'
    );
  },

  // 验证备份数据
  validateBackupData: (data) => {
    return (
      typeof data === 'object' &&
      data !== null &&
      (data.todos === undefined || validator.validateTodos(data.todos)) &&
      (data.settings === undefined || validator.validateSettings(data.settings)) &&
      typeof data.exportDate === 'string'
    );
  }
};

// 在 storage.js 中使用
import { validator } from './validator';

export const storage = {
  // 导入数据
  importData: async (data) => {
    try {
      if (!validator.validateBackupData(data)) {
        throw new Error('无效的备份数据');
      }
      
      if (data.todos) {
        await storage.saveTodos(data.todos);
      }
      if (data.settings) {
        await storage.saveSettings(data.settings);
      }
      return true;
    } catch (error) {
      console.error('导入数据失败:', error);
      return false;
    }
  },
};

7. 性能优化

7.1 批量操作

对于多个存储操作,我们可以使用批量操作来提高性能:

javascript
// 批量保存数据
const batchSave = async (todos, settings) => {
  try {
    const todoValue = JSON.stringify(todos);
    const settingsValue = JSON.stringify(settings);
    
    await AsyncStorage.multiSet([
      [STORAGE_KEY, todoValue],
      [SETTINGS_KEY, settingsValue]
    ]);
    return true;
  } catch (error) {
    console.error('批量保存失败:', error);
    return false;
  }
};

7.2 缓存机制

对于频繁访问的数据,我们可以添加内存缓存:

javascript
// utils/storage.js - 添加缓存
import AsyncStorage from '@react-native-async-storage/async-storage';

const STORAGE_KEY = '@TodoApp:todos';
const SETTINGS_KEY = '@TodoApp:settings';

// 内存缓存
let cache = {
  todos: null,
  settings: null,
  lastUpdated: {
    todos: 0,
    settings: 0
  }
};

// 缓存有效期(毫秒)
const CACHE_TTL = 5 * 60 * 1000; // 5分钟

export const storage = {
  // 存储待办事项
  saveTodos: async (todos) => {
    try {
      const jsonValue = JSON.stringify(todos);
      await AsyncStorage.setItem(STORAGE_KEY, jsonValue);
      // 更新缓存
      cache.todos = todos;
      cache.lastUpdated.todos = Date.now();
      return true;
    } catch (error) {
      console.error('保存待办事项失败:', error);
      return false;
    }
  },

  // 获取待办事项
  getTodos: async () => {
    try {
      // 检查缓存是否有效
      const now = Date.now();
      if (cache.todos && (now - cache.lastUpdated.todos) < CACHE_TTL) {
        return cache.todos;
      }
      
      const jsonValue = await AsyncStorage.getItem(STORAGE_KEY);
      const todos = jsonValue != null ? JSON.parse(jsonValue) : [];
      // 更新缓存
      cache.todos = todos;
      cache.lastUpdated.todos = now;
      return todos;
    } catch (error) {
      console.error('获取待办事项失败:', error);
      // 缓存失败时返回缓存数据
      return cache.todos || [];
    }
  },

  // 其他方法类似...
};

8. 测试存储功能

执行以下命令运行项目,测试本地数据存储功能:

bash
# 启动开发服务器
npx expo start

# 在 iOS 模拟器中运行
# 按 i

# 在 Android 模拟器中运行
# 按 a

# 在网页中运行
# 按 w

测试步骤:

  1. 添加一些待办事项
  2. 关闭应用并重新打开,检查待办事项是否保存
  3. 进入设置页面,测试备份和恢复功能
  4. 测试清除数据功能

9. 总结

在本章节中,我们完善了待办清单应用的本地数据存储功能,包括:

  1. 增强了存储工具,添加了设置存储、数据导出导入、存储信息获取等功能
  2. 实现了数据备份和恢复功能,允许用户将数据导出为文件或从文件导入
  3. 添加了错误处理和异常情况处理,提高应用的稳定性
  4. 优化了存储性能,添加了批量操作和缓存机制

这些功能不仅提高了应用的用户体验,也为用户的数据安全提供了保障。在接下来的章节中,我们将美化应用的用户界面,使应用更加美观和易用。

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