7

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.

Dolidod Teethtard
  • 553
  • 1
  • 7
  • 22
  • Hello there, I'm trying to acheive something similar. Did you manage to do it eventually? and if so then how?? Thank you. – D10S Nov 01 '20 at 17:32
  • It's an old post, I needed that for one of my personal projects. I don't remember what exactly it was, but here's link to this project, I am sure it will help (if you can read something of this spaghetti :D ) - https://github.com/Dolidodzik/cycling-tracker/blob/master/CyclingAFE/screens/MapScreen.js – Dolidod Teethtard Nov 19 '20 at 18:26
  • Thanks a lot for taking the time to respond. I like spaghetii. I will take a look. hope it will like me ;) – D10S Nov 20 '20 at 14:33
  • Did any of the answers work for you? – themthem May 07 '21 at 08:04

2 Answers2

9

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);
  }
});
Pedro Andrade
  • 4,556
  • 1
  • 25
  • 24
  • could you please explain to me the 3rd step why we need to stop it? – Ahsan Ullah Oct 16 '20 at 14:45
  • 1
    ideally nothing should run indefinitely. if your app use user's location all the time, in all the screens, you may have a case for not stopping. I'd argue that's bad UX, though. – Pedro Andrade Oct 17 '20 at 18:21
  • 1
    Do you get consistent location updates in the task? I tried this, but when my app is in the background I don't get consistent logs in the task. Some guys report up to 12 minutes before they see any logging. It is inconsistent for me, sometimes I see the logs quick, other times not so much. Also I never get a collection with more than 1 item in it. So if it report after 2minutes, one location update is not enough to track – Jean Roux Jul 26 '21 at 10:47
  • not really @JeanRoux. It seems to me that small numbers (timeInterval: 1000, distanceInterval: 1) may keep the service working better than larger numbers (like 5000 and 5). how did you configure it? did you notice and change changing these parameters? – Pedro Andrade Sep 23 '21 at 15:30
  • Does this still work for the newer method of background locations support by SDK 43? – MK_Pierce Nov 06 '21 at 01:06
1

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

andersryanc
  • 939
  • 7
  • 19