Servicios nativos de Android en React Native desde CERO para principiantes

rect native

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.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *