I'm using Expo together with react-native-maps for a rather simple map component. It works fine on iOS, however on Android I get the following error:
abi30_0_0.com.facebook.react.bridge.ReadableNativeMap cannot be cast to java.lang.String
getString
ReadableNativeMap.java:168
showAlert
DialogModule.java:247
invoke
Method.java
invoke
JavaMethodWrapper.java:372
invoke
JavaModuleWrapper.java:160
run
NativeRunnable.java
handleCallback
Handler.java:790
dispatchMessage
Handler.java:99
dispatchMessage
MessageQueueThreadHandler.java:29
loop
Looper.java:164
run
MessageQueueThreadImpl.java:192
run
Thread.java:764
Here is my map component (sorry it's a bit longer):
import React from 'react';
import {
StyleSheet,
View,
Dimensions,
Alert,
TouchableOpacity,
Text,
Platform,
} from 'react-native';
import {
MapView,
Location,
Permissions,
Constants,
} from 'expo';
import { Ionicons } from '#expo/vector-icons';
import axios from 'axios';
import geolib from 'geolib';
import Polyline from '#mapbox/polyline';
import api from '../helpers/api';
import appConfig from '../app.json';
const { width, height } = Dimensions.get('window');
class MapScreen extends React.Component {
static navigationOptions = {
title: 'Map',
};
constructor(props) {
super(props);
this.state = {
coordinates: [],
focusedLocation: {
latitude: 0,
longitude: 0,
latitudeDelta: 0.0122,
longitudeDelta: width / height * 0.0122,
},
destinationReached: false,
isMapReady: false,
};
this.apikey = appConfig.expo.android.config.googleMaps.apiKey;
// bind this in constructor so state can be set in these methods
this.getLocation = this.getLocation.bind(this);
this.getDirections = this.getDirections.bind(this);
this.checkUserLocation = this.checkUserLocation.bind(this);
this.animateToCoordinates = this.animateToCoordinates.bind(this);
}
async componentDidMount() {
// ask the user for location permission
if (Platform.OS === 'android' && !Constants.isDevice) {
Alert.alert('Warning', 'This will not work on sketch in an android emulator. Try it on your device!');
return;
}
if (await !this.isPermissionGranted(Permissions.LOCATION)) {
Alert.alert('Permission', 'You need to enable location services');
return;
}
// get the current location of the user
// retrieve the destination location where the users shift will start
const [currentLocation, destinationLocation] = await Promise.all([
this.getLocation(),
this.getInterceptionCoords(),
]);
// retrieve a direction between these two points
this.getDirections(currentLocation, destinationLocation);
// monitor the current position of the user
this.watchid = await Location.watchPositionAsync({
enableHighAccuracy: true,
distanceInterval: 1,
}, this.checkUserLocation);
}
componentWillUnmount() {
if (this.watchid) {
this.watchid.remove();
}
}
/**
* retrieve current coordinates and move to them on the map
* assumes that location permission has already been granted
* #returns {Promise<{latitude: (number|*|string), longitude: (number|*|string)}>}
*/
async getLocation() {
// get current position if permission has been granted
const { coords } = await Location.getCurrentPositionAsync({
enableHighAccuracy: true,
});
// initalize map at current position
this.animateToCoordinates(coords);
this.setState(prevState => {
return {
focusedLocation: {
...prevState.focusedLocation,
latitude: coords.latitude,
longitude: coords.longitude,
},
};
});
return {
latitude: coords.latitude,
longitude: coords.longitude,
};
}
/**
* retrieves the coordinates of a route
* route: safety drivers position to the interception point
* #param startLoc
* #param destinationLoc
* #returns {Promise<*>}
*/
async getDirections(startLoc, destinationLoc) {
try {
const response = await axios({
method: 'GET',
url: 'https://maps.googleapis.com/maps/api/directions/json',
params: {
origin: Object.values(startLoc).join(','),
destination: Object.values(destinationLoc).join(','),
key: this.apikey,
},
responseType: 'json',
headers: {},
});
if (response.status !== 200) {
// this will execute the catch block
throw new Error('Fetching the coordinates of the interception point failed');
}
const { data } = response;
if (data.status !== 'OK') {
throw new Error('Determining a route between the two points failed');
}
const points = Polyline.decode(data.routes[0].overview_polyline.points);
const coordinates = points.map(point => {
return {
latitude: point[0],
longitude: point[1],
};
});
this.setState({ coordinates: coordinates });
return coordinates;
} catch (error) {
console.log(error);
Alert.alert('Network error', error);
return error;
}
}
/**
* get the coordinates of the interception point
* #returns {Promise<*>}
*/
async getInterceptionCoords() {
try {
const response = await api.get('/shifts/next');
if (response.status !== 200) {
// this will execute the catch block
throw new Error('Fetching the coordinates of the interception point failed');
}
const { data } = response;
return {
latitude: data.latStart,
longitude: data.longStart,
};
} catch (error) {
console.log(error);
Alert.alert('Network error', error);
return error;
}
}
checkUserLocation(location) {
const { coordinates } = this.state;
const { coords } = location;
if (Platform.OS === 'android') {
// follow the user location
// mapview component handles this for ios devices
this.animateToCoordinates(coords);
}
const destinationCoords = coordinates[coordinates.length - 1];
const distance = geolib.getDistance(coords, destinationCoords);
if (distance <= 20) {
// distance to destination is shorter than 20 metres
// show button so user can confirm arrival
this.setState({ destinationReached: true });
} else {
// remove arrival button in case the user moves away from the destination
this.setState({ destinationReached: false });
}
}
/**
* animate to specified coordinates on the map
* #param coords
*/
animateToCoordinates(coords) {
const { focusedLocation } = this.state;
const { latitude, longitude } = coords;
if (focusedLocation && latitude && longitude) {
this.map.animateToRegion({
...focusedLocation,
latitude: latitude,
longitude: longitude,
});
}
}
renderConfirmalButton() {
const { destinationReached } = this.state;
if (!destinationReached) {
return null;
}
return (
<View style={styles.confirmContainer}>
<TouchableOpacity
style={styles.confirmButton}
onPress={this.onArrivalConfirmed}
>
<View style={styles.drawerItem}>
<Ionicons
name="ios-checkmark-circle-outline"
size={30}
color="#ffffff"
style={styles.drawerItemIcon}
/>
<Text style={styles.buttonText}>Confirm Arrival</Text>
</View>
</TouchableOpacity>
</View>
);
}
isPermissionGranted = async permission => {
const { status } = await Permissions.askAsync(permission);
return (status === 'granted');
};
onArrivalConfirmed = () => {
Alert.alert('Confirmation', 'Arrival confirmed');
};
onMapReady = () => {
this.setState({ isMapReady: true });
};
render() {
const { coordinates, focusedLocation, isMapReady } = this.state;
return (
<View style={styles.container}>
<MapView
style={styles.map}
initialRegion={focusedLocation}
showsUserLocation
followsUserLocation={Platform.OS === 'ios'}
loadingEnabled
ref={map => { this.map = map; }}
onMapReady={() => this.onMapReady()}
>
<MapView.Polyline
coordinates={coordinates}
strokeWidth={3}
strokeColor="blue"
/>
{isMapReady && coordinates.length > 0 && (
<MapView.Marker
coordinate={coordinates[coordinates.length - 1]}
/>
)}
</MapView>
{this.renderConfirmalButton()}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
map: {
width: width,
height: height,
},
confirmContainer: {
position: 'absolute',
left: 0,
bottom: 0,
height: 150,
width: '100%',
justifyContent: 'center',
},
confirmButton: {
paddingHorizontal: 30,
},
drawerItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
padding: 10,
backgroundColor: 'lightblue',
borderRadius: 15,
},
drawerItemIcon: {
marginRight: 10,
},
buttonText: {
color: '#ffffff',
fontSize: 22,
},
});
export default MapScreen;
Any help is greatly appreciated!
Related
I'm trying to get React Native to whistle 3 times before a timer, so for example, whistle 3 seconds in a row, then let the timer go, then whistle again, but for some reason it is only doing it twice, it's skipping the middle whistle and sometimes the last one.
I've tried mounting the sound before hand, reducing the sound duration to about .3 seconds, and it is still skipping some plays. I know I need to do some refactor on the timers, but I think at least playing the sound should work.
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Dimensions,
Vibration,
} from "react-native";
import React from "react";
import { StatusBar } from "expo-status-bar";
import { Audio } from "expo-av";
const screen = Dimensions.get("window");
let timeout: NodeJS.Timeout | undefined = undefined;
interface TimerComponentProps {
timeInSeconds?: number;
}
export const TimerComponent: React.FC<TimerComponentProps> = ({
timeInSeconds = 5,
}) => {
const [remaningSeconds, setRemainingSeconds] = React.useState(timeInSeconds);
const [isActive, setIsActive] = React.useState(false);
const [sound, setSound] = React.useState<Audio.Sound | undefined>(undefined);
const [shouldCount, setShouldCount] = React.useState(false);
const [counter, setCounter] = React.useState(3);
const { minutes, seconds } = React.useMemo(() => {
const minutes = Math.floor(remaningSeconds / 60);
const seconds = remaningSeconds % 60;
return { minutes, seconds };
}, [remaningSeconds]);
async function mountSound() {
try {
const { sound } = await Audio.Sound.createAsync(
require("../../assets/audio/Whistle.wav")
);
setSound(sound);
} catch (error) {
console.error(error);
}
}
async function playWhistle() {
if (sound) {
try {
await sound.playAsync();
} catch (error) {
console.error(error);
}
}
}
const endTimer = async () => {
try {
await playWhistle();
setIsActive(false);
} catch (error) {
console.error(error);
}
};
const startCounter = async () => {
await mountSound();
setShouldCount(true);
};
const resetTimer = () => {
if (timeout) {
clearTimeout(timeout);
} else {
timeout = setTimeout(() => {
setRemainingSeconds(timeInSeconds);
clearTimeout(timeout);
}, 1000);
}
};
React.useEffect(() => {
let counterInterval: NodeJS.Timer | undefined = undefined;
if (shouldCount) {
counterInterval = setInterval(() => {
try {
if (counter === 1) {
setCounter((counter) => counter - 1);
}
if (counter > 1) {
playWhistle();
Vibration.vibrate();
setCounter((counter) => counter - 1);
} else {
// Plays the whistle sound and vibrates the device
playWhistle();
Vibration.vibrate();
// Restarts the counter
setCounter(3);
setShouldCount(false);
// Starts the timer
setIsActive(true);
// Stops the counter
clearInterval(counterInterval);
}
} catch (error) {
console.error(error);
}
}, 1000);
} else if (!shouldCount && counter !== 0) {
clearInterval(counterInterval);
}
return () => clearInterval(counterInterval);
}, [shouldCount, counter]);
React.useEffect(() => {
let timerInterval: NodeJS.Timer | undefined = undefined;
if (isActive) {
timerInterval = setInterval(() => {
if (remaningSeconds === 1) {
setRemainingSeconds((remaningSeconds) => remaningSeconds - 1);
}
if (remaningSeconds > 1) {
setRemainingSeconds((remaningSeconds) => remaningSeconds - 1);
} else {
Vibration.vibrate();
endTimer();
resetTimer();
}
}, 1000);
} else if (!isActive && remaningSeconds === 0) {
resetTimer();
clearInterval(timerInterval);
}
return () => clearInterval(timerInterval);
}, [isActive, remaningSeconds]);
React.useEffect(() => {
return sound
? () => {
sound.unloadAsync();
setSound(undefined);
}
: undefined;
}, [sound]);
const parseTime = (time: number) => {
return time < 10 ? `0${time}` : time;
};
return (
<View style={styles.container}>
<StatusBar style="light" />
<Text style={styles.timerText}>{`${parseTime(minutes)}:${parseTime(
seconds
)}`}</Text>
<TouchableOpacity onPress={startCounter} style={styles.button}>
<Text style={styles.buttonText}>{isActive ? "Pause" : "Start"}</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#07121B",
alignItems: "center",
justifyContent: "center",
},
button: {
borderWidth: 10,
borderColor: "#B9AAFF",
width: screen.width / 2,
height: screen.width / 2,
borderRadius: screen.width / 2,
alignItems: "center",
justifyContent: "center",
},
buttonText: {
color: "#B9AAFF",
fontSize: 20,
},
timerText: {
color: "#fff",
fontSize: 90,
},
});
The issue was that expo-av leaves the audio file at it's end, so the next time you play it, nothing will sound because the file is already over, the way to fix it is pretty simple:
async function playWhistle() {
if (sound) {
try {
await sound.playAsync();
sound.setPositionAsync(0); // ADD THIS LINE
} catch (error) {
console.error(error);
}
}
}
I am using react native 0.68.5
When I call the renderReusableTextInput method in Main.js I am sending all the styles including the needed and unneeded styles. This can lead performance penalty.
So, is there a way that I can send the only needed styles to the renderReusableTextInput method.
I also want to know is the code reusing approach of mine correct or there are better ways.
I have the following code-logic structure:
(i) A Reusable component; It has only structure no style or state
(ii) A file that includes reusable methods
(iii) The Main component, which contains components, styles and states
ReusableTextInput.js
// import ...
const ReusableTextInput = forwardRef(({
inputName,
inputLabel,
inputValue,
secureTextEntry,
textInputContainerWarperStyle,
textInputContainerStyle,
textInputLabelStyle,
textInputStyle,
textInputHelperStyle,
textInputErrorStyle,
helperText,
inputError,
onFocus,
onChangeText,
onBlur,
}, inputRef) => {
return (
<View style={[textInputContainerWarperStyle]}>
<View style={[textInputContainerStyle]}>
<Text style={[textInputLabelStyle]}>
{inputLabel}
</Text>
<TextInput
ref={(elm) => inputRef[inputName] = elm}
style={[textInputStyle]}
value={inputValue}
secureTextEntry={secureTextEntry}
onChangeText={onChangeText}
onFocus={onFocus}
onBlur={onBlur}
/>
</View>
{
((inputRef[inputName]) && (inputRef[inputName].isFocused()) && (!inputValue))
?
<Text style={[textInputHelperStyle]}>
{helperText}
</Text>
:
null
}
{
((inputError) && (inputValue))
?
<Text style={[textInputErrorStyle]}>
{inputError}
</Text>
:
null
}
</View>
);
});
export default memo(ReusableTextInput);
reusableMethods.js
// import ...
const handleFocus = (state, setState, styles) => {
const stateData = { ...state };
stateData.styleNames.textInputContainer = {
...styles.textInputContainer,
...styles[`${stateData.name}ExtraTextInputContainer`],
...styles.textInputContainerFocus,
};
stateData.styleNames.textInputLabel = {
...styles.textInputLabel,
...styles[`${stateData.name}ExtraTextInputLabel`],
...styles.textInputLabelFocus,
};
stateData.styleNames.textInput = {
...styles.textInput,
...styles[`${stateData.name}ExtraTextInput`],
...styles.textInputFocus,
};
// other logics...
setState(stateData);
};
const handleChangeText = (state, setState, text) => {
const stateData = { ...state };
// individual validation
const schemaData = Joi.object().keys(stateData.validationObj); // I used Joi for validation
const inputData = { [stateData.name]: text };
const options = { abortEarly: false, errors: { label: false } };
const result = schemaData.validate(inputData, options);
// -----
stateData.error = (result.error) ? result.error.details[0].message : '';
stateData.value = text;
// other logics...
setState(stateData);
};
const handleBlur = (state, setState, styles) => {
const stateData = { ...state };
if (stateData.value) {
stateData.styleNames.textInputContainer = {
...styles.textInputContainer,
...styles[`${stateData.name}ExtraTextInputContainer`],
...styles.textInputContainerFocus,
...styles[`${stateData.name}ExtraTextInputContainerFocus`],
...styles.textInputContainerBlurText,
...styles[`${stateData.name}ExtraTextInputContainerBlurText`],
};
stateData.styleNames.textInputLabel = {
...styles.textInputLabel,
...styles[`${stateData.name}ExtraTextInputLabel`],
...styles.textInputLabelFocus,
...styles[`${stateData.name}ExtraTextInputLabelFocus`],
...styles.textInputLabelBlurText,
...styles[`${stateData.name}ExtraTextInputLabelBlurText`],
};
stateData.styleNames.textInput = {
...styles.textInput,
...styles[`${stateData.name}ExtraTextInput`],
...styles.textInputFocus,
...styles[`${stateData.name}ExtraTextInputFocus`],
...styles.textInputBlurText,
...styles[`${stateData.name}ExtraTextInputBlurText`],
};
}
else {
stateData.styleNames.textInputContainer = { ...styles.textInputContainer, ...styles[`${stateData.name}ExtraTextInputContainer`] };
stateData.styleNames.textInputLabel = { ...styles.textInputLabel, ...styles[`${stateData.name}ExtraTextInputLabel`] };
stateData.styleNames.textInput = { ...styles.textInput, ...styles[`${stateData.name}ExtraTextInput`] };
}
// other logics...
setState(stateData);
};
// other methods...
export const renderReusableTextInput = (
state,
setState,
inputRef,
styles,
// contains all the styles from Main component and here I am sending all the styles including the needed and unneeded styles. I want improvement here
) => {
return (
<ReusableTextInput
inputName={state.name}
inputLabel={state.label}
inputValue={state.value}
inputRef={inputRef}
secureTextEntry={state.secureTextEntry}
textInputContainerWarperStyle={{...styles.textInputContainerWarper, ...styles[`${state.name}ExtraTextInputContainerWarper`]}}
textInputContainerStyle={state.styleNames.textInputContainer}
textInputLabelStyle={state.styleNames.textInputLabel}
textInputStyle={state.styleNames.textInput}
textInputHelperStyle={{...styles.textInputHelper, ...styles[`${state.name}ExtraTextInputHelper`]}}
textInputErrorStyle={{...styles.textInputError, ...styles[`${state.name}ExtraTextInputError`]}}
helperText={state.helperText}
inputError={state.error}
onFocus={() => handleFocus(state, setState, styles)}
onChangeText={(text) => handleChangeText(state, setState, text)}
onBlur={() => handleBlur(state, setState, styles)}
/>
);
};
Main.js
// import Joi from 'joi';
// import { joiPasswordExtendCore } from 'joi-password';
// import { renderReusableTextInput } from ''; ...
const schema =
{
email: Joi.string().strict()
.case("lower")
.min(5)
.max(30)
.email({ minDomainSegments: 2, tlds: { allow: ["com", "net", "org"] } })
.required(),
countryCode: // Joi.string()...,
phoneNumber: // Joi.string()...,
password: // Joi.string()...,
// ...
};
const Main = () => {
const { width: windowWidth, height: windowHeight, scale, fontScale } = useWindowDimensions();
const minimumWidth = (windowWidth <= windowHeight) ? windowWidth : windowHeight;
const styles = useMemo(() => currentStyles(minimumWidth), [minimumWidth]);
const [email, setEmail] = useState({
name: 'email', // unchangeable
label: 'Email', // unchangeable
value: '',
error: '',
validationObj: { email: schema.email }, // unchangeable
trailingIcons: [require('../../file/image/clear_trailing_icon.png')], // unchangeable
helperText: 'only .com, .net and .org allowed', // unchangeable
styleNames: {
textInputContainer: { ...styles.textInputContainer, ...styles.emailExtraTextInputContainer },
textInputLabel: { ...styles.textInputLabel, ...styles.emailExtraTextInputLabel },
textInput: { ...styles.textInput, ...styles.emailExtraTextInput },
},
});
const [phoneNumber, setPhoneNumber] = useState({
// ...
});
const [countryCode, setCountryCode] = useState({
});
const [password, setPassword] = useState({
});
// ...
const references = useRef({});
return (
<View style={[styles.mainContainer]}>
{
useMemo(() => renderReusableTextInput(email, setEmail, references.current, styles), [email, minimumWidth])
}
</View>
);
}
export default memo(Main);
const styles__575 = StyleSheet.create({
// 320 to 575
mainContainer: {
},
textInputContainerWarper: {
},
emailExtraTextInputContainerWarper: {
},
countryCodeExtraTextInputContainerWarper: {
},
phoneNumberExtraTextInputContainerWarper: {
},
passwordExtraTextInputContainerWarper: {
},
textInputContainer: {
},
emailExtraTextInputContainer: {
},
textInputContainerFocus: {
},
textInputContainerBlurText: {
},
textInputLabel: {
},
emailExtraTextInputLabel: {
},
textInputLabelFocus: {
},
textInputLabelBlurText: {
},
textInput: {
},
emailExtraTextInput: {
},
textInputFocus: {
},
textInputBlurText: {
},
textInputHelper: {
},
emailExtraTextInputHelper: {
},
textInputError: {
},
emailExtraTextInputError: {
},
// other styles...
});
const styles_576_767 = StyleSheet.create({
// 576 to 767
});
const styles_768_ = StyleSheet.create({
// 768; goes to 1024;
});
const currentStyles = (width, stylesInitial = { ...styles__575 }, styles576 = { ...styles_576_767 }, styles768 = { ...styles_768_ }) => {
let styles = {};
if (width < 576) {
// ...
}
else if ((width >= 576) && (width < 768)) {
// ...
}
else if (width >= 768) {
// ...
}
return styles;
};
I tried mentioned approach and want a better answer.
First, I think you want global styles, so that you can access it from anywhere and you can also be able to execute needed code.
Make sure you use useMemo, useCallback in right manner, for better performance.
Move all Schema and Styles and Methods of your screens inside the reusableMethods.js file (at most case). It will act like the controller of all screens of your app, also make a demo method which return a tiny component and this method execute another method, so that you can get styles for different dimentions(see below code)
Store only changeable and needed properties in state variables.
Is code reusing approach of yours, correct? I can't say about that. I will say that it depends on developer choice.
you can try like below:
reusableMethods.js
// import all schema, style variants, utility methods and other
let screenStyles = {};
const executeDimensionBasedMethods = (width, screenName) => {
if (screenName === 'a_screen_name') screenStyles[screenName] = currentStyles(width, otherParameter);
// else if()
// ...
};
export const renderDimension = (width, screenName) => {
executeDimensionBasedMethods(width, screenName);
return (
<Text style={{ width: 0, height: 0 }}></Text>
);
};
// all method definitions and logics
export const renderReusableTextInput = (
state,
setState,
inputRef,
screenName
) => {
return (
<ReusableTextInput
inputName={state.name}
inputLabel={state.label}
inputValue={state.value}
inputRef={inputRef}
secureTextEntry={state.secureTextEntry}
textInputContainerWarperStyle={{ ...screenStyles[screenName].textInputContainerWarper, ...screenStyles[screenName][`${state.name}ExtraTextInputContainerWarper`] }}
textInputContainerStyle={state.styleNames.textInputContainer || { ...screenStyles[screenName].textInputContainer }
textInputLabelStyle={state.styleNames.textInputLabel || { ...screenStyles[screenName].textInputLabel }
textInputStyle={state.styleNames.textInput || { ...screenStyles[screenName].textInput }
textInputHelperStyle={{ ...screenStyles[screenName].textInputHelper, ...screenStyles[screenName][`${state.name}ExtraTextInputHelper`] }}
textInputErrorStyle={{ ...screenStyles[screenName].textInputError, ...screenStyles[screenName][`${state.name}ExtraTextInputError`] }}
helperText={state.helperText}
inputError={state.error}
onFocus={() => handleFocus(state, setState, screenStyles[screenName])}
onChangeText={(text) => handleChangeText(state, setState, text)}
onBlur={() => handleBlur(state, setState, screenStyles[screenName])}
/>
);
};
Main.js
const Main = () => {
const { width: windowWidth, height: windowHeight} = useWindowDimensions();
const minimumWidth = (windowWidth <= windowHeight) ? windowWidth : windowHeight;
const [email, setEmail] = useState({
name: 'email', // unchangeable
label: 'Email', // unchangeable
value: '',
error: '',
trailingIcons: [require('../../file/image/clear_trailing_icon.png')], // unchangeable
helperText: 'only .com, .net and .org allowed', // unchangeable
styleNames: {
textInputContainer: undefined,
textInputLabel: undefined,
textInput: undefined,
},
});
// other states
const references = useRef({});
return (
<>
{
useMemo(() => renderDimension(minimumWidth), [minimumWidth])
}
<View style={{ // inline style}}>
{
useMemo(() => renderReusableTextInput(email, setEmail, references.current, 'nameofscreen'), [email, minimumWidth])
}
</View>
</>
}
export default memo(Main);
I want to create my own Endomono/Runtastic-like app using RN + expo (This app will be just for me, and I have android phone with pretty decent performance/battery life (Redmi note 7) so I don't worry about performance too much). I wanted to use all-in-one library for that, or just and library that allows me to execute some code each X seconds in background (and getAsyncLocation there). My point is just to send lat/lon data every X seconds to my backend HTTP django-rest-framework powered server.
I just spent whole day trying figure out any way to do that, I tried couple of libraries like this ones: react-native-background-geolocation, react-native-background-timer, react-native-background-job and few more. I followed step by step instalation guide, and I kept getting errors like: null is not an object (evaluating 'RNBackgroundTimer.setTimeout') .
I also tried this: I fixed some errors in this code (imports related), it seemed to work, but when I changed my GPS location using Fake GPS, and only one cast of didFocus functions appears in the console. Here's code:
import React from 'react';
import { EventEmitter } from 'fbemitter';
import { NavigationEvents } from 'react-navigation';
import { AppState, AsyncStorage, Platform, StyleSheet, Text, View, Button } from 'react-native';
import MapView from 'react-native-maps';
import * as Permissions from 'expo-permissions';
import * as Location from 'expo-location';
import * as TaskManager from 'expo-task-manager';
import { FontAwesome, MaterialIcons } from '#expo/vector-icons';
const STORAGE_KEY = 'expo-home-locations';
const LOCATION_UPDATES_TASK = 'location-updates';
const locationEventsEmitter = new EventEmitter();
export default class MapScreen extends React.Component {
static navigationOptions = {
title: 'Background location',
};
mapViewRef = React.createRef();
state = {
accuracy: 4,
isTracking: false,
showsBackgroundLocationIndicator: false,
savedLocations: [],
initialRegion: null,
error: null,
};
didFocus = async () => {
console.log("Hello")
let { status } = await Permissions.askAsync(Permissions.LOCATION);
if (status !== 'granted') {
AppState.addEventListener('change', this.handleAppStateChange);
this.setState({
error:
'Location permissions are required in order to use this feature. You can manually enable them at any time in the "Location Services" section of the Settings app.',
});
return;
} else {
this.setState({ error: null });
}
const { coords } = await Location.getCurrentPositionAsync();
console.log(coords)
const isTracking = await Location.hasStartedLocationUpdatesAsync(LOCATION_UPDATES_TASK);
const task = (await TaskManager.getRegisteredTasksAsync()).find(
({ taskName }) => taskName === LOCATION_UPDATES_TASK
);
const savedLocations = await getSavedLocations();
const accuracy = (task && task.options.accuracy) || this.state.accuracy;
this.eventSubscription = locationEventsEmitter.addListener('update', locations => {
this.setState({ savedLocations: locations });
});
if (!isTracking) {
alert('Click `Start tracking` to start getting location updates.');
}
this.setState({
accuracy,
isTracking,
savedLocations,
initialRegion: {
latitude: coords.latitude,
longitude: coords.longitude,
latitudeDelta: 0.004,
longitudeDelta: 0.002,
},
});
};
handleAppStateChange = nextAppState => {
if (nextAppState !== 'active') {
return;
}
if (this.state.initialRegion) {
AppState.removeEventListener('change', this.handleAppStateChange);
return;
}
this.didFocus();
};
componentWillUnmount() {
if (this.eventSubscription) {
this.eventSubscription.remove();
}
AppState.removeEventListener('change', this.handleAppStateChange);
}
async startLocationUpdates(accuracy = this.state.accuracy) {
await Location.startLocationUpdatesAsync(LOCATION_UPDATES_TASK, {
accuracy,
showsBackgroundLocationIndicator: this.state.showsBackgroundLocationIndicator,
});
if (!this.state.isTracking) {
alert(
'Now you can send app to the background, go somewhere and come back here! You can even terminate the app and it will be woken up when the new significant location change comes out.'
);
}
this.setState({ isTracking: true });
}
async stopLocationUpdates() {
await Location.stopLocationUpdatesAsync(LOCATION_UPDATES_TASK);
this.setState({ isTracking: false });
}
clearLocations = async () => {
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify([]));
this.setState({ savedLocations: [] });
};
toggleTracking = async () => {
await AsyncStorage.removeItem(STORAGE_KEY);
if (this.state.isTracking) {
await this.stopLocationUpdates();
} else {
await this.startLocationUpdates();
}
this.setState({ savedLocations: [] });
};
onAccuracyChange = () => {
const next = Location.Accuracy[this.state.accuracy + 1];
const accuracy = next ? Location.Accuracy[next] : Location.Accuracy.Lowest;
this.setState({ accuracy });
if (this.state.isTracking) {
// Restart background task with the new accuracy.
this.startLocationUpdates(accuracy);
}
};
toggleLocationIndicator = async () => {
const showsBackgroundLocationIndicator = !this.state.showsBackgroundLocationIndicator;
this.setState({ showsBackgroundLocationIndicator }, async () => {
if (this.state.isTracking) {
await this.startLocationUpdates();
}
});
};
onCenterMap = async () => {
const { coords } = await Location.getCurrentPositionAsync();
const mapView = this.mapViewRef.current;
if (mapView) {
mapView.animateToRegion({
latitude: coords.latitude,
longitude: coords.longitude,
latitudeDelta: 0.004,
longitudeDelta: 0.002,
});
}
};
renderPolyline() {
const { savedLocations } = this.state;
if (savedLocations.length === 0) {
return null;
}
return (
<MapView.Polyline
coordinates={savedLocations}
strokeWidth={3}
strokeColor={"black"}
/>
);
}
render() {
if (this.state.error) {
return <Text style={styles.errorText}>{this.state.error}</Text>;
}
if (!this.state.initialRegion) {
return <NavigationEvents onDidFocus={this.didFocus} />;
}
return (
<View style={styles.screen}>
<MapView
ref={this.mapViewRef}
style={styles.mapView}
initialRegion={this.state.initialRegion}
showsUserLocation>
{this.renderPolyline()}
</MapView>
<View style={styles.buttons} pointerEvents="box-none">
<View style={styles.topButtons}>
<View style={styles.buttonsColumn}>
{Platform.OS === 'android' ? null : (
<Button style={styles.button} onPress={this.toggleLocationIndicator} title="background/indicator">
<Text>{this.state.showsBackgroundLocationIndicator ? 'Hide' : 'Show'}</Text>
<Text> background </Text>
<FontAwesome name="location-arrow" size={20} color="white" />
<Text> indicator</Text>
</Button>
)}
</View>
<View style={styles.buttonsColumn}>
<Button style={styles.button} onPress={this.onCenterMap} title="my location">
<MaterialIcons name="my-location" size={20} color="white" />
</Button>
</View>
</View>
<View style={styles.bottomButtons}>
<Button style={styles.button} onPress={this.clearLocations} title="clear locations">
Clear locations
</Button>
<Button style={styles.button} onPress={this.toggleTracking} title="start-stop tracking">
{this.state.isTracking ? 'Stop tracking' : 'Start tracking'}
</Button>
</View>
</View>
</View>
);
}
}
async function getSavedLocations() {
try {
const item = await AsyncStorage.getItem(STORAGE_KEY);
return item ? JSON.parse(item) : [];
} catch (e) {
return [];
}
}
if (Platform.OS !== 'android') {
TaskManager.defineTask(LOCATION_UPDATES_TASK, async ({ data: { locations } }) => {
if (locations && locations.length > 0) {
const savedLocations = await getSavedLocations();
const newLocations = locations.map(({ coords }) => ({
latitude: coords.latitude,
longitude: coords.longitude,
}));
savedLocations.push(...newLocations);
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(savedLocations));
locationEventsEmitter.emit('update', savedLocations);
}
});
}
const styles = StyleSheet.create({
screen: {
flex: 1,
},
mapView: {
flex: 1,
},
buttons: {
flex: 1,
flexDirection: 'column',
justifyContent: 'space-between',
padding: 10,
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
},
topButtons: {
flexDirection: 'row',
justifyContent: 'space-between',
},
bottomButtons: {
flexDirection: 'column',
alignItems: 'flex-end',
},
buttonsColumn: {
flexDirection: 'column',
alignItems: 'flex-start',
},
button: {
paddingVertical: 5,
paddingHorizontal: 10,
marginVertical: 5,
},
errorText: {
fontSize: 15,
color: 'rgba(0,0,0,0.7)',
margin: 20,
},
});
If you know any way to easily complete my target (of sending simple HTTP GET with location from background of Expo + RN app to my DRF backend) please let me know.
If you're using Expo you can simply use expo-task-manager and expo-location to get background location updates.
Here's a simplified version that I'm using (and it's working for sure on Android) on the App I'm currently developing:
import * as Location from 'expo-location';
import * as TaskManager from 'expo-task-manager';
import axios from 'axios';
const TASK_FETCH_LOCATION = 'TASK_FETCH_LOCATION';
// 1 define the task passing its name and a callback that will be called whenever the location changes
TaskManager.defineTask(TASK_FETCH_LOCATION, async ({ data: { locations }, error }) => {
if (error) {
console.error(error);
return;
}
const [location] = locations;
try {
const url = `https://<your-api-endpoint>`;
await axios.post(url, { location }); // you should use post instead of get to persist data on the backend
} catch (err) {
console.error(err);
}
});
// 2 start the task
Location.startLocationUpdatesAsync(TASK_FETCH_LOCATION, {
accuracy: Location.Accuracy.Highest,
distanceInterval: 1, // minimum change (in meters) betweens updates
deferredUpdatesInterval: 1000, // minimum interval (in milliseconds) between updates
// foregroundService is how you get the task to be updated as often as would be if the app was open
foregroundService: {
notificationTitle: 'Using your location',
notificationBody: 'To turn off, go back to the app and switch something off.',
},
});
// 3 when you're done, stop it
Location.hasStartedLocationUpdatesAsync(TASK_FETCH_LOCATION).then((value) => {
if (value) {
Location.stopLocationUpdatesAsync(TASK_FETCH_LOCATION);
}
});
It doesn't necessarily work with Expo, but if "eject" your project or start with the React Native CLI (via react-native init) then you could use an Android specific React Native "NativeModule" to accomplish your goal. I like using the react-native-location package, which has great support on iOS for background location updates, but on Android there is a bug currently. I put together an example project which has the necessary Android specific code inside a NativeModule you could use to start from:
https://github.com/andersryanc/ReactNative-LocationSample
How to remove network request failed error screen and display message "No internet connection" for better user experience in react-native when there is no internet connection.
You can use NetInfo in React-Native to check network state. This is link:
https://facebook.github.io/react-native/docs/netinfo.html
This is example code:
NetInfo.getConnectionInfo().then((connectionInfo) => {
if (connectionInfo.type === 'none') {
alert("No internet connection")
} else {
// online
// do something
}
});
Here i wrote an component for handling internet status issues refer this:
import React, { Component } from "react";
import {
View,
NetInfo,
Animated,
Easing,
Dimensions,
Platform,
AppState
} from "react-native";
// import { colors, Typography, primaryFont } from "../../Config/StylesConfig";
import { connect } from "react-redux";
import { changeConnectionStatus } from "../../actions/authActions";
import CustomText from "./CustomText";
import firebaseHelper from "../../helpers/firebaseHelper";
const { width } = Dimensions.get("window");
class InternetStatusBar extends Component {
constructor(props) {
super(props);
this.state = {
isNetworkConnected: true
};
this._updateConnectionStatus = this._updateConnectionStatus.bind(this);
this.positionValue = new Animated.Value(-26);
this.colorValue = new Animated.Value(0);
this.isMount = true;
this.isOnline = true;
}
_handleAppStateChange = nextAppState => {
if (nextAppState.match(/inactive|background/) && this.isOnline) {
firebaseHelper.goOffline();
// console.log("offline");
this.isOnline = false;
} else if (nextAppState === "active" && this.isOnline === false) {
firebaseHelper.goOnline();
// console.log("online");
this.isOnline = true;
}
};
componentDidMount() {
AppState.addEventListener("change", this._handleAppStateChange);
// NetInfo.isConnected.fetch().done(isNetworkConnected => {
// this._updateConnectionStatus(isNetworkConnected);
// });
NetInfo.isConnected.addEventListener(
"connectionChange",
this._updateConnectionStatus
);
}
componentWillUnmount() {
AppState.removeEventListener("change", this._handleAppStateChange);
NetInfo.isConnected.removeEventListener(
"connectionChange",
this._updateConnectionStatus
);
}
_updateConnectionStatus(isNetworkConnected) {
// this.setState({ isNetworkConnected });
if (this.isMount) {
this.isMount = false;
} else {
if (isNetworkConnected) {
this.animateColorChange(isNetworkConnected);
setTimeout(() => {
this.animateErrorView(isNetworkConnected);
}, 1000);
} else {
this.animateErrorView(isNetworkConnected);
this.colorValue = new Animated.Value(0);
}
}
// this.props.changeConnectionStatus(isNetworkConnected);
}
// componentWillReceiveProps = nextProps => {
// if (
// nextProps.isInternetConnected &&
// nextProps.isInternetConnected != this.state.isInternetConnected
// ) {
// const date = new Date();
// Actions.refresh({ refreshContent: date.getTime() });
// }
// };
animateErrorView(connected) {
Animated.timing(this.positionValue, {
toValue: connected ? -40 : Platform.OS === "ios" ? 20 : 0,
easing: Easing.linear,
duration: 600
}).start();
}
animateColorChange(connected) {
Animated.timing(this.colorValue, {
toValue: connected ? 150 : 0,
duration: 800
}).start();
}
render() {
return (
<Animated.View
style={[
{
position: "absolute",
backgroundColor: this.colorValue.interpolate({
inputRange: [0, 150],
outputRange: ["rgba(0,0,0,0.6)", "rgba(75, 181, 67, 0.8)"]
}),
zIndex: 1,
width: width,
top: 0,
transform: [{ translateY: this.positionValue }]
}
]}
>
<View
style={[
{
padding: 4,
flexDirection: "row",
flex: 1
}
]}
>
<CustomText
style={{
fontSize: 12,
textAlign: "center",
flex: 1
}}
>
{this.state.isInternetConnected ? "Back online" : "No connection"}
</CustomText>
</View>
</Animated.View>
);
}
}
const mapStateToProps = state => {
return {
isInternetConnected: state.user.isInternetConnected
};
};
export default connect(mapStateToProps, { changeConnectionStatus })(
InternetStatusBar
);
A Snackbar is a good way to convey this. Have a look at this library :
https://github.com/9gag-open-source/react-native-snackbar-dialog
Easy and Simple with good user experience
use "react-native-offline-status"
Reference:
https://github.com/rgabs/react-native-offline-status
Signed APK crash after launch, in logCat i got requiring unknown module 'React'
Debug application works fine, but in logCat i got >> Requiring module 'React' by name is only supported for debugging purposes and will BREAK IN PRODUCTION!
React v15.4.1, React native v0.39.2 ?
Sorry for my english
this is my index.android.js
import React from 'react';
import {AppRegistry} from 'react-native';
import myapp from './index_start.js';
AppRegistry.registerComponent('myapp', () => myapp);
and index_start.js
import React, { Component } from "react";
import {
StyleSheet,
AppRegistry,
Text,
Image,
View,
AsyncStorage,
NetInfo,
StatusBar,
Navigator,
Dimensions
} from 'react-native';
// Window dismensions
const { width, height } = Dimensions.get('window');
// Device infos
import DeviceInfo from 'react-native-device-info';
// Native SplashScreen
import SplashScreen from 'react-native-splash-screen';
// Spinner
import Spinner from 'react-native-spinkit';
// Models
import User from './model/UserModel';
// Json data for initial launch
var DB = require('./DB.json');
// Components
import Stage from './components/stage/stage.js'
import Player from './components/player/player.js'
import Settings from './components/settings/settings.js'
import House from './stages/house/house.js'
// LocalStorage key
var USER_KEY = 'user_key';
const routes = [
{name: 'loading'},
{name: 'stage', component: Stage},
{name: 'house', component: House},
{name: 'settings', component: Settings}
];
const _navigator = null;
export default class myapp extends Component {
constructor(props) {
super(props);
this.state = {
isConnected: false,
isLoading: true,
_navigator: null,
stages: null
}
}
componentWillMount() {
// check if connected
this._checkConnexionType();
}
componentDidMount() {
SplashScreen.hide();
this._loadInitialData();
}
componentDidUpdate() {
// console.log(this.state.stages)
if (!this.state.isLoading && this.state.stages !== null) {
_navigator.push({
name: 'stage',
passProps: {
data: this.state.stages
}
})
}
}
/**
* Load localStorage Data
*/
async _loadInitialData() {
// GET User LocalStorage
if (this.state.stages == null) {
var localData;
//AsyncStorage.removeItem(USER_KEY)
AsyncStorage.getItem(USER_KEY).then((data) => {
if (data !== null) {
var localData = JSON.parse(data);
// User.uuid = localData.uuid;
User.setStages(localData.stages)
this.setState({
'stages' : localData.stages
})
} else {
var storage = {};
storage.setUiid = DeviceInfo.getUniqueID();
storage.stages = DB.stages;
AsyncStorage.setItem(USER_KEY, JSON.stringify(storage));
this.setState({
'stages' : DB.stages
})
}
})
}
if (this.state.isConnected) {
// var rStages = this._loadRemoteStages();
// console.log(rStages);
}
// Change state
setTimeout((function() {
this.setState({
'isLoading': false
})
}).bind(this), 1500);
}
/**
* GET stages from remote DB
*/
async _loadRemoteStages() {
await fetch(API_URL)
.then((response) => response.json())
.then((responseJson) => {
console.log(responseJson)
return responseJson;
})
.catch((error) => {
console.error(error);
});
}
/**
* CHECK IF user is connected to Network
* SET bool to state isLoading
*/
_checkConnexionType() {
NetInfo.isConnected.fetch().then(response => {
this.setState({ isConnected: response})
})
}
_renderScene(route, navigator) {
_navigator = navigator;
if (route.name == 'loading') {
return (
<View style={styles.container}>
<StatusBar hidden={true} />
<Image
style={{width: width, height: height}}
source={require('./img/screen.jpg')}
/>
<View style={styles.loading}>
<Text style={styles.loadingText}>CHARGEMENT</Text>
<Spinner type="ThreeBounce" color={'#fff'}/>
</View>
</View>
)
} else if (route.name == 'stage') {
return (
<Stage navigator={_navigator} {...route.passProps}/>
)
} else if (route.name == 'player') {
return (
<House navigator={_navigator} {...route.passProps}}/>
)
} else if (route.name == 'settings') {
return (
<Settings navigator={_navigator} {...route.passProps}/>
)
}
}
render() {
return (
<Navigator
initialRoute={{name: 'loading'}}
configureScene={() => Navigator.SceneConfigs.FloatFromBottomAndroid}
renderScene={this._renderScene.bind(this)}
/>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
loading: {
flex: 1,
position: 'absolute',
bottom: 50,
left: 0,
right: 0,
alignItems: 'center',
},
loadingText:{
flex: 1,
fontFamily: 'CarterOne',
fontSize: 20,
color: '#fff'
}
});