React Native es una opción para crear aplicaciones tanto para Android como para iOS de manera rápida y sencilla ahorrando mucho tiempo de programación, mismas razones por la que viene a la alza últimamente, pero si no eres muy experto en los sistemas nativos de Android e iOS te puedes atorar al inicio del proyecto por no entender que arreglar.
Este blog abarca los detalles para crear propio servicio nativo tratando de explicar cómo resolver los problemas que puedes enfrentar en este caso exclusivamente en Android, dependiendo de la respuesta de este blog se entrará más a detalle a la elaboración del proyecto completo y a los problemas de iOS.
El proyecto fue creado usando el comando “react-native init”, para más información referirse a la documentación https://facebook.github.io/react-native/docs/getting-started en la sección Building Projects with Native Code”, y se siguió la guía de modulos nativos para react native en https://facebook.github.io/react-native/docs/native-modules-android.
Cuando se crea una aplicación en React Native usando el comando “react-native init” se crean varios archivos en la carpeta Android, hay que visualizar dichos archivos y mi recomendación es usar Android Studio dado que te indica los errores de manera que los puedes atacar más rápido, si no lo tienes instalado entra a https://developer.android.com/studio/install?hl=es-419 para una guía completa.
Abrir Proyecto
Al abrir Android Studio se abre la siguiente imagen.
Primero selecciona la opción de abrir proyecto existente (Open an existing Android Studio project).
Busca la carpeta de tu proyecto en React Native y selecciona la carpeta dentro de el que dice android.
NO SELECCIONES TODO EL PROYECTO, si seleccionas todo el proyecto no se visualizará bien en Android Studio.
Si al cargar les sale el siguiente cuadro.
No se preocupen, simplemente autoricen la actualización del Gradle dándole clic a update.
React Native aún no actualiza esto dado que esta versión es relativamente nueva
Pero ofrece mejores capacidades que veremos a continuación.
Errores de Gradle
Si actualizaron, o inclusive si no, pueden recibir el siguiente error:
The specified Android SDK Build Tools version (27.0.3) is ignored.
As it is below the minimum supported version (28.0.3) for Android Gradle Plugin 3.2.1.
Android SDK Build Tools 28.0.3 will be used.
To suppress this warning, remove “buildToolsVersion ‘27.0.3’” from your build.gradle file.
As each version of the Android Gradle Plugin now has a default version of the build tools.
Update Build Tools version and sync project
Open File
Esto se debe por declarar “buildToolsVersion” el cual ya no es necesario en el nuevo gradle. Para ello procedemos a borrarlo en todas partes donde salga.
En el navegador del proyecto (ver imagen a continuación) seleccione el archivo build.gradle nivel módulo: app.
*El navegador usualmente se encuentra a la izquierda y en modo Android.
Dentro del navegador borre la siguiente línea de código:
buildToolsVersion rootProject.ext.buildToolsVersion
Una vez borrada aparecerá una opción en amarillo para sincronizar el proyecto.
*Si no aparece en el menú superior seleccione Build y luego Rebuild Project.
Sincronice y con eso no tendrá más errores.
Creación del servicio
En este caso crearemos un servicio de Geolocalización.
Primero Seleccionamos File->New->New Module…
Seleccione la segunda opción, la cual es Android Library.
Asigne un nombre al modulo, en mi caso es “react-geolocation”.
Dele finish para crear el módulo.
El módulo lucirá algo así:
apply plugin: 'com.android.library' android { compileSdkVersion 27 defaultConfig { minSdkVersion 21 targetSdkVersion 27 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile( 'proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "com.facebook.react:react-native:+" implementation 'com.android.support:appcompat-v7:27.1.1' }
Mi recomendación es cambiar la sección de dependencies y defaultConfig para incluir variables globales, para ello modificamos primero el build.gradle versión proyecto (OJO proyecto no módulo), usualmente se encuentra primero en la lista de “Gradle Scripts”, cambia las siguientes líneas:
ext { buildToolsVersion = "27.0.3" minSdkVersion = 16 compileSdkVersion = 27 targetSdkVersion = 26 supportLibVersion = "27.1.1" }
Por estas:
ext { minSdkVersion = 16 compileSdkVersion = 28 targetSdkVersion = 28 supportLibVersion = "28.0.0" reactNativeVersion = "0.57.0" }
Siempre intenta usar las últimas versiones disponibles del SDK, OJO con “reactNativeVersion” esta debe coincidir con la empleada en el proyecto en el archivo “package.json“. Eso cambia tus archivos módulos por esto:
apply plugin: 'com.android.library' android { compileSdkVersion rootProject.ext.compileSdkVersion defaultConfig { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile( 'proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "com.android.support:appcompat-v7:${ rootProject.ext.supportLibVersion}" implementation "com.facebook.react:react-native:${ rootProject.ext.reactNativeVersion}" }
No olvides sincronizar el proyecto. Continuando el el build.gradle nivel módulo: app agrega esta línea de código:
implementation project(':react-geolocation')
Esto añade tu servicio a la aplicación principal para que pueda acceder a ella, nuevamente sincroniza el proyecto.
Ahora sí podemos comenzar a crear el servicio, primero creamos el paquete, en la carpeta de java del módulo creado le damos clic derecho a la carpeta, seguramente vacía, que NO diga test, y le damos New->Java Class, asignamos un nombre, en mi caso “GeoLocationPackage”, agrega la Interfaz “com.facebook.react.ReactPackage” y da clic en OK. En el archivo creado saldrá un error, intente darle al foco rojo y dele “Implement methods”, si no sale o logra darle al foco rojo intente con el comando ctrl+o, seleccione las dos funciones de “ReactPackage”.
De igual manera cree el módulo, en mi caso “GeoLocationModule”, agregue la Superclase “com.facebook.react.bridge.ReactContextBaseJavaModule”, implemente los métodos recomendados y cree el constructor.
Por último cree el archivo servicio, para esto en vez de seleccionar New->Java Class seleccione New->Service->Service, asigne un nombre, en mi caso “GeoLocationService”, y listo, ya puede empezar a trabajar sobre el servicio para que haga lo que debe hacer. En este caso el servicio lee el GPS y muesta una notificación para indicar que esta activo:
import android.Manifest; import android.annotation.TargetApi; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.support.v4.app.NotificationCompat; import android.support.v4.content.ContextCompat; import android.support.v4.content.LocalBroadcastManager; import javax.annotation.Nullable; public class GeoLocationService extends Service { public static final String FOREGROUND = "com.test.location.FOREGROUND"; private static int GEOLOCATION_NOTIFICATION_ID = 123456789; LocationManager locationManager = null; LocationListener locationListener = new LocationListener() { @Override public void onLocationChanged(Location location) { sendMessage(location); } @Override public void onStatusChanged(String provider, int status, Bundle extras) { } @Override public void onProviderEnabled(String provider) { } @Override public void onProviderDisabled(String provider) { } }; @Override @TargetApi(Build.VERSION_CODES.LOLLIPOP) public void onCreate() { locationManager = (LocationManager) getSystemService( Context.LOCATION_SERVICE); int permissionCheck = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION); if (permissionCheck == PackageManager.PERMISSION_GRANTED) { locationManager.requestLocationUpdates( LocationManager.GPS_PROVIDER, 1000, 0, locationListener); } } private void sendMessage(Location location) { try { Intent intent = new Intent("GeoLocationUpdate"); intent.putExtra("message", location); LocalBroadcastManager.getInstance(this).sendBroadcast(intent); } catch (Exception e) { e.printStackTrace(); } } @Override public void onDestroy() { locationManager.removeUpdates(locationListener); super.onDestroy(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { startForeground(GEOLOCATION_NOTIFICATION_ID, createNotification( "Servicio de ubicación ejecutandose", this)); return START_STICKY; } @Nullable @Override public IBinder onBind(Intent intent) { return null; } private NotificationManager notifManager; public Notification createNotification(String aMessage, Context context) { final int NOTIFY_ID = 0; // ID of notification String id = "wafito"; // default_channel_id String title = "Toca para ir a la aplicación"; // Default Channel Intent intent; PendingIntent pendingIntent; NotificationCompat.Builder builder; if (notifManager == null) { notifManager = (NotificationManager) context .getSystemService(Context.NOTIFICATION_SERVICE); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { int importance = NotificationManager.IMPORTANCE_LOW; NotificationChannel mChannel = notifManager .getNotificationChannel(id); if (mChannel == null) { mChannel = new NotificationChannel(id, title, importance); //mChannel.enableVibration(true); //mChannel.setVibrationPattern( //new long[]{100, 200, 300, 400, 500, //400, 300, 200, 400}); notifManager.createNotificationChannel(mChannel); } builder = new NotificationCompat.Builder(context, id); } else { builder = new NotificationCompat.Builder(context, id); } try { intent = new Intent(context, Class.forName( "com.test.MainActivity")); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); builder.setContentTitle(aMessage) .setSmallIcon(android.R.drawable.ic_dialog_map) .setContentText(context.getString(R.string.app_name)) .setContentText("Toca para ir a la aplicación.") .setDefaults(Notification.DEFAULT_ALL) .setAutoCancel(true) .setContentIntent(pendingIntent) .setTicker(aMessage) .setPriority(Notification.PRIORITY_LOW); } catch (ClassNotFoundException e) { e.printStackTrace(); } return builder.build(); } }
El “locationListener” se encarga de enviar las actualizaciones del GPS. Al crearse el servicio se inicia el “locationManager”, el cual inicia las actualizaciones, la función “sendMessage” envía información por un Broadcast el cual es leído en el módulo, lo demás es para mostrar la notificación. Al archivo módulo se modifica de la siguiente manera:
import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.location.Location; import android.support.v4.content.LocalBroadcastManager; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; 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; public class GeoLocationModule extends ReactContextBaseJavaModule { private ReactApplicationContext reactContext; GeoLocationModule(ReactApplicationContext reactContext) { super(reactContext); this.reactContext = reactContext; BroadcastReceiver geoLocationReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Location message = intent.getParcelableExtra("message"); sendEvent(message); } }; LocalBroadcastManager.getInstance(reactContext).registerReceiver( geoLocationReceiver, new IntentFilter("GeoLocationUpdate")); } @Override public String getName() { return "GeoLocation"; } @ReactMethod public void startService(Promise promise) { String result = "Success"; try { Intent intent = new Intent(GeoLocationService.FOREGROUND); intent.setClass(reactContext, GeoLocationService.class); reactContext.startService(intent); } catch (Exception e) { promise.reject(e); return; } promise.resolve(result); } @ReactMethod public void stopService(Promise promise) { String result = "Success"; try { Intent intent = new Intent(GeoLocationService.FOREGROUND); intent.setClass(reactContext, GeoLocationService.class); this.reactContext.stopService(intent); } catch (Exception e) { promise.reject(e); return; } promise.resolve(result); } private void sendEvent(Location message) { WritableMap map = Arguments.createMap(); WritableMap coordMap = Arguments.createMap(); coordMap.putDouble("latitude", message.getLatitude()); coordMap.putDouble("longitude", message.getLongitude()); coordMap.putDouble("accuracy", message.getAccuracy()); coordMap.putDouble("altitude", message.getAltitude()); coordMap.putDouble("heading", message.getBearing()); coordMap.putDouble("speed", message.getSpeed()); map.putMap("coords", coordMap); map.putDouble("timestamp", message.getTime()); reactContext.getJSModule(DeviceEventManagerModule .RCTDeviceEventEmitter.class) .emit("updateLocation", map); } }
Como se menciono arriba el servicio envía un Broadcast el cual se recibe en el módulo, se inicializa el mismo en el constructor gracias al filtro de “GeoLocationUpdate” y se obtiene el mensaje por el valor “message”, el cual se envía a JS a través del módulo mismo y a “DeviceEventManagerModule”, el parámetro “updateLocation” en “emit” es la clave para asignar el listener en JS, dado que deben ser del mismo nombre. El título “@ReactMethod” sirve para acceder a la función desde JS, sin el no podrías en este caso iniciar o terminar el servicio desde JS.
Y por último en el archivo Paquete se añade e inicia el módulo en la lista de módulos:
import com.facebook.react.ReactPackage; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.uimanager.ViewManager; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class GeoLocationPackage implements ReactPackage { @Override public List<NativeModule> createNativeModules( ReactApplicationContext reactContext) { List<NativeModule> modules = new ArrayList<NativeModule>(); modules.add(new GeoLocationModule(reactContext)); return modules; } @Override public List<ViewManager> createViewManagers( ReactApplicationContext reactContext) { return Collections.emptyList(); } }
Además para que se reconozca como módulo nativo es necesario modificar el código del archivo java “MainApplication” del módulo app. Específicamente de la función “getPackages()”:
@Override protected List<ReactPackage> getPackages() { return Arrays.asList( new MainReactPackage(), new GeoLocationPackage() ); }
Una vez agregado el código hemos terminado con la parte de Android.
Modificaciones en JavaScript
Primero, agregamos 2 librerías de ‘react-native’:
import { DeviceEventEmitter, NativeModules } from 'react-native';
Después importamos el módulo:
// Modulo desde android native.
const geoLocation = NativeModules.GeoLocation;
En la interfaz, que no mostraré completa, simplemente añadimos un botón para cambiar el estado del servicio. En otras palabras lo prendemos y apagamos:
/** toggle del listener de location */ async toggleLocation() { const { position } = this.state; let live = false; if (position.live) { geoLocation.stopService(); // this.location.clearWatch(); } else { geoLocation.startService(); live = true; // this.location.getCurrent(); // this.location.watchPosition(); } this.setState({ position: { ...position, live } }); }
async componentDidMount() { // permissions try { const permiso = await new Permissions().requestLocation(); if (permiso !== PermissionsAndroid.RESULTS.GRANTED) { /* throw error? */ return; } } catch (err) { console.error(err); } // GeoLocationService Listener this.locationListener(); /* Location desde react-native. utilities/Location.js this.initiateLocation(); */ }
Además iniciamos el listener para actualizar los cambios en la vista:
/** * Metodo para crear un listener para el servicio nativo de geolocation. * Update state y enviar info por socket si esta abierto. */ locationListener() { this.deviceListener = DeviceEventEmitter.addListener( 'updateLocation', (geoData) => { const { position, socket, socket: { room, uuid } } = this.state; const { timestamp, coords: { latitude, longitude } } = geoData; // actualizando state this.setState({ position: { ...position, latitude, longitude, timestamp }, }); // enviando posicion if (socket.status) { const msg = { type: 'position', room, uuid, position: { latitude, longitude, timestamp } }; this.socket.websocketWrite(JSON.stringify(msg)); } }); }
Y no se olvide de apagar el servicio al salir de la vista, sino seguirá corriendo y no se detendrá:
componentWillUnmount() { geoLocation.stopService(); }
Con esto terminamos este tutorial, si aún tienes dudas o quieres saber más házmelo saber en los comentarios.