ocetnik / react-native-background-timer

Emit event periodically (even when app is in the background)
MIT License
1.61k stars 227 forks source link

Timer is slower than an actual timer #270

Open MrShakes opened 3 years ago

MrShakes commented 3 years ago

The timer is slower(noticeable within 1 minute) than an actual timer or Javascript's default setInterval, I tried using runBackgroundTimer and setInterval(for Android), the latter being a bit faster however it would still lag behind an actual timer or Javascript's setInterval. Used on an actual device outside dev.

Code(using with Redux):

BackgroundTimer.runBackgroundTimer(() => {
    // code that will be called every 1 second
    tickTock(dispatch);
}, 1000);

"react-native-background-timer": "2.4.1" "react-native": "0.61.5"

tisch7 commented 3 years ago

Same isssue here !

Skr1pt1k commented 3 years ago

@MrShakes Did you find a solution?

VTSingle commented 3 years ago

Same issue

hadri3n commented 3 years ago

Same issue, did anyone find a way to fix that ? This behaviour was mentioned in issue #28 as well.

jnjacobson commented 3 years ago

Maybe this can help: https://github.com/ocetnik/react-native-background-timer/issues/258#issuecomment-749721388

serapme commented 3 years ago

same issue .10 milliseconds delay every called

taschik commented 3 years ago

same here about 5s per minute offset :( anyone found a workaround yet?

david-gettins commented 3 years ago

I attempted to use this for a stopwatch implementation and also experienced this issue. I ended up using a JS implementation instead. In reflection the README of this library does not claim to guarantee accuracy.

maxim-green commented 2 years ago

@david-gettins hi! Can you describe in two words how you implemented it with JS? I tried to do that using Headless JS, but it seems like it involves some Java coding, and i'm not good at Java:)

david-gettins commented 2 years ago

@david-gettins hi! Can you describe in two words how you implemented it with JS? I tried to do that using Headless JS, but it seems like it involves some Java coding, and i'm not good at Java:)

Although a JS implementation worked on iOS, on Android it stopped when the app was backgrounded.

I ended up creating my own React Native Native Module, in Kotlin for Android and Swift for iOS. I employed techniques described in the Android developer docs and Apple developer docs to get exact timing and now the solution works with exact second timing and while apps are backgrounded.

Unfortunately this was for an internal project for the company I work full time for. I'll have to ask for permission to publish the code publicly before I share any implementation details.

david-gettins commented 2 years ago

@mgeorgievsky To answer your question more directly, it was using setTimeout and correcting the next interval period by diffing the previous fire time with the expected time and adjusting the next millisecond value. If you don't already know setTimeout and setInterval are not exact and may fire before or after the specified period.

maxim-green commented 2 years ago

@mgeorgievsky thanks for your answer! As a result i just did like that: https://github.com/ocetnik/react-native-background-timer/issues/258#issuecomment-749721388

hadnet commented 1 year ago

Here a more recent implementation (hook) for a stopwatch. I think that can fix this delay issue (not tested on Android, though):

import {useState, useRef, useEffect} from 'react';
import BackgroundTimer from 'react-native-background-timer';
import {mmkvStorage} from 'App';

const padStart = (num: number) => {
  return num.toString().padStart(2, '0');
};

const formatMs = (milliseconds: number) => {
  let seconds = ~~(milliseconds / 1000);
  let minutes = ~~(seconds / 60);
  let hours = ~~(minutes / 60);

  minutes = minutes % 60;
  seconds = seconds % 60;
  const ms = ~~((milliseconds % 1000) / 10);

  let str = `${padStart(minutes)}:${padStart(seconds)}.${padStart(ms)}`;

  if (hours > 0) {
    str = `${padStart(hours)}:${str}`;
  }

  return str;
};

const enum MMKV_KEYS {
  timeWhenLastStopped = 'timeWhenLastStopped',
  isRunning = 'isRunning',
  startTime = 'startTime',
}

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 dataLoaded = useRef(false);

  useEffect(() => {
    // loading persisted data
    const timeWhenLastStopped = mmkvStorage.getNumber(
      MMKV_KEYS.timeWhenLastStopped,
    );
    const isRunning = mmkvStorage.getBoolean(MMKV_KEYS.isRunning);
    const startTime = mmkvStorage.getNumber(MMKV_KEYS.startTime);

    setTimeWhenLastStopped(timeWhenLastStopped ? timeWhenLastStopped : 0);
    setIsRunning(!!isRunning);
    setStartTime(startTime ? startTime : 0);
    dataLoaded.current = true;
  }, []);

  useEffect(() => {
    const persist = () => {
      try {
        mmkvStorage.set(MMKV_KEYS.timeWhenLastStopped, timeWhenLastStopped);
        mmkvStorage.set(MMKV_KEYS.isRunning, isRunning);
        mmkvStorage.set(MMKV_KEYS.startTime, startTime);
      } catch (e) {
        console.log('Error persisting data', e);
      }
    };

    if (dataLoaded.current) persist();
  }, [timeWhenLastStopped, isRunning, startTime, dataLoaded.current]);

  useEffect(() => {
    if (startTime > 0) {
      BackgroundTimer.runBackgroundTimer(() => {
        setTime(() => Date.now() - startTime + timeWhenLastStopped);
      }, 1);
    } else {
      BackgroundTimer.stopBackgroundTimer();
    }
  }, [startTime]);

  const start = () => {
    setIsRunning(true);
    setStartTime(Date.now());
  };

  const pause = () => {
    setIsRunning(false);
    setStartTime(0);
    setTimeWhenLastStopped(time);
  };

  const reset = () => {
    setIsRunning(false);
    setStartTime(0);
    setTimeWhenLastStopped(0);
    setTime(0);
  };

  return {
    start,
    pause,
    reset,
    isRunning,
    rawTime: time,
    time: formatMs(time),
    hasStarted: time > 0,
  };
};
thatgriffith commented 1 year ago

For anyone still experiencing this issue (despite the solution above from @hadnet), I solved it with the following:

For iOS it works with JS own setInterval so no additional package needed here. For Android I found the package: https://github.com/juanamd/react-native-background-timer-android that states to be an improvement of this package. I gave it a go and it works perfect. However, in dev mode on real device when returning from background the UI could lag from time to time. But when testing on a production apk, it works smoothly. No delay in time or any other problem. Hopefully this helps anyone still struggling with this problem.