2020年使用react-native + Expo在后台跟踪位置的最佳方式

问题描述 投票:0回答:3

我想使用 RN + expo 创建我自己的 Endomono/Runtastic 类应用程序(这个应用程序只适合我,而且我有性能/电池寿命相当不错的 Android 手机(Redmi note 7),所以我不担心性能太差)。我想为此使用一体化库,或者只是使用允许我在后台每 X 秒执行一些代码的库(以及那里的 getAsyncLocation)。我的观点只是每 X 秒发送一次纬度/经度数据到我的后端 HTTP django-rest-framework 支持的服务器。

我花了一整天的时间试图找出任何方法来做到这一点,我尝试了几个像这样的库:react-native-background-geolocationreact-native-background-timerreact-native-background -工作还有更多。我按照逐步安装指南进行操作,但不断收到如下错误:

null is not an object (evaluating 'RNBackgroundTimer.setTimeout')

我也尝试过this:我修复了这段代码中的一些错误(与导入相关),它似乎有效,但是当我使用假GPS更改我的GPS位置时,控制台中只出现了一个didFocus函数。这是代码:

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,
  },
});

如果您知道有什么方法可以轻松完成我的目标(将带有位置的简单 HTTP GET 从 Expo + RN 应用程序后台发送到我的 DRF 后端),请告诉我。

react-native geolocation expo
3个回答
9
投票

如果您使用的是 Expo,您只需使用

expo-task-manager
expo-location
即可获取后台位置更新。

这是我正在开发的应用程序上使用的简化版本(并且它肯定可以在 Android 上运行):

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);
  }
});

1
投票

它不一定适用于 Expo,但如果“弹出”您的项目或从 React Native CLI 开始(通过

react-native init
),那么您可以使用 Android 特定的 React Native“NativeModule”来实现您的目标。我喜欢使用 react-native-location 包,它在 iOS 上对后台位置更新有很好的支持,但在 Android 上目前存在一个错误。我整理了一个示例项目,其中在 NativeModule 中包含必要的 Android 特定代码,您可以从以下位置开始:

https://github.com/andersryanc/ReactNative-LocationSample


0
投票

即使应用程序被杀死、关闭、在后台或屏幕锁定,您也可以按照此跟踪用户位置 - https://gist.github.com/diegogangl/a6e82bddbf85795954aa0fc1855096ac

在此之前,请确保您已在 app.json 中授予权限 -

   "ios": {
    "NSLocationAlwaysUsageDescription": "Your location will be used in the 
     background for tracking attendance and geofencing purposes.",
    "NSLocationAlwaysAndWhenInUseUsageDescription": "Location access is required 
     both in the foreground and background for tracking attendance and 
     geofencing.",
    "UIBackgroundModes": ["location","fetch"]
    }
  },
  "android": {
  "permissions": [
  "INTERNET",
  "ACCESS_NETWORK_STATE",
  "ACCESS_COARSE_LOCATION",
  "ACCESS_BACKGROUND_LOCATION",
  "ACCESS_WIFI_STATE",
  "CHANGE_WIFI_STATE"
  ]
© www.soinside.com 2019 - 2024. All rights reserved.