这篇文章将涵盖一个在许多应用程序中都是关键的主题,并将侧重于一个在后台和最有趣的部分中完全起作用的解决方案:从Android 4.4(SDK 19)到撰写本文时的最新版本这是Android Oreo(SDK 27)。
该解决方案的目的不是进行实时跟踪,而是以很小的间隔(例如5或10分钟)跟踪用户位置,但是如果您想尝试在较小的位置上获取位置更新,则可能会采取极端做法间隔(例如1分钟)。
Android 8及其特殊性
Google希望通过Oreo对表格进行一些更改,主要涉及后台服务和后台位置。
可以在以下两个页面中更详细地了解更改:
但是,我将向您简要介绍它们的含义。
促使Google进行这些更改的原因是第三方应用程序消耗了电源和资源 ,换句话说,他们希望通过给予开发人员更大程度的控制自由来更好地控制其操作系统环境。 这意味着我们无法像在早期版本的Android中那样自由地运行后台任务。
为了向目标市场提供更好的用户体验,此更改很有意义。 毕竟,我们不希望有一个应用程序窃取我们所有的智能手机资源,因为它运行过多的后台任务,或者根本无法很好地处理代码,从而反过来不仅会破坏处理资源,还会破坏内存。
有了新的后台服务限制,Google在其“ 后台执行限制 ”页面中建议:
在大多数情况下,应用程序可以通过使用JobScheduler作业来解决这些限制。 这种方法使应用程序可以在应用程序未积极运行时安排执行工作,但仍给系统留出了以不影响用户体验的方式安排这些任务的余地。
这似乎是一个很好的建议,但是有一些陷阱 。
陷阱#1
JobScheduler是在SDK 21(又称为Android 5)中添加的,因此,如果我们的目标是Android 4.4,则不是真正的选择。
陷阱#2
即使我们的目标是Android 5及更高版本,也会有一个小问题。 排定由JobScheduler运行的作业只能以至少15分钟的间隔运行,这使我们进入了文章标题“ React-Native Background Location ”,我们希望设计一种解决方案来获取用户的位置更新。 如果要谈论位置,则15分钟是一个很大的间隔,因此我们需要找出其他方法来获得所需的结果。
解决方案的目的
因此,我们要建设的只是大型项目的一小部分,该项目旨在供卡车驾驶员在执行运送货物时使用。 目的是为了雇用卡车司机的公司,并使客户能够知道司机的位置以及因此知道他们的装运地点。 此功能有一些要求 :
跟踪的间隔可能为5-10分钟。跟踪必须在应用程序处于后台的状态下处于活动状态。功耗应保持在最低水平。内存泄漏和内存使用率应分别不存在/较低
根据这些指南,我们绝对需要在Android中寻找后台服务API。
要看的第一个逻辑选择是WorkManager API。
WorkManager API
乍一看,该API似乎非常适合我们的情况,因为它甚至支持具有Android 4.0的设备,并且即使手机重启也可以创建后台任务。 它非常灵活,因为它可以为您完成大量工作,但它有一个巨大的陷阱 : 它是AndroidX中的依赖项 。
因此,对于不认识的人来说,AndroidX是Android生态系统提供的新支持库,目的是统一并提供较新的API和程序包供开发人员在其应用程序中使用。 这里的窍门是: 您可以在AndroidX下或在AndroidX之前使用依赖项 。 您不能将两者混为一谈,因为导入两个支持库提供程序中都存在的依赖项时会发生命名冲突。 许多API都属于此问题。
我想指出的另一件事是,此解决方案源自的原始项目是一个企业级应用程序,以响应本机的方式进行,并在此基础上进行了2年的开发,并且已经安装了许多依赖项。
因此将所有依赖项迁移到AndroidX几乎是不可能的,因为您将不得不更改应用程序下的每个直接依赖项以及项目中的每个本机项目依赖项。
总而言之,此API需要排除在外,因为在我们的特殊情况下,它无法完成其设计要完成的工作。
那么AlarmManager API呢?
借助AlarmManager
API,我们可以安排后台服务以我们定义的特定时间间隔运行,而最小间隔时间为1分钟,这真是太好了!
使用此API的主要陷阱在于,如果应用程序本身在后台(至少在Android 8及更高版本中),则它将无法启动后台服务。 如果我们要确保在消息传递或任何其他应用程序处于打开状态且处于前台时,我们的应用程序仍在跟踪我们的用户,则这是一个阻止程序。
尽管它不能完全解决我们的问题,但它仍然是一个非常有用的API,因为它启动了一个新线程来为我们运行服务,并且我们可以取消计划的警报并以这种方式关闭分配的线程。
输入前台服务
借助Foreground Services,即使我们的应用程序不在前台,我们也可以维持在后台连续运行的服务。 我们之所以能够做到这一点,是因为我们的应用程序将收到一条不可撤销的通知,通知用户该应用程序仍在运行,尽管此时他可能看不到它。 由于我们的应用程序正在通知用户其仍在运行,因此Google允许它几乎执行您想执行的任何后台任务,在我们看来,这是完美的!
因此,我们要分析的解决方案包括Foreground服务, AlarmManager
API实例, IntentService
和BroadcastReceiver
,以便获取常规位置更新并将其存储在本地。
解决方案
此处提供了最终的可复制演示,它是我们想要实现的很小的可复制示例。
首先,我们的解决方案基于许多本地Java代码,因此从现在开始,只有在弹出的react-native应用程序出现的情况下,您才可以实施此解决方案,这与创建react-native的过程类似使用react-native init my-project的项目。
为了使用智能手机的位置,您自然必须为此请求权限。 这部分是在JS端处理的,但没什么大不了的,只是浏览一下文件以检查发生了什么。
首先,让我们创建本机模块,以便能够从JavaScript端初始化本机代码。
package com.rnbglocation.location;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import androidx.core.content.ContextCompat;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.google.gson.Gson;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import static com.rnbglocation.location.LocationForegroundService.LOCATION_EVENT_DATA_NAME;
public class LocationModule extends ReactContextBaseJavaModule implements LocationEventReceiver , JSEventSender {
private static final String MODULE_NAME = "LocationManager" ;
private static final String CONST_JS_LOCATION_EVENT_NAME = "JS_LOCATION_EVENT_NAME" ;
private static final String CONST_JS_LOCATION_LAT = "JS_LOCATION_LAT_KEY" ;
private static final String CONST_JS_LOCATION_LON = "JS_LOCATION_LON_KEY" ;
private static final String CONST_JS_LOCATION_TIME = "JS_LOCATION_TIME_KEY" ;
private Context mContext;
private Intent mForegroundServiceIntent;
private BroadcastReceiver mEventReceiver;
private Gson mGson;
LocationModule( @Nonnull ReactApplicationContext reactContext) {
super (reactContext);
mContext = reactContext;
mForegroundServiceIntent = new Intent(mContext, LocationForegroundService.class);
mGson = new Gson();
createEventReceiver();
registerEventReceiver();
}
@ReactMethod
public void startBackgroundLocation () {
ContextCompat.startForegroundService(mContext, mForegroundServiceIntent);
}
@ReactMethod
public void stopBackgroundLocation () {
mContext.stopService(mForegroundServiceIntent);
}
@Nullable
@Override
public Map<String, Object> getConstants () {
final Map<String, Object> constants = new HashMap<>();
constants.put(CONST_JS_LOCATION_EVENT_NAME, LocationForegroundService.JS_LOCATION_EVENT_NAME);
constants.put(CONST_JS_LOCATION_LAT, LocationForegroundService.JS_LOCATION_LAT_KEY);
constants.put(CONST_JS_LOCATION_LON, LocationForegroundService.JS_LOCATION_LON_KEY);
constants.put(CONST_JS_LOCATION_TIME, LocationForegroundService.JS_LOCATION_TIME_KEY);
return constants;
}
@Nonnull
@Override
public String getName () {
return MODULE_NAME;
}
@Override
public void createEventReceiver () {
mEventReceiver = new BroadcastReceiver() {
@Override
public void onReceive (Context context, Intent intent) {
LocationCoordinates locationCoordinates = mGson.fromJson(
intent.getStringExtra(LOCATION_EVENT_DATA_NAME), LocationCoordinates.class);
WritableMap eventData = Arguments.createMap();
eventData.putDouble(
LocationForegroundService.JS_LOCATION_LAT_KEY,
locationCoordinates.getLatitude());
eventData.putDouble(
LocationForegroundService.JS_LOCATION_LON_KEY,
locationCoordinates.getLongitude());
eventData.putDouble(
LocationForegroundService.JS_LOCATION_TIME_KEY,
locationCoordinates.getTimestamp());
// if you actually want to send events to JS side, it needs to be in the "Module"
sendEventToJS(getReactApplicationContext(),
LocationForegroundService.JS_LOCATION_EVENT_NAME, eventData);
}
};
}
@Override
public void registerEventReceiver () {
IntentFilter eventFilter = new IntentFilter();
eventFilter.addAction(LocationForegroundService.LOCATION_EVENT_NAME);
mContext.registerReceiver(mEventReceiver, eventFilter);
}
@Override
public void sendEventToJS (ReactContext reactContext, String eventName, @Nullable WritableMap params) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}
}
这是我们定位功能的第一步。 这是您要构建的每个本机模块以响应本机的通用过程,您可以在此处关注更多文档。
该文件中最重要的两个部分是LocationEventReceiver
和JSEventSender
接口。 创建它们的目的是为了显示分别实现它们的任何类的两种职责:
通过BroadcastReceiver
接收位置更新。将事件发送到JS端(带有位置更新)。
需要特别注意的是,如果要将事件发送到JS端,则需要在我们创建的React模块中实际发送事件,因为需要ReactApplicationContext
来执行。 否则,我们的前台服务可以直接实现JSEventSender
接口。
此模块将公开JS端可访问的2种方法:
-
startBackgroundLocation
-
stopBackgroundLocation
他们将负责启动或停止我们的前台服务。
接下来,我们将分析一下前台服务,这是我们背后一切的策划者。
package com.rnbglocation.location;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.IBinder;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import com.google.gson.Gson;
import com.rnbglocation.MainActivity;
public class LocationForegroundService extends Service implements LocationEventReceiver {
public static final String CHANNEL_ID = "ForegroundServiceChannel" ;
public static final int NOTIFICATION_ID = 1 ;
public static final String LOCATION_EVENT_NAME = "com.rnbglocation.LOCATION_INFO" ;
public static final String LOCATION_EVENT_DATA_NAME = "LocationData" ;
public static final int LOCATION_UPDATE_INTERVAL = 60000 ;
public static final String JS_LOCATION_LAT_KEY = "latitude" ;
public static final String JS_LOCATION_LON_KEY = "longitude" ;
public static final String JS_LOCATION_TIME_KEY = "timestamp" ;
public static final String JS_LOCATION_EVENT_NAME = "location_received" ;
private AlarmManager mAlarmManager;
private BroadcastReceiver mEventReceiver;
private PendingIntent mLocationBackgroundServicePendingIntent;
private Gson mGson;
@Override
public void onCreate () {
super .onCreate();
mAlarmManager = (AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE);
mGson = new Gson();
createEventReceiver();
registerEventReceiver();
}
@Override
public int onStartCommand (Intent intent, int flags, int startId) {
createNotificationChannel();
startForeground(NOTIFICATION_ID, createNotification());
createLocationPendingIntent();
mAlarmManager.setRepeating(
AlarmManager.RTC,
System.currentTimeMillis(),
LOCATION_UPDATE_INTERVAL,
mLocationBackgroundServicePendingIntent
);
return START_NOT_STICKY;
}
@Override
public void createEventReceiver () {
mEventReceiver = new BroadcastReceiver() {
@Override
public void onReceive (Context context, Intent intent) {
LocationCoordinates locationCoordinates = mGson.fromJson(
intent.getStringExtra(LOCATION_EVENT_DATA_NAME), LocationCoordinates.class);
/*
TODO: do any kind of logic in here with the LocationCoordinates class
e.g. like a request to an API, etc --> all on the native side
*/
}
};
}
@Override
public void registerEventReceiver () {
IntentFilter eventFilter = new IntentFilter();
eventFilter.addAction(LOCATION_EVENT_NAME);
registerReceiver(mEventReceiver, eventFilter);
}
@Nullable
@Override
public IBinder onBind (Intent intent) {
return null ;
}
@Override
public void onDestroy () {
unregisterReceiver(mEventReceiver);
mAlarmManager.cancel(mLocationBackgroundServicePendingIntent);
stopSelf();
super .onDestroy();
}
private void createNotificationChannel () {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel serviceChannel = new NotificationChannel(
CHANNEL_ID,
"Foreground Service Channel" ,
NotificationManager.IMPORTANCE_DEFAULT
);
NotificationManager manager = getSystemService(NotificationManager.class);
manager.createNotificationChannel(serviceChannel);
}
}
private Notification createNotification () {
Intent notificationIntent = new Intent( this , MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity( this , 0 , notificationIntent, 0 );
return new NotificationCompat.Builder( this , CHANNEL_ID)
.setContentIntent(pendingIntent)
.build();
}
private void createLocationPendingIntent () {
Intent i = new Intent(getApplicationContext(), LocationBackgroundService.class);
mLocationBackgroundServicePendingIntent = PendingIntent.getService(getApplicationContext(), 1 , i, PendingIntent.FLAG_UPDATE_CURRENT);
}
}
在此服务中,我们将像在模块上一样实现LocationEventReceiver
。 在这样一个简单的演示中,这可能有点多余,但是让此服务知道位置更新的目的是因为您可能希望前台服务根据用户的位置启动具有不同工作的不同后台任务,或者您可能想要做特定的本机工作,例如在本地保留这些坐标。
onStartCommand
方法是服务的基础,它负责:
- 创建我们的通知渠道(从Oreo开始的必要步骤)
- 创建显示给我们用户的通知
- 调用
startForeground
方法,该方法对于将该服务作为前台服务启动至关重要 - 使用
AlarmManager
安排后台任务以1分钟的间隔获取我们的用户位置(该间隔当然是完全可自定义的)
不要忘记,为了使您的应用程序能够使用前台服务,它必须在清单文件中使用以下命令声明其权限:
-
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
实际的位置获取和处理是在后台任务中与主线程不同的线程中完成的。 让我们看看这个后台服务。
package com.rnbglocation.location;
import android.annotation.SuppressLint;
import android.app.IntentService;
import android.content.Intent;
import android.location.Location;
import android.os.Handler;
import androidx.annotation.Nullable;
import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;
import com.google.gson.Gson;
import java.util.Date;
public class LocationBackgroundService extends IntentService {
private FusedLocationProviderClient mFusedLocationClient;
private LocationCallback mLocationCallback;
private Gson mGson;
public LocationBackgroundService () {
super (LocationForegroundService.class.getName());
mGson = new Gson();
}
@SuppressLint ( "MissingPermission" )
@Override
protected void onHandleIntent (@Nullable Intent intent) {
mFusedLocationClient = LocationServices.getFusedLocationProviderClient(getApplicationContext());
mLocationCallback = createLocationRequestCallback();
LocationRequest locationRequest = LocationRequest.create()
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
.setInterval( 0 )
.setFastestInterval( 0 );
new Handler(getMainLooper()).post(() -> mFusedLocationClient.requestLocationUpdates(locationRequest, mLocationCallback, null ));
}
private LocationCallback createLocationRequestCallback () {
return new LocationCallback() {
@Override
public void onLocationResult (LocationResult locationResult) {
if (locationResult == null ) {
return ;
}
for (Location location : locationResult.getLocations()) {
LocationCoordinates locationCoordinates = createCoordinates(location.getLatitude(), location.getLongitude());
broadcastLocationReceived(locationCoordinates);
mFusedLocationClient.removeLocationUpdates(mLocationCallback);
}
}
};
}
private LocationCoordinates createCoordinates ( double latitude, double longitude) {
return new LocationCoordinates()
.setLatitude(latitude)
.setLongitude(longitude)
.setTimestamp( new Date().getTime());
}
private void broadcastLocationReceived (LocationCoordinates locationCoordinates) {
Intent eventIntent = new Intent(LocationForegroundService.LOCATION_EVENT_NAME);
eventIntent.putExtra(LocationForegroundService.LOCATION_EVENT_DATA_NAME, mGson.toJson(locationCoordinates));
getApplicationContext().sendBroadcast(eventIntent);
}
}
该服务非常简单,它使用FusedLocationProviderClient
来获取具有我们LocationRequest
指定的详细信息的用户位置。
当位置提供者完成获取用户位置后,我们将通过广播机制将转换为我们的LocationCoordinates
类的位置发送给任何想听这些坐标的人。
这些文件是该解决方案的核心,借助此文件,您可以使用本机解决方案来完美地控制几乎所有Android智能手机上的用户位置,该解决方案非常节省资源和电池。
补充笔记
对于Android 4.4支持,还需要一些额外的步骤,这些步骤已包含在提供的演示中,但我认为仍然值得一提。
在我们的app.gradle
文件中,我们需要包含有关APK构建过程的新依赖关系,该依赖关系为:
-
implementation “com.android.support:multidex:1.0.3”
除此之外,在同一个文件中,我们需要声明我们希望我们的应用程序通过以下语句启用多重删除:
-
multiDexEnabled true
在defaultConfig
配置下为multiDexEnabled true
这将使我们的应用程序知道我们要包含此内容,但是为了使所有功能正常运行,我们需要在MainApplication.java文件下再做一件事:
- …
MainApplication extends MultiDexApplication
…
通过这些配置,我们确保我们的应用程序现在与Android 4.4完全兼容,这太棒了!
结论
React-Native是供开发人员使用的绝佳平台,并且绝对可以为大多数来自Web背景的开发人员加快开发速度。 尽管如此,仍有许多东西需要Android或iOS上的本机知识,而此演示就是一个例子。
关于技术本身,我想说的一件事是:
在企业环境中,为您的应用选择技术时要小心。 React Native远非稳定之举,各个版本之间的重大更改是数个。
只是有一个小主意:
- v0.60 —某些用户所涉及的重大更改
- v0.59 —在Android上进行重大更改
- v0.58 —某些组件的重大更改
- v0.57.2 —由于元素被删除而造成的重大更改
考虑到这一点,您会发现,当您尝试为某些新功能,优化或错误修复而升级应用程序时,将来版本之间的所有重大更改都可能在应用程序维护方面出现问题。 在考虑将该技术与传统技术进行权衡时,请仔细权衡取舍,尤其是如果您要长期维护某些东西,或者您没有专门的大型或专业团队来构建和维护产品。
希望您喜欢这个简单的演示,最重要的是,希望对您的项目有所帮助! 🎉
我也希望收到您的反馈。🙂如果您觉得本文有趣,请分享,因为您知道—分享很重要!
另外,如果您喜欢在具有全球影响力的大型项目中工作,并且喜欢挑战,请通过 xgeeks 与我们 联系 ! 我们一直在寻找人才加入我们的团队 our
From: https://hackernoon.com/react-native-background-location-module-for-android-p3ey36hj