AppState Always firing on Timer - react-native

I am building a Timer to run in the background
I am Using React Native and Expo to build the app
Now the timer I am using I am trying to let it "run" in the background
To do this I am using AppState and a event listner, taking the Start Time, and the elapsed Time (Elabsed while the app is minimized)
And then recalculating the Time elapsed and adding that to the timer
In the Timer There are start, pause, reset and done buttons
Want I am not getting to work is that the elapsed time must not get set into the setstate if the pause button is clicked, and no matter how many IFs I put in there, the Time always gets changed
const appState = useRef(AppState.currentState);
const [timerOn, setTimerOn] = useState(false);
const [time, setTime] = useState(0);
const [Paused,isPaused] = useState("");
const getElapsedTime = async () => {
try {
const startTime = await AsyncStorage.getItem("#start_time");
const now = new Date();
return differenceInSeconds(now, Date.parse(startTime));
} catch (err) {
console.warn(err);
}
};
const recordStartTime = async () => {
try {
const now = new Date()
await AsyncStorage.setItem("#start_time", now.toISOString());
} catch (err) {
console.warn(err);
}
};
useEffect(() => {
Timer()
}, []);
function Timer(){
if(Paused == "no"){
AppState.addEventListener("change", handleAppStateChange);
return () => AppState.removeEventListener("change", handleAppStateChange);
}
else{
console.log("eVENT lISTNER")
}
}```
const handleAppStateChange = async (nextAppState) => {
if (appState.current.match(/inactive|background/) &&
nextAppState == "active" && Paused == "no") {
// We just became active again: recalculate elapsed time based
// on what we stored in AsyncStorage when we started.
const elapsed = await getElapsedTime();
// Update the elapsed seconds state
//THE BELOW STATE IS UPDATED TO "ELAPSED" EVENTHOUGH IN THE IF STATEMENT ABOVE SAYS "PAUSED = NO"
setTime(elapsed);
}
else{
console.log("YES")
}
appState.current = nextAppState;
};
useEffect(() => {
let interval = null;
if (timerOn) {
interval = setInterval(() => {
setTime((prevTime) => prevTime + 1);
}, 1000);
} else if (!timerOn) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [timerOn]);
let MyHour = ("0" + Math.floor((time / 3600))).slice(-2);
let MyMinutes =("0" + Math.floor((time / 60) % 60)).slice(-2);
let MySeconds = ("0" + ((time % 60))).slice(-2);
function TimerBtn(){
isPaused("no")
setTimerOn(true)
recordStartTime()
}
function PauseBtn(){
isPaused("yes")
setTimerOn(false)
}
function ResetBtn(){
setTimerOn(false)
setTime(0)
}
function DoneBtn(){
}

Your problem is probably that you cannot access the current state of Paused inside a listener. Read this answer here for a detailed explanation
To work around, use a reference instead (useRef).
const [Paused,isPaused] = useState("");
to
const isPaused = useRef(true); // use booleans instead of "yed"/"no"
keep in mind to read and write to references you have to append '.current' to the reference, e.g:
isPaused.current = true;
// or
if (isPaused.current) {...}

Related

Expo AV playback callback strange behaviour with state properties

I am starting to go nuts with this one. I am recording an audio, once I stop the recording as part of the this process I also load the audio so that it is ready to be played back when necessary and here I do setOnPlaybackStatusUpdate. I use the playback callback so that I can update a my currentSeconds state based on positionMillis.
The problem is the currentSeconds and recordedDuration state values that I am getting. How come their values change between playAudio method which triggers the audio to be played and onPlaybackStatusUpdate which is the callback method?
When I output in the console their values in both methods this is what I obtain when I expect them to be the same:
In playAudio - currentSeconds: 0
In playAudio - recordedDuration: 4.5
In onPlaybackStatusUpdate - currentSeconds: 115.5
In onPlaybackStatusUpdate - recordedDuration: 0
And here is the code:
const AudioRecorder = useRef(new Audio.Recording());
const AudioPlayer = useRef(new Audio.Sound());
const timerMaxDuration = 120
const [currentSeconds, setCurrentSeconds] = useState<number>(timerMaxDuration);
const [recordedDuration, setRecordedDuration] = useState<number>(0);
const stopRecording = async () => {
try {
await AudioRecorder.current.stopAndUnloadAsync();
// To hear sound through speaker and not earpiece on iOS
await Audio.setAudioModeAsync({ allowsRecordingIOS: false });
const recordedURI = AudioRecorder.current.getURI();
SetRecordingURI(recordedURI)
AudioRecorder.current = new Audio.Recording();
send('STOP')
setRecordedDuration(+(timerMaxDuration - currentSeconds).toFixed(1)) // there is subtraction because during the recording there is a countdown from timerMaxDuration
setCurrentSeconds(0)
// Load audio after recording so that it is ready to be played
loadAudio(recordedURI)
} catch (error) {
console.log(error);
}
};
const loadAudio = async (recordedUri) => {
try {
const playerStatus = await AudioPlayer.current.getStatusAsync();
if (playerStatus.isLoaded === false) {
AudioPlayer.current.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate)
await AudioPlayer.current.loadAsync({ uri: recordedUri }, { progressUpdateIntervalMillis: 20 }, true)
}
} catch (error) {
console.log(error)
}
}
const playAudio = async () => {
console.log(`In playAudio - currentSeconds: ${currentSeconds}`)
console.log(`In playAudio - recordedDuration: ${recordedDuration}`)
try {
const playerStatus = await AudioPlayer.current.getStatusAsync();
if (playerStatus.isLoaded) {
if (playerStatus.isPlaying === false) {
AudioPlayer.current.playAsync();
send('PLAY')
}
}
} catch (error) {
console.log(error);
}
};
const onPlaybackStatusUpdate = playbackStatus => {
if (playbackStatus.isPlaying) {
console.log(`In onPlaybackStatusUpdate - currentSeconds: ${currentSeconds}`)
console.log(`In onPlaybackStatusUpdate - recordedDuration: ${recordedDuration}`)
if(currentSeconds >= recordedDuration){
stopAudio()
}
else{
setCurrentSeconds(+(playbackStatus.positionMillis / 1000).toFixed(1))
}
}
}
Ok so there was nothing wrong with the playback callback. The thing is that both the playback callback is an arrow functions which means that the only property value that will change inside the callback is the one of the argument playbackStatus, the other properties' values will remain the same as the time the function was created.
A walkaround in React is to use useEffect in the following way, which allows to access the state values currentSeconds and recordedDuration:
useEffect(() => {
if(currentSeconds >= recordedDuration)
stopAudio()
}, [currentSeconds]);
const onPlaybackStatusUpdate = playbackStatus => {
if (playbackStatus.isPlaying)
setCurrentSeconds(+(playbackStatus.positionMillis / 1000).toFixed(1))
}

React Native Hooks initializer not taking the correct value

What I am trying to do is sync a list of attendees from an online database, and if the current user is in the list, then disable a button, else enable the button.
I am using react native hook (I am not sure if I am using the term correctly as I am fairly new to react), in order to set the value of disabling the button.
The issue that I am facing is that the value is getting initialized to false, even tho it should clearly get initialized to true.
After adding some logging I made sure that the function is executing correctly and reaching the code where it sets the value to true.
const [buttonDisabled, changeButtonState] = useState( () => {
var database = firebase.database();
var userId = firebase.auth().currentUser.uid;
const dbRef = firebase.database().ref();
var Attendees = [];
var disable = false;
dbRef.child("gameAttendees").child(gameinfo.gameID).get().then((snapshot) => {
if (snapshot.exists()) {
Attendees = snapshot.val().Attendees;
for(var i=0;i<Attendees.length;i++){
if(Attendees[i]==userId){
return true;
}
}
} else {
console.log("no value");
return false;
}
}).catch((error) => {
console.error(error);
});
});
Adding an example of an async mount effect:
const Comp = () => {
const [s, setS] = useState(); // State will be undefined for first n renders
useEffect(() => {
// Call the async function and set the component state some time in the future
someAsyncFunction().then(result => setS(result));
}, []); // An effect with no dependencies will run only once on mount
return </>;
};

React Native - How to delay a start

Hi everybody I'm trying to make a countdown timer with a lot of customizations but my first problem is how to delay the start of it.
It starts with 0 seconds and takes a bit to get the value I passed from the previous page
const CountDownScreen = ({route, navigation}) => {
const meditationTime = route.params.time;
I take the time
const getMedTime = () => {
let res = meditationTime;
return new Promise(resolve => resolve(res));
};
state to put the time I'll get
const [timeToGive, setTimeToGive] = useState();
useEffect(() => {
const setTimeState = async () => {
const getTime = await getMedTime();
console.log('get time', getTime);
// setGotTime(getTime);
let timeToGive = {
minutes: 0,
seconds: getTime,
};
setTimeToGive(timeToGive);
};
setTimeState();
updateTimer();
}, []);
state with the time
const [time, setTime] = useState(
{
eventDate: moment.duration().add(timeToGive),
},
);
the timer starts (I suppose after the PROMISE)
const updateTimer = () => {
const x = setInterval(() => {
let { eventDate } = time;
if (eventDate <= 0) {
clearInterval(x);
sessionEnded();
} else {
eventDate = eventDate.subtract(1, 's');
const mins = eventDate.minutes();
const secs = eventDate.seconds();
setTime({
mins,
secs,
eventDate,
});
}
}, 1000);
setIntervalTime(x);
};
I tried a lot of different solutions but none worked.
It always starts late
I would like to avoid a setTimeout is there any better function?
Thanks !
Thats because you are using setInterval, which starts only after 1 second (in your case).
You should probably readapt your code and do something like that :
x(); // Call x once
setInterval(() => {
x();
}, 1000); // Call x after every seconds
You can store the intervalId into a state, that way you can clear it.

Clear the app data if disconnected from internet for more then 5min in react native

I want to clear the react native app data if my app is disconnected from internet for more than 5 min.
I am using react native NetInfo to check network connectivity status.
Saving the time when app disconnected and checking when it will reconnect to internet.
If interval is more than 5 min then I want to clear the app data.
My Code is:
class OfflineMessage extends PureComponent {
constructor(props) {
super(props);
this.state = {
isConnected: true
};
}
componentDidMount() {
NetInfo.addEventListener((state) => {
this.handleConnection(state.isConnected);
});
}
componentWillUnmount() {
NetInfo.removeEventListener((state) => {
this.handleConnection(state.isConnected);
});
}
handleConnection = (isConnected) => {
this.setState({ isConnected });
if(!isConnected){
this.startTimer();
} else {
this.checkElapsed();
}
};
startTimer = async () => {
try {
console.log('Internet disconnected at: ');
await AsyncStorage.setItem('time', JSON.stringify(Date.now()));
} catch (error) {
// console.log('Something went wrong', error);
}
}
checkElapsed = async () => {
try {
let startTime = await AsyncStorage.getItem('time');
if(startTime){
let endTime = Date.now();
const elapsedTime = Math.floor((endTime -JSON.parse(startTime))/1000);
if(elapsedTime > 5){
alert("5 min is completed.");
// Clear app data
}
console.log('Time elapsed'+ elapsedTime);
}
} catch (error) {
// console.log('Something went wrong', error);
}
}
Problem:
Both the methods startTimer and checkElapsed called whenever connectivity status changes.
What is wrong with this code.
if I modify given code as :
state = {
isConnected: true
};
componentDidMount() {
this.unsubscribeFromNetInfo = NetInfo.addEventListener((state) => {
this.handleConnection(state.isConnected);
});
}
componentWillUnmount() {
this.unsubscribeFromNetInfo();
}
handleConnection = (isConnected) => {
console.log(isConnected);
this.setState({ isConnected });
};
EventListener called multiple times and status changes frequently true false,true,false .
Now, you are handling the NetInfo subscription wrong, according to https://github.com/react-native-community/react-native-netinfo#usage
You would have to do:
componentDidMount() {
this.unsubscribeFromNetInfo = NetInfo.addEventListener(state => {
this.handleConnection(state.isConnected);
});
}
componentWillUnmount() {
this.unsubscribeFromNetInfo();
}
Also, if you want to check for 5 minutes use:
if (elapsedTime > 5 * 60)
as your conversion
Math.floor((endTime - JSON.parse(startTime)) / 1000)
converts it to seconds not minutes.
In the current state, your app will trigger almost everything as the code only checks for 5 seconds.
Otherwise, the logic that you implemented itself should be working :)

How to combine a loop of Axios calls with an await-ed function?

That's a tricky one I guess. I'm prompting user for a word which I validate with an Axios API call. Once validation clears, the main loop of my game - hangman - starts with a wait between each move (hence the use of await).
Problem: in current version, the main game loop (starting after "once validation clears, game starts below" comment) must starts after validation, when in fact it starts at the same time which messes everything up.
And I can't put my main loop inside the then() part of my Axios call because in that case the await-ed function call ceases to work.
Any idea to get out of this mess?
async startGameComputerGuesser () {
var wordIsValidated = false
const vm = this
const dispatcher = {
execute: function () {
const wordApiBaseUrl = 'https://www.dictionaryapi.com/api/v1/references/sd4/xml'
wordToGuess = prompt('Enter a word:').toLowerCase()
const dispatcher = this
vm.axios.get(`${wordApiBaseUrl}/${wordToGuess}?key=${wordApiKey}`).then(res => {
if (!res.data.includes('def')) {
dispatcher.execute()
} else {
wordIsValidated = true
}
})
}
}
dispatcher.execute()
// once validation clears, game starts below
if (wordIsValidated) {
while (!this.$store.state.gameIsOver) {
await this.resolveAfter2Seconds()
// main loop of the game goes here
}
}
}
use await inside the execute and return true/false then use while to check that condition like below
async startGameComputerGuesser() {
let wordIsValidated = false;
const vm = this;
const dispatcher = {
async execute() {
const wordApiBaseUrl = 'https://www.dictionaryapi.com/api/v1/references/sd4/xml'
const wordToGuess = prompt('Enter a word:').toLowerCase();
const res = await vm.axios.get(`${wordApiBaseUrl}/${wordToGuess}?key=${wordApiKey}`);
return res.data.includes('def');
}
}
// validation
while (!wordIsValidated) {
wordIsValidated = await dispatcher.execute();
}
// game starts below
while (!this.$store.state.gameIsOver) {
await this.resolveAfter2Seconds()
// main loop of the game goes here
}
}
Example code:
const startGameComputerGuesser = async function() {
let wordIsValidated = false;
const dispatcher = {
async execute() {
const res = await new Promise(res => setTimeout(() => res(Math.floor(Math.random() * 10)), 500));
console.log(res);
return res == 6;
}
}
// validation
while (!wordIsValidated) {
wordIsValidated = await dispatcher.execute();
}
// once validation clears, game starts below
console.log('started');
}
startGameComputerGuesser();