앱이 종료된 경우에도 작동하는 스톱워치 후크 빌드
61683 단어 mobilereactnative
해당 구현의 문제는 앱이 백그라운드에서 활성화/실행되는 동안에만 작동한다는 것입니다. 앱이 종료되면 타이머가 중지됩니다.
오늘 우리는 앱이 몇 달 동안 종료되더라도 작동하도록 후크를 업그레이드할 것입니다. 다시 열면 타이머가 시작되는 한 시작한 이후 경과된 시간이 표시됩니다.
이 모든 것의 핵심은 데이터를 디스크에 유지할 수 있게 해주는
@react-native-async-storage/async-storage
패키지입니다.계속하기 전에 확인하십시오install the library.
This post was originally published on React Native School.
시작 코드
아래에서 우리가 시작할 코드를 볼 수 있습니다. 그 이유를 알아보려면 read the previous tutorial walking you through it step by step .
// hooks/useStopWatch.ts
import { useState, useRef, useEffect } from "react"
export type LapData = {
time: string
lap: number
}
const padStart = (num: number) => {
return num.toString().padStart(2, "0")
}
const formatMs = (milliseconds: number) => {
let seconds = Math.floor(milliseconds / 1000)
let minutes = Math.floor(seconds / 60)
let hours = Math.floor(minutes / 60)
// using the modulus operator gets the remainder if the time roles over
// we don't do this for hours because we want them to rollover
// seconds = 81 -> minutes = 1, seconds = 21.
// 60 minutes in an hour, 60 seconds in a minute, 1000 milliseconds in a second.
minutes = minutes % 60
seconds = seconds % 60
// divide the milliseconds by 10 to get the tenths of a second. 543 -> 54
const ms = Math.floor((milliseconds % 1000) / 10)
let str = `${padStart(minutes)}:${padStart(seconds)}.${padStart(ms)}`
if (hours > 0) {
str = `${padStart(hours)}:${str}`
}
return str
}
export const useStopWatch = () => {
const [time, setTime] = useState(0)
const [isRunning, setIsRunning] = useState(false)
const [startTime, setStartTime] = useState<number>(0)
const [timeWhenLastStopped, setTimeWhenLastStopped] = useState<number>(0)
const [laps, setLaps] = useState<number[]>([])
const interval = useRef<ReturnType<typeof setInterval>>()
useEffect(() => {
if (startTime > 0) {
interval.current = setInterval(() => {
setTime(() => Date.now() - startTime + timeWhenLastStopped)
}, 1)
} else {
if (interval.current) {
clearInterval(interval.current)
interval.current = undefined
}
}
}, [startTime])
const start = () => {
setIsRunning(true)
setStartTime(Date.now())
}
const stop = () => {
setIsRunning(false)
setStartTime(0)
setTimeWhenLastStopped(time)
}
const reset = () => {
setIsRunning(false)
setStartTime(0)
setTimeWhenLastStopped(0)
setTime(0)
setLaps([])
}
const lap = () => {
setLaps(laps => [time, ...laps])
}
let slowestLapTime: number | undefined
let fastestLapTime: number | undefined
const formattedLapData: LapData[] = laps.map((l, index) => {
const previousLap = laps[index + 1] || 0
const lapTime = l - previousLap
if (!slowestLapTime || lapTime > slowestLapTime) {
slowestLapTime = lapTime
}
if (!fastestLapTime || lapTime < fastestLapTime) {
fastestLapTime = lapTime
}
return {
time: formatMs(lapTime),
lap: laps.length - index,
}
})
return {
start,
stop,
reset,
lap,
isRunning,
time: formatMs(time),
laps: formattedLapData,
currentLapTime: laps[0] ? formatMs(time - laps[0]) : formatMs(time),
hasStarted: time > 0,
slowestLapTime: formatMs(slowestLapTime || 0),
fastestLapTime: formatMs(fastestLapTime || 0),
}
}
데이터 지속
가장 먼저 해야 할 일은 데이터를 AsyncStorage에 유지(저장)하는 것입니다. 사용 사례에서는 다음과 같은 상태를 저장하려고 합니다.
참고: 각 데이터 조각은 AsyncStorage에 문자열로 저장되어야 합니다.
// hooks/useStopWatch.ts
import { useState, useRef, useEffect } from "react"
import AsyncStorage from "@react-native-async-storage/async-storage"
export type LapData = {
time: string
lap: number
}
const padStart = (num: number) => {
/* ... */
}
const formatMs = (milliseconds: number) => {
/* ... */
}
const ASYNC_KEYS = {
timeWhenLastStopped: "useStopWatch::timeWhenLastStopped",
isRunning: "useStopWatch::isRunning",
startTime: "useStopWatch::startTime",
laps: "useStopWatch::laps",
}
export const useStopWatch = () => {
const [time, setTime] = useState(0)
const [isRunning, setIsRunning] = useState(false)
const [startTime, setStartTime] = useState<number>(0)
const [timeWhenLastStopped, setTimeWhenLastStopped] = useState<number>(0)
const [laps, setLaps] = useState<number[]>([])
const interval = useRef<ReturnType<typeof setInterval>>()
useEffect(() => {
// persist the latest data to async storage to be used later, if needed
const persist = async () => {
try {
await AsyncStorage.multiSet([
[ASYNC_KEYS.timeWhenLastStopped, timeWhenLastStopped.toString()],
[ASYNC_KEYS.isRunning, isRunning.toString()],
[ASYNC_KEYS.startTime, startTime.toString()],
[ASYNC_KEYS.laps, JSON.stringify(laps)],
])
} catch (e) {
console.log("error persisting data")
}
}
persist()
}, [timeWhenLastStopped, isRunning, startTime, laps])
useEffect(() => {
/* ... */
}, [startTime])
const start = () => {
/* ... */
}
const stop = () => {
/* ... */
}
const reset = () => {
/* ... */
}
const lap = () => {
/* ... */
}
let slowestLapTime: number | undefined
let fastestLapTime: number | undefined
const formattedLapData: LapData[] = laps.map((l, index) => {
/* ... */
})
return {
/* ... */
}
}
위의 코드에서 저는
useEffect
를 사용하여 상태 변경의 대상 부분 중 하나를 실행하고(각 상태를 종속성으로 추가하여) AsyncStorage의multiSet 기능을 활용하여 모든 데이터를 저장했습니다. 한 때.AsyncStorage에서 잠시 데이터를 가져오려면 동일한 키가 필요하므로 다른 데이터 조각을 참조하는 데 사용하는 키를 개체로 가져왔습니다.
AsyncStorage에서 데이터 로드
이제 후크가 처음 초기화될 때 AsyncStorage에서 실제로 데이터를 로드해야 합니다.
이를 위해 다시 한 번
useEffect
후크를 사용하지만 종속성이 없는 빈 종속성 배열을 전달합니다. 그렇게 하면 이 후크를 호출하는 구성 요소가 처음 마운트될 때만 실행됩니다.// hooks/useStopWatch.ts
import { useState, useRef, useEffect } from "react"
import AsyncStorage from "@react-native-async-storage/async-storage"
export type LapData = {
time: string
lap: number
}
const padStart = (num: number) => {
/* ... */
}
const formatMs = (milliseconds: number) => {
/* ... */
}
const ASYNC_KEYS = {
timeWhenLastStopped: "useStopWatch::timeWhenLastStopped",
isRunning: "useStopWatch::isRunning",
startTime: "useStopWatch::startTime",
laps: "useStopWatch::laps",
}
export const useStopWatch = () => {
const [time, setTime] = useState(0)
const [isRunning, setIsRunning] = useState(false)
const [startTime, setStartTime] = useState<number>(0)
const [timeWhenLastStopped, setTimeWhenLastStopped] = useState<number>(0)
const [laps, setLaps] = useState<number[]>([])
const interval = useRef<ReturnType<typeof setInterval>>()
useEffect(() => {
// load data from async storage in case app was quit
const loadData = async () => {
try {
const persistedValues = await AsyncStorage.multiGet([
ASYNC_KEYS.timeWhenLastStopped,
ASYNC_KEYS.isRunning,
ASYNC_KEYS.startTime,
ASYNC_KEYS.laps,
])
const [
persistedTimeWhenLastStopped,
persistedIsRunning,
persistedStartTime,
persistedLaps,
] = persistedValues
setTimeWhenLastStopped(
persistedTimeWhenLastStopped[1]
? parseInt(persistedTimeWhenLastStopped[1])
: 0
)
setIsRunning(persistedIsRunning[1] === "true")
setStartTime(
persistedStartTime[1] ? parseInt(persistedStartTime[1]) : 0
)
setLaps(persistedLaps[1] ? JSON.parse(persistedLaps[1]) : [])
} catch (e) {
console.log("error loading persisted data", e)
}
}
loadData()
}, [])
useEffect(() => {
// persist the latest data to async storage to be used later, if needed
/* ... */
}, [timeWhenLastStopped, isRunning, startTime, laps])
useEffect(() => {
/* ... */
}, [startTime])
const start = () => {
/* ... */
}
const stop = () => {
/* ... */
}
const reset = () => {
/* ... */
}
const lap = () => {
/* ... */
}
let slowestLapTime: number | undefined
let fastestLapTime: number | undefined
const formattedLapData: LapData[] = laps.map((l, index) => {
/* ... */
})
return {
/* ... */
}
}
이 코드는
multiGet
API 의 특성상 약간 지저분합니다.multiGet
를 사용할 때 응답은 다음과 같습니다.[
["useStopWatch::timeWhenLastStopped", "1000"],
["useStopWatch::isRunning", "false"],
["useStopWatch::startTime", "0"],
["useStopWatch::laps", "[]"],
]
따라서
example[1]
는 모든 곳에서 볼 수 있습니다. 해당 속성의 값에 액세스하기 위한 것입니다.AsyncStorage에서 값을 가져오면 해당 상태에 대한 올바른 유형으로 변환하거나 AsyncStorage에 값이 없으면 기본값을 설정해야 합니다.
데이터 로드 대기 중
끝났다고 생각할 수도 있지만 지금 바로 앱을 사용하려고 하면 앱을 새로 고칠 때 모든 것이 기본값으로 돌아가는 것을 볼 수 있습니다.
이는 데이터를 유지하는 후크가 AsyncStorage에서 데이터를 가져오기 전에 실행되어 데이터를 재정의하고 기본값으로 설정할 수 있기 때문입니다.
따라서 새로운 것을 유지하기 전에 AsyncStorage에서 데이터가 로드될 때까지 기다려야 합니다. 이를 처리하기 위해 새로운 상태 조각
dataLoaded
을 추가합니다. 아래 코드에서 // NEW LINE
를 찾아 추가된 내용을 확인하세요.// hooks/useStopWatch.ts
import { useState, useRef, useEffect } from "react"
import AsyncStorage from "@react-native-async-storage/async-storage"
export type LapData = {
time: string
lap: number
}
const padStart = (num: number) => {
/* ... */
}
const formatMs = (milliseconds: number) => {
/* ... */
}
const ASYNC_KEYS = {
timeWhenLastStopped: "useStopWatch::timeWhenLastStopped",
isRunning: "useStopWatch::isRunning",
startTime: "useStopWatch::startTime",
laps: "useStopWatch::laps",
}
export const useStopWatch = () => {
const [time, setTime] = useState(0)
const [isRunning, setIsRunning] = useState(false)
const [startTime, setStartTime] = useState<number>(0)
const [timeWhenLastStopped, setTimeWhenLastStopped] = useState<number>(0)
const [laps, setLaps] = useState<number[]>([])
const [dataLoaded, setDataLoaded] = useState(false)
const interval = useRef<ReturnType<typeof setInterval>>()
useEffect(() => {
// load data from async storage in case app was quit
const loadData = async () => {
try {
const persistedValues = await AsyncStorage.multiGet([
ASYNC_KEYS.timeWhenLastStopped,
ASYNC_KEYS.isRunning,
ASYNC_KEYS.startTime,
ASYNC_KEYS.laps,
])
const [
persistedTimeWhenLastStopped,
persistedIsRunning,
persistedStartTime,
persistedLaps,
] = persistedValues
setTimeWhenLastStopped(
persistedTimeWhenLastStopped[1]
? parseInt(persistedTimeWhenLastStopped[1])
: 0
)
setIsRunning(persistedIsRunning[1] === "true")
setStartTime(
persistedStartTime[1] ? parseInt(persistedStartTime[1]) : 0
)
setLaps(persistedLaps[1] ? JSON.parse(persistedLaps[1]) : [])
setDataLoaded(true) // NEW LINE
} catch (e) {
console.log("error loading persisted data", e)
setDataLoaded(true) // NEW LINE
}
}
loadData()
}, [])
useEffect(() => {
// persist the latest data to async storage to be used later, if needed
const persist = async () => {
try {
await AsyncStorage.multiSet([
[ASYNC_KEYS.timeWhenLastStopped, timeWhenLastStopped.toString()],
[ASYNC_KEYS.isRunning, isRunning.toString()],
[ASYNC_KEYS.startTime, startTime.toString()],
[ASYNC_KEYS.laps, JSON.stringify(laps)],
])
} catch (e) {
console.log("error persisting data")
}
}
// NEW LINE
if (dataLoaded) {
persist()
}
}, [timeWhenLastStopped, isRunning, startTime, laps, dataLoaded])
useEffect(() => {
/* ... */
}, [startTime])
const start = () => {
/* ... */
}
const stop = () => {
/* ... */
}
const reset = () => {
/* ... */
}
const lap = () => {
/* ... */
}
let slowestLapTime: number | undefined
let fastestLapTime: number | undefined
const formattedLapData: LapData[] = laps.map((l, index) => {
/* ... */
})
return {
start,
stop,
reset,
lap,
isRunning,
time: formatMs(time),
laps: formattedLapData,
currentLapTime: laps[0] ? formatMs(time - laps[0]) : formatMs(time),
hasStarted: time > 0,
slowestLapTime: formatMs(slowestLapTime || 0),
fastestLapTime: formatMs(fastestLapTime || 0),
dataLoaded,
}
}
위의 몇 가지 변경 사항을 볼 수 있습니다.
dataLoaded
를 truedataLoaded
가 AsyncStorage에 데이터를 저장하는 후크의 종속성으로 추가됨dataLoaded
함수를 호출하기 전에 persist()
가 참인지 확인합니다. dataLoaded
는 UI에서 사용할 수 있도록 후크에서 반환됩니다UI에서 플래시 방지
지금 앱을 실행하면 모든 것이 완벽하게 작동하지만 사용자가 앱을 열면 타이머가 실행 중이더라도 잠시
00:00.00
가 표시됩니다.구성 요소에서
dataLoaded
를 사용하고 모든 것이 로드될 때까지 null을 반환하면 이를 피할 수 있습니다.import { StyleSheet } from "react-native";
import { Text, View, StatusBar, SafeAreaView } from "components/themed";
import { CircleButton } from "components/buttons";
import { useStopWatch } from "hooks/useStopWatch";
import { LapList } from "components/lists";
const StopWatch = () => {
const {
time,
isRunning,
start,
stop,
reset,
lap,
laps,
currentLapTime,
hasStarted,
slowestLapTime,
fastestLapTime,
dataLoaded,
} = useStopWatch();
if (!dataLoaded) {
return null;
}
return (
/* ... */
);
};
const styles = StyleSheet.create({
/* ... */
});
export default StopWatch;
이제 타이머가 영원히 실행됩니다! 당신은 할 수 있습니다 view the final code on Github
Reference
이 문제에 관하여(앱이 종료된 경우에도 작동하는 스톱워치 후크 빌드), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/spencercarli/build-a-stop-watch-hook-that-works-even-when-the-app-is-quit-29mc텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)