【问题标题】:The best way of tracking location in background using react-native + Expo in 20202020 年使用 react-native + Expo 在后台跟踪位置的最佳方式
【发布时间】:2020-06-26 23:57:49
【问题描述】:

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

我花了一整天的时间想办法做到这一点,我尝试了几个这样的库:react-native-background-geolocationreact-native-background-timerreact-native-background-job 等等。我按照分步安装指南进行操作,但一直收到如下错误:null is not an object (evaluating 'RNBackgroundTimer.setTimeout')

我也尝试过this:我修复了这段代码中的一些错误(与导入相关),它似乎可以工作,但是当我使用 Fake 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 后端),请告诉我。

【问题讨论】:

  • 您好,我正在尝试实现类似的目标。你最终做到了吗?如果是这样,那怎么办?谢谢。
  • 这是一篇旧帖子,我的一个个人项目需要它。我不记得它到底是什么,但这里是这个项目的链接,我相信它会有所帮助(如果你能读到这个意大利面条的东西:D)-github.com/Dolidodzik/cycling-tracker/blob/master/CyclingAFE/…
  • 非常感谢您花时间回复。我喜欢意大利面。我会看看。希望它会喜欢我;)
  • 任何答案对你有用吗?

标签: react-native geolocation expo


【解决方案1】:

如果您使用 Expo,您可以简单地使用 expo-task-managerexpo-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);
  }
});

【讨论】:

  • 你能解释一下为什么我们需要停止第三步吗?
  • 理想情况下,任何东西都不应该无限期地运行。如果您的应用程序一直使用用户的位置,在所有屏幕中,您可能会有不停止的情况。不过,我认为这是糟糕的用户体验。
  • 您是否在任务中获得一致的位置更新?我试过这个,但是当我的应用程序在后台时,我没有在任务中获得一致的日志。有些人在看到任何日志记录之前长达 12 分钟报告。这对我来说是不一致的,有时我会很快看到日志,而其他时候则看不到那么多。此外,我从来没有得到一个包含超过 1 个项目的集合。因此,如果它在 2 分钟后报告,则一次位置更新不足以跟踪
  • 不是真的@JeanRoux。在我看来,小数字(timeInterval:1000,distanceInterval:1)可能会使服务比大数字(如 5000 和 5)更好地工作。你是怎么配置的?您是否注意到并更改了这些参数?
  • 这仍然适用于 SDK 43 支持的较新的后台位置方法吗?
【解决方案2】:

它不一定适用于 Expo,但如果“退出”您的项目或从 React Native CLI(通过 react-native init)开始,那么您可以使用 Android 特定的 React Native“NativeModule”来实现您的目标。我喜欢使用react-native-location 包,它在iOS 上非常支持后台位置更新,但在Android 上目前存在一个错误。我整理了一个示例项目,它在 NativeModule 中包含必要的 Android 特定代码,您可以使用以下代码开始:

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

【讨论】:

    猜你喜欢
    • 2021-09-14
    • 2022-08-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-05
    相关资源
    最近更新 更多