앱이 종료된 경우에도 작동하는 스톱워치 후크 빌드

61683 단어 mobilereactnative
이전에 a custom hook to power a stop watch을 빌드했습니다.

해당 구현의 문제는 앱이 백그라운드에서 활성화/실행되는 동안에만 작동한다는 것입니다. 앱이 종료되면 타이머가 중지됩니다.



오늘 우리는 앱이 몇 달 동안 종료되더라도 작동하도록 후크를 업그레이드할 것입니다. 다시 열면 타이머가 시작되는 한 시작한 이후 경과된 시간이 표시됩니다.

이 모든 것의 핵심은 데이터를 디스크에 유지할 수 있게 해주는 @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에 유지(저장)하는 것입니다. 사용 사례에서는 다음과 같은 상태를 저장하려고 합니다.
  • timeWhenLastStopped
  • isRunning
  • 시작시간
  • 랩스

  • 참고: 각 데이터 조각은 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,
      }
    }
    


    위의 몇 가지 변경 사항을 볼 수 있습니다.
  • AsyncStorage에서 데이터 로드에 성공하거나 실패하면 dataLoaded를 true
  • 로 설정합니다.
  • dataLoaded가 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

    좋은 웹페이지 즐겨찾기