I am working on something where I need to track background location if the app is in background and also if the device is asleep. I currently have it working for app in background but it stops tracking when the device is asleep. I am using Expo for the app and using Expo Task Manager alongside Expo Location to fetch location in background.
Anyone have any idea how to fetch location while app is in background and device is in sleep mode ?
Here's the code
import { StatusBar } from 'expo-status-bar';
import React, { useState, useEffect } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import * as Location from 'expo-location';
import * as TaskManager from 'expo-task-manager';
const App = () => {
useEffect(() => {
(async () => await _askForLocationPermission())();
});
this.backgroundLocationFetch = async () => {
const { status } = await Location.requestBackgroundPermissionsAsync();
if (status === 'granted') {
console.log('cmon dance with me!')
await Location.startLocationUpdatesAsync('FetchLocationInBackground', {
accuracy: Location.Accuracy.Balanced,
timeInterval: 3000,
distanceInterval: 1,
foregroundService: {
notificationTitle: 'Live Tracker',
notificationBody: 'Live Tracker is on.'
}
});
}
}
const _askForLocationPermission = async () => {
(async () => {
let { status } = await Location.requestBackgroundPermissionsAsync();
if (status !== 'granted') {
setgpsErrorMsg('Permission to access location was denied');
}
})();
};
return(
<View>
<Text></Text>
</View>
)
};
TaskManager.defineTask('FetchLocationInBackground', ({ data, error }) => {
if (error) {
console.log("Error bg", error)
return;
}
if (data) {
const { locations } = data;
console.log("BGGGG->", locations[0].coords.latitude, locations[0].coords.longitude);
}
});
export default App;
I had precisely the same problem and solved this by using and EventEmitter to dispatch the location updates to the UI component.
Top of file:
import EventEmitter from 'EventEmitter'
const locationEmitter = new EventEmitter();
didMount:
locationEmitter.on(LOCATION_UPDATE, (locationData) => {
console.log('locationEmitter locationUpdate fired! locationData: ', locationData);
let coordinatesAmount = locationData.newRouteCoordinates.length - 1;
this.setState({
latitude: locationData.newRouteCoordinates[coordinatesAmount - 1].latitude,
longitude: locationData.newRouteCoordinates[coordinatesAmount - 1].longitude,
routeCoordinates: this.state.routeCoordinates.concat(locationData.newRouteCoordinates)
})
})
componentWillUnmount:
locationEmitter.off(LOCATION_UPDATE);
inside background task definition:
locationEmitter.emit(LOCATION_UPDATE, locationData)
This succesfully sends the location data from the background task, but I'm still stuck in the problem of how to make the background task send location update batches more often. My related post is here.
Related
Thank you for visiting this post.
I am having a trouble with React Native getting user location.
I have searched for a quite some time and read other people's posts about this function.
It seems like this function has some several problems.
And also react native expo documentation seems to be outdated.
https://docs.expo.dev/versions/latest/sdk/location/
I use the code from here under usage section.
import React, { useState, useEffect } from 'react';
import { Platform, Text, View, StyleSheet } from 'react-native';
import * as Location from 'expo-location';
export default function App() {
const [location, setLocation] = useState(null);
const [errorMsg, setErrorMsg] = useState("");
useEffect(async () => {
(async () => {
let { status } = await Location.requestForegroundPermissionsAsync();
console.log(status);
if (status !== 'granted') {
console.log("denied")
setErrorMsg('Permission to access location was denied');
return;
}
console.log("status", status); // <=== always says "granted"
let location = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.Highest,
maximumAge: 10000,
timeout: 5000
});
console.log({ location }) // <== never reach here.
setLocation(location);
setErrorMsg('No ERROR');
})();
}, []);
let text = 'Waiting..';
if (errorMsg) {
text = errorMsg;
} else if (location) {
text = JSON.stringify(location);
}
return (
<View>
<Text>{text}</Text>
</View>
);
}
I have seen one of the post saying I have to pass the arguments of accuracy, maximumAge to getCurrentPositionAsync, instead of {} empty object which is provided in the expo docs.
But still does not work. since getCurrentPositionAsync is hanging, the screen keep displaying waiting....
And of course, the android emulator is setup correctly I believe, since I do see the status log and it says "granted".
Thank you in advance so much for your help and reading my post.
"expo": "~43.0.2",
"expo-location": "~13.0.4",
"expo-status-bar": "~1.1.0",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-native": "0.64.3",
"react-native-web": "0.17.1"
You can't use async with useEffect, and therefore not await.
Have you tried the other way around?
const [position, setPosition] = useState(false)
// useEffect
Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.Highest,
maximumAge: 10000,
timeout: 5000
})
.then(res => setPosition(res))
.catch(e => console.log(e)
I've also had more chances without non-fat arrowed IIFE, you should try out
(function () {
let { status } = await Location.requestForegroundPermissionsAsync();
console.log(status);
if (status !== 'granted') {
console.log("denied")
setErrorMsg('Permission to access location was denied');
return;
}
console.log("status", status); // <=== always says "granted"
let location = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.Highest,
maximumAge: 10000,
timeout: 5000
});
console.log({ location }) // <== never reach here.
setLocation(location);
setErrorMsg('No ERROR');
})();
Main Problem:
I am using expo-location in my Android app in order to find the gps coordinates of the user. I have read and used the sample code provided in the expo documentation. My main problem is that, Location.getCurrentPositionAsync({}) returns Location provider is unavailable. Make sure that location services are enabled. This is my code below:
useEffect(() => {
(async () => {
let { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
setErrorMsg('Permission to access location was denied');
return;
}
let location = await Location.getCurrentPositionAsync({});
console.log(location);
})();
}, []);
Status returns granted but getCurrentPositionAsync({}) returns an error. I implemented a janky solution by using try-catch blocks and running getCurrentPositionAsync({}) again in the catch block, which seems to work. This is my code below:
useEffect(() => {
(async () => {
let { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
setErrorMsg('Permission to access location was denied');
return;
}
try {
var location = await Location.getCurrentPositionAsync({});
} catch {
location = await Location.getCurrentPositionAsync({});
}
console.log(location);
})();
}, []);
Does anyone know why this is happening?
EDIT: I tried to run the sample code posted in the expo-location documentation using their snack example. Same result. Could this be a problem with my phone/area? I've tested it with two phones, and both return the same error.
I think location variable can't log / use directly and this is the way that I retrieve location:
let { status } = await Location.requestPermissionsAsync();
let location = await Location.getCurrentPositionAsync({});
if (status !== 'granted') setErrorMsg('...');
const { coords } = location;
if (coords) {
const { latitude, longitude } = coords;
}
I run a React Native app on Android emulator but found networking does not work, I run it on iOS it works fine.
Here is the simple code:
import React from 'react';
import {StyleSheet, View, Text} from 'react-native';
export default () => {
React.useEffect(() => {
const useFetch = async () => {
try {
console.log('fetch ...');
let response = await fetch('https://mytestdomain.com');
console.log(response.status);
// let json = await response.json();
} catch (error) {
console.log(error);
}
};
useFetch();
const useXMLHttpRequest = async () => {
try {
var request = new XMLHttpRequest();
request.onreadystatechange = (e) => {
if (request.readyState !== 4) {
console.log(request.readyState);
return;
}
if (request.status === 200) {
console.log('success', request.responseText);
} else {
console.warn('error');
}
};
request.open('GET', 'https://mytestdomain.com');
request.send();
} catch (error) {
console.log(error);
}
};
useXMLHttpRequest();
}, []);
return (
<View style={styles.layout}>
<Text>React Native Android networking</Text>
</View>
);
};
const styles = StyleSheet.create({
layout: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
By call useFetch() I just see 'fetch ...' in console, even can not see response.status, and there is no error from catch.
By call useXMLHttpRequest() I see request.readyState is 1.
Thanks advance for any help
You probably should give this asynchronous log a more explicit message to find this specific one among other logs, like :
console.log("RS : " + response.status);
I tried to reproduce your issue, and I can't, it works on my Android simulator just fine.
Would you please share your console logs ?
A workaround is to use the #react-native-community/netinfo library to determine whether you have internet access or not.
Beware, it could be slow.
This library provides a listener with the connection details changes. Getting the connection details can be slow... you would have to test it on real devices to determine whether this workaround is efficient or not.
Quick Demo based on your source code :
[...]
import { StyleSheet, View, Text } from "react-native";
+import NetInfo from "#react-native-community/netinfo";
export default () => {
- React.useEffect(() => {
+ NetInfo.addEventListener((netInfo) => {
const useFetch = async () => {
try {
- console.log("fetch ...");
- let response = await fetch("https://mytestdomain.com");
- console.log(response.status);
- // let json = await response.json();
+ // Just because there is a connection, it does not mean that internet is accessible, so test both isConnected and isInternetReachable
+ if (netInfo.isConnected && netInfo.isInternetReachable) {
+ console.log("fetch ...");
+ let response = await fetch(
+ "https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch"
+ );
+ console.log("start 1");
+ console.log("success 1", response.status);
+ console.log("stop 1");
+ } else {
+ console.log("internet is not ready");
+ }
} catch (error) {
[...]
I have a react native app and I'm trying to implement push notification with the react-native-push-notification package and firebase cloud messaging. It turns out that when I get a notification, the app just stops working. The RemotePushController function seems to work, as soon as the App starts I get the console log with the token. I've tried several things but nothing works.
[]
[
I was able to solve the problem by changing the versions of the googlePlayServicesVersion and firebaseMessagingVersion at android/build.gradle as shown in the image below. I set both to "+".
For Android, no need to set anything in AndroidManifest.xml.
And in the top level build.gradle having the following configuration should be sufficient. Notice the "+" against the versions.
buildscript {
ext {
buildToolsVersion = "28.0.3"
minSdkVersion = 16
compileSdkVersion = 28
targetSdkVersion = 28
googlePlayServicesVersion = "16.+"
androidMapsUtilsVersion = "0.5+"
firebaseVersion = "+"
firebaseMessagingVersion = "+"
}
Here's a hook that I have created for FCM.
import React, { useEffect } from 'react'
import { Alert } from 'react-native'
import messaging from '#react-native-firebase/messaging'
import { useNavigation } from '#react-navigation/native'
import { useAsyncStorage } from '#react-native-community/async-storage'
const useFirebaseCloudMessaging = () => {
const navigation = useNavigation()
const { getItem: getFcmToken, setItem: saveFcmToken } = useAsyncStorage('fcmToken')
const [fcmToken, setFcmToken] = React.useState(null)
const [initialRoute, setInitialRoute] = React.useState('Home')
const getToken = async () => {
const token = await getFcmToken()
if (!token) {
// Get the device token
messaging()
.getToken()
.then(token => {
setFcmToken(token)
saveFcmToken(token)
})
}
}
const requestUserPermission = async () => {
const authStatus = await messaging().requestPermission()
const enabled =
authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
authStatus === messaging.AuthorizationStatus.PROVISIONAL
if (enabled) {
console.log('Authorization status:', authStatus)
}
}
useEffect(() => {
// If using other push notification providers (ie Amazon SNS, etc)
// you may need to get the APNs token instead for iOS:
// if(Platform.OS == 'ios') { messaging().getAPNSToken().then(token => { return saveTokenToDatabase(token); }); }
// Listen to whether the token changes
return messaging().onTokenRefresh(token => {
saveFcmToken(token)
})
}, [])
useEffect(() => {
const unsubscribe = messaging().onMessage(async remoteMessage => {
Alert.alert('A new FCM message arrived!', JSON.stringify(remoteMessage))
})
return unsubscribe
}, [])
useEffect(() => {
// Assume a message-notification contains a "type" property in the data payload of the screen to open
messaging().onNotificationOpenedApp(remoteMessage => {
console.log(
'Notification caused app to open from background state:',
remoteMessage.notification
)
navigation.navigate(remoteMessage.data.type)
})
// Check whether an initial notification is available
messaging()
.getInitialNotification()
.then(remoteMessage => {
if (remoteMessage) {
console.log(
'Notification caused app to open from quit state:',
remoteMessage.notification
)
setInitialRoute(remoteMessage.data.type) // e.g. "Settings"
}
})
}, [])
return {
fcmToken,
getToken,
requestUserPermission
}
}
export default useFirebaseCloudMessaging
Use the hook in a top level App component
import React, { useEffect } from 'react'
import Root from './App'
import useFirebaseCloudMessaging from './FirebaseCloudMessaging'
const App = () => {
const {
getToken,
requestUserPermission
} = useFirebaseCloudMessaging()
useEffect(() => {
requestUserPermission()
getToken()
}, [])
return <Root />
}
export default App
Fairly new to React native and its concepts. I have been playing with RN for a while to create an application to fetch API data from
http://jsonplaceholder.typicode.com/photos
I have been looking into the documentation of AsyncStorage to implement how i can cache the API data so that upon terminating the application, it doesn't have to deal with fetching the data from web again and again, but wasn't successfully able to implement it.
It will be great if you can provide me help/suggestion based on it. I have included my source code for the 2 important files in my application, along with the a Test.js file with how i was trying to achieve.
import React, {Component} from 'react';
import { FlatList, View, Text, AsyncStorage, ActivityIndicator } from 'react-native';
import axios from 'axios';
import GalleryDetail from './GalleryDetail';
class GalleryList extends Component {
state = { photos: []};
componentDidMount() {
axios.get('http://jsonplaceholder.typicode.com/photos')
.then(response => this.setState({ photos: response.data }))
.catch((error)=> console.warn("fetch Error: ", error));
}
getPhotos = async()=> {
try {
photos = await AsyncStorage.getItem('GalleryPhotos');
}
catch (error) {
console.error(error);
}
}
savePhotos(){
AsyncStorage.setItem('GalleryPhotos', this.state.photos);
console.log('works !');
}
renderPhoto = ({item})=> {
return <GalleryDetail photo={item}/>
}
keyExtractor = (photo, index) => photo.id;
render () {
if(!this.state.photos){
return <ActivityIndicator/>;
}
return (
<FlatList
data = {this.state.photos}
keyExtractor={this.keyExtractor}
renderItem={this.renderPhoto}
/>
);
}
}
export default GalleryList;
and GalleryDetail linked with GalleryList-
import React, {Component} from 'react';
import { Text, View, Image } from 'react-native';
import Card from './Card';
import CardSection from './CardSection';
const GalleryDetail = (props)=> {
return (
<Card>
<CardSection style = {styles.headerContentStyle}>
<Image
style={styles.thumbnailStyle}
source = {{ uri: props.photo.thumbnailUrl}}/>
<Text style= {styles.textStyle}>{props.photo.title} </Text>
</CardSection>
</Card>
);
};
const styles = {
headerContentStyle: {
flexDirection: 'column',
justifyContent: 'space-around'
},
thumbnailStyle: {
height: 60,
width: 60
},
textStyle: {
fontSize: 12,
//textAlign: 'right',
flexDirection: 'row',
justifyContent: 'flex-end',
flex: 1,
flexWrap: 'wrap',
marginLeft: 5,
marginRight: 5,
}
}
export default GalleryDetail;
My method of trying was that-
Upon launching the application, it will first look in asyncStorage, if it finds the data- it fetches from async otherwise going to the web,fetching and storing again for later use.
I tried to implement somewhat like this in a separate file since i dint wanted to breakdown my already running app. The weird broken syntax is
State = {
photos: []
}
componentDidMount() {
// just a variable acting to fetch data from the stored keyvalue pair
check = AsyncStorage.getItem("PhotosKey").then((response) => {
this.setState({"PhotosKey": response});
}).done();
if(check) {
console.log('Data was fetched!!!!!');
check();
}
else {
console.log("Data was not fetched!");
var Data = axios.get('http://jsonplaceholder.typicode.com/photos').
then(response => this.setState({ photos: response.data })).
catch((error)=> console.warn("fetch Error: ", error));
}
}
Thanks in advance!
async componentDidMount() {
const photoStorage = await AsyncStorage.getItem('GalleryPhotos')
if(photoStorage) {
try {
const photoResp = await axios.get('http://jsonplaceholder.typicode.com/photos')
const photoData = await JSON.stringify(photoResp.data)
await AsyncStorage.setItem('GalleryPhotos', photoData);
} catch(e) {
console.warn("fetch Error: ", error)
}
.then(response => this.setState({ photos: response.data }))
}
}
later
getPhotos = async()=> {
try {
photos = JSON.parse(await AsyncStorage.getItem('GalleryPhotos'));
}
catch (error) {
console.error(error);
}
}
The approach from Subramanya is basically all you need to get started, I'm just going to introduce a state management approach with redux-persist where you can definitely appreciate when your app grows.
Redux Persist is performant, easy to implement, and easy to extend.
Let say you have your app hooked up with redux and implemented a fairly organised state tree, redux-persist stores the entire app state with AsyncStorage or any storage engine of your choice.
For instance, let's assume that your API endpoint returns a collection of photos, all you need to do is update the store, and your users can expect their data is safe and saved with redux-persist.
I have not tested all the code below
Let's define the store first,
import { AsyncStorage } from 'react-native';
import { createStore, compose, applyMiddleware, } from "redux";
import { persistStore } from "redux-persist";
import ReduxThunk from "redux-thunk";
import reducers from "../reducers"
const middleWare = [ReduxThunk]
const store = createStore(
reducers,
{},
compose(applyMiddleware(...middleWare))
)
// you can define more parameters, like blacklist or whitelist a reducer
// also, specify storage engine
persistStore(store, { storage: AsyncStorage });
export default store;
At your app's entry point,
import React, { Component } from "react";
import { Provider } from "react-redux";
import Router from "./Router";
import store from './store';
export default class App extends Component {
constructor(props) {
super(props);
}
render() {
return (
<Provider store={store}>
<Router /> // navigator
</Provider>
);
}
}
Finally, your API logic.
// action creator
export storePhoto = photos => {
return {
type: 'STORE_PHOTOS',
payload: photos
}
}
// photos reducer
import { REHYDRATE } from 'redux-persist/constants';
export default (state = {}, action) => {
switch (action.type) {
case STORE_PHOTOS:
return { ...state, photos: action.payload }
// this is where `redux-persist` handles caching
case REHYDRATE:
var incoming = action.payload;
if(incoming) return { ...state, ...incoming }
return state;
default:
return state;
}
};
To retrieve data, you will see that redux abstracts away all the excess logics and there is no more setItem, getItem because redux-persist does that automagically for your already.
import { connect } from "react-redux";
import { storePhotos } from "./actions";
class GalleryList extends Component {
async componentDidMount() {
const photos = await axios.get('http://jsonplaceholder.typicode.com/photos');
storePhoto(photos)
}
renderPhoto = ({ item }) => <GalleryDetail photo={item}/>
keyExtractor = (photo, index) => photo.id;
render () {
return (
<FlatList
data = {this.props.photos}
keyExtractor={this.keyExtractor}
renderItem={this.renderPhoto}
/>
);
}
}
// pull data from photos reducer
const mapStateToProps = ({ photos }) => {
return {
photos: photos.photos
}
}
export default connect(mapStateToProps, { storePhotos })(GalleryList);
To summarise,
Install redux-persist in you project.
Import persistStore and autoRehydrate form redux-persist.
Add autoRehydrate to your store.
Pass your store to persistStore.
Listen to the persist/REHYDRATE action on your reducer and populate state accordingly.
Hope my answer helps!
Answer
Caching the data for a specific period of time
const cacheIntervaInHours = 24
const cacheExpiryTime = new Date()
cacheExpiryTime.setHours(cacheExpiryTime.getHours() + cacheIntervalInHours)
const lastRequest = await AsyncStorage.getItem("lastRequest")
if (lastRequest == null || lastRequest > cacheExpiryTime) {
fetch(`${apiUrl}/blogPosts/recent`)
.then(async (response) => {
return await response.json()
})
.then(async (json) => {
if (!json || json.length == 0) {
throw new Error()
}
AsyncStorage.setItem("lastRequest", new Date());
return await AsyncStorage.setItem('blogPosts', JSON.stringify(json))
})
.catch(error => {
console.error(error)
})
}