Appearance
项目1:简易待办清单(Todo App)- 本地数据存储
1. 本地数据存储概述
在 React Native 应用中,本地数据存储是一个重要的功能,它允许应用在设备上持久化存储数据,即使应用被关闭或重启后,数据仍然保留。对于待办清单应用来说,本地数据存储尤为重要,因为用户希望他们的待办事项能够被保存,而不是每次打开应用都重新开始。
在本章节中,我们将:
- 完善本地数据存储功能
- 优化存储结构
- 添加数据备份和恢复功能
- 处理存储异常
2. 存储方案选择
React Native 提供了多种本地存储方案,我们可以根据应用的需求选择合适的方案:
| 存储方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| AsyncStorage | 简单的键值对存储 | 使用简单,API 友好 | 存储容量有限,不适合大量数据 |
| Realm | 复杂数据结构 | 高性能,支持复杂查询 | 集成复杂,包大小增加 |
| SQLite | 关系型数据 | 支持 SQL 查询,适合结构化数据 | 学习成本较高,需要管理数据库 |
| 文件系统 | 大文件存储 | 适合存储图片、视频等大文件 | 操作复杂,需要管理文件路径 |
对于待办清单应用,我们选择使用 AsyncStorage,因为:
- 待办事项数据结构简单,适合键值对存储
- 存储量通常不大,AsyncStorage 足够满足需求
- API 简单易用,开发效率高
- 跨平台兼容,无需针对不同平台编写不同代码
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-picker4.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-icons6. 错误处理和异常情况
为了提高应用的稳定性,我们需要处理可能出现的异常情况:
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测试步骤:
- 添加一些待办事项
- 关闭应用并重新打开,检查待办事项是否保存
- 进入设置页面,测试备份和恢复功能
- 测试清除数据功能
9. 总结
在本章节中,我们完善了待办清单应用的本地数据存储功能,包括:
- 增强了存储工具,添加了设置存储、数据导出导入、存储信息获取等功能
- 实现了数据备份和恢复功能,允许用户将数据导出为文件或从文件导入
- 添加了错误处理和异常情况处理,提高应用的稳定性
- 优化了存储性能,添加了批量操作和缓存机制
这些功能不仅提高了应用的用户体验,也为用户的数据安全提供了保障。在接下来的章节中,我们将美化应用的用户界面,使应用更加美观和易用。
