Expo Go中AsyncStorage存储计时器值后无法正确读取求助
问题分析与修复方案
你的秒表应用在重启后无法正确恢复计时数据,核心问题出在异步存储的处理逻辑和计时器初始化逻辑上,以下是具体问题和修复步骤:
关键问题点
saveAsync未等待AsyncStorage.setItem完成,可能导致数据未成功写入就退出应用- 计时器间隔逻辑错误:每次触发间隔时重复累加
retrievedTime,而非仅用它初始化一次计时 - 恢复数据时错误地将
retrievedTime加到当前time上,而非直接替换为保存的值 - 未恢复
isRunning状态,导致应用重启后无法自动继续计时 displayTime函数传入了未使用的参数,依赖组件状态却多此一举
修复步骤
1. 修复异步存储的 await 逻辑
AsyncStorage.setItem 是异步操作,必须等待它完成才能确保数据写入:
const saveAsync = async (key, value) => { await AsyncStorage.setItem(key, JSON.stringify(value)); // 添加 await console.log(key+" value saved: ", value); };
2. 简化计时器间隔逻辑
移除间隔内的 retrievedTime 判断,因为我们会在初始化时直接将 time 设置为保存的值:
useEffect(() => { if (isRunning) { console.log("it is running"); intervalRef.current = setInterval(() => { // 仅正常累加计时 setTime(previousTime => previousTime + 100); }, 100); } else { clearInterval(intervalRef.current); console.log("it stopped"); } return () => clearInterval(intervalRef.current); }, [isRunning]);
3. 正确恢复计时数据
将 time 直接设置为保存的 retrievedTime,而非累加:
useEffect(() => { setTime(retrievedTime); }, [retrievedTime]);
4. 恢复运行状态
在初始化时同时读取 isRunning 状态,确保重启后能继续计时:
const retrieveAsync = async (key) => { try { const jsonValue = await AsyncStorage.getItem(key); if (jsonValue !== null) { const parsedValue = JSON.parse(jsonValue); console.log("parsedValue: ", parsedValue); if(key === 'time') { setRetrievedTime(parsedValue); } else if(key === 'isRunning') { // 新增处理 isRunning setIsRunning(parsedValue); } } } catch (e) { console.log('error: ', e); } }; // 初始化时同时读取两个状态 useEffect(() => { retrieveAsync('time'); retrieveAsync('isRunning'); }, []);
5. 修复 displayTime 函数
移除未使用的参数,直接使用组件状态:
const displayTime = () => { const milliseconds = parseInt((time%1000)/10); const centiseconds = Math.floor(milliseconds/10); const seconds = parseInt((time/1000)%60); const minutes = parseInt((time/(1000*60))%60) const hours = parseInt((time/(1000*60*60))%24); return { hours: pad(hours), minutesSeconds: pad(minutes % 60) + ':' + pad(seconds % 60), centiseconds: centiseconds, }; }; // JSX 中调用时无需传参 <Text style={[styles.hoursTimerOverlay, styles.textFont]}>{displayTime().hours}</Text> <Text style={[styles.timerOverlay, styles.textFont]}>{displayTime().minutesSeconds}</Text> <Text style={[styles.centisecondsTimerOverlay, styles.textFont]}>{displayTime().centiseconds}</Text>
完整修复后的代码
import React, { useState, useRef, useEffect, useCallback } from 'react'; import { View, Text, TouchableOpacity, StyleSheet, Image, Modal, Platform } from 'react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; export default function StopwatchScreen() { const [menuVisible, setMenuVisible] = useState(false); const [backgroundColor, setBackgroundColor] = useState('#11167F'); const [headerBackgroundColor, setHeaderBackgroundColor] = useState('#0a1055'); const [isRunning, setIsRunning] = useState(false); const [time, setTime] = useState(0); const [retrievedTime, setRetrievedTime] = useState(0); const intervalRef = useRef(null); const handleLapButtonPress = useCallback(() => { if(isRunning) { } else { setTime(0); setRetrievedTime(0); saveAsync('time', 0); console.log("cleared time: ", time); console.log("cleared retrieved time: ", retrievedTime); } }, [isRunning]); const handleStartButtonPress = async () => { setIsRunning(!isRunning); saveAsync('isRunning', !isRunning); saveAsync('time', time); }; useEffect(() => { if (isRunning) { console.log("it is running"); intervalRef.current = setInterval(() => { setTime(previousTime => previousTime + 100); }, 100); } else { clearInterval(intervalRef.current); console.log("it stopped"); } return () => clearInterval(intervalRef.current); }, [isRunning]); useEffect(() => { saveAsync('time', time); }, [time]); const toggleMenu = () => { setMenuVisible(!menuVisible); } const displayTime = () => { const milliseconds = parseInt((time%1000)/10); const centiseconds = Math.floor(milliseconds/10); const seconds = parseInt((time/1000)%60); const minutes = parseInt((time/(1000*60))%60) const hours = parseInt((time/(1000*60*60))%24); return { hours: pad(hours), minutesSeconds: pad(minutes % 60) + ':' + pad(seconds % 60), centiseconds: centiseconds, }; }; const pad = (number) => { return number < 10 ? '0' + number : number; }; const retrieveAsync = async (key) => { try { const jsonValue = await AsyncStorage.getItem(key); if (jsonValue !== null) { const parsedValue = JSON.parse(jsonValue); console.log("parsedValue: ", parsedValue); if(key === 'time') { setRetrievedTime(parsedValue); } else if(key === 'isRunning') { setIsRunning(parsedValue); } } } catch (e) { console.log('error: ', e); } }; const saveAsync = async (key, value) => { await AsyncStorage.setItem(key, JSON.stringify(value)); console.log(key+" value saved: ", value); }; useEffect(() => { setTime(retrievedTime); }, [retrievedTime]); useEffect(() => { retrieveAsync('time'); retrieveAsync('isRunning'); }, []); return ( <View style={[styles.container, Platform.OS === 'android' && styles.androidPadding]}> <View style={[styles.header, {backgroundColor: headerBackgroundColor}]}> <Text style={[styles.title, styles.textFont]}>Futuristic Stopwatch</Text> <TouchableOpacity onPress={toggleMenu}> <Image source={require('../assets/images/three_dots.png')} style={styles.dotsIcon} /> </TouchableOpacity> </View> <Modal transparent={false} animationType="slide" visible={menuVisible} onRequestClose={() => setMenuVisible(false)} > <View style={styles.modalContainer}> Modal </View> </Modal> <View style={[styles.contentContainer, {backgroundColor: backgroundColor}]}> <View style={styles.rowHud}> <Text style={[styles.hoursTimerOverlay, styles.textFont]}>{displayTime().hours}</Text> <Text style={[styles.timerOverlay, styles.textFont]}>{displayTime().minutesSeconds}</Text> <Text style={[styles.centisecondsTimerOverlay, styles.textFont]}>{displayTime().centiseconds}</Text> </View> <View style={styles.controls}> <TouchableOpacity onPress={handleStartButtonPress} style={[styles.controlButtonBorder, { backgroundColor: isRunning ? "#340e0d" : "#0a2a12" }]}> <View style={styles.controlButton}> <Text style={{ color: isRunning ? "#ea4c49" : "#37d05c" }}>{isRunning ? 'Stop' : 'Start'}</Text> </View> </TouchableOpacity> <TouchableOpacity onPress={handleLapButtonPress} style={[styles.controlButtonBorder, { backgroundColor: isRunning ? "#333333" : "#1c1c1e" }]}> <View style={styles.controlButton}> <Text style={{ color: isRunning ? "#fff" : "#9d9ca2" }}>{isRunning ? 'Lap' : 'Reset'}</Text> </View> </TouchableOpacity> </View> </View> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', padding: 0, }, androidPadding: { paddingTop: Platform.OS === 'android' ? 25 : 0, }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 0, paddingTop: 18, paddingBottom: 15, paddingLeft: 10, paddingRight: 10, }, title: { fontSize: 18, color: 'white', }, dotsIcon: { width: 20, height: 20, resizeMode: 'contain', }, modalContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(0, 0, 0, 0.5)', }, textFont: { fontFamily: 'Orbitron Black', }, contentContainer: { flex: 1, }, rowHud: { flex: 0.6, flexDirection: 'row', marginBottom: 0, marginTop: 10 }, timerOverlay: { position: 'absolute', top: '50%', left: '50%', transform: [{ translateX: -90 }, { translateY: -30 }], fontSize: 50, color: 'white', }, hoursTimerOverlay: { position: 'absolute', top: '35%', left: '50%', transform: [{ translateX: -30 }, { translateY: -30 }], fontSize: 35, color: 'white', }, centisecondsTimerOverlay: { position: 'absolute', top: '71%', left: '50%', transform: [{ translateX: -15 }, { translateY: -35 }], fontSize: 35, color: 'white', }, controls: { flexDirection: 'row', justifyContent: 'space-between', paddingLeft: 20, paddingRight: 20, }, controlButtonBorder: { justifyContent: "center", alignItems: "center", width: 70, height: 70, borderRadius: 70, }, controlButton: { justifyContent: "center", alignItems: "center", width: 65, height: 65, borderRadius: 65, borderColor: "#000", borderWidth: 1 }, });
内容的提问来源于stack exchange,提问作者f1ian
相关产品推荐
相关产品推荐

