Empezamos una nueva sección muy útil y en la que todos vamos a poder participar, así que dadle la bienvenida a Descubriendo librerías.

En esta nueva sección iré hablando de las librerías más populares y útiles para Android, explicando paso a paso su funcionamiento. Además van a poder participar todos, pues podéis dejarme comentarios pidiendo que hable de alguna librería o pidiendo una librería para algún comportamiento y si encuentro una librería de calidad y fiable, crearé un post.

Librerías sí, pero solo las necesarias.

¿Qué quiero decir con esto? Pues que las librerías son muy útiles y necesarias, pero debemos comprender cuándo las tenemos que implementar y cuándo es mejor hacerlo nosotros mismos.

Yo por ejemplo sigo un par de reglas muy sencillas.

  • Si es un diseño o comportamiento sencillo, prefiero hacerlo yo.
  • Si veo que la librería hace tiempo que no se actualiza, prefiero hacerlo yo.
  • Cuando la librería es muy reciente, prefiero hacerlo yo.

Esas son mis tres reglas de oro. Es el momento de empezar la sección.

¿Qué es Zxing?

Zxing es una librería que llevo años utilizando en todo tipo de aplicaciones, desde personales hasta profesionales. Esta librería nos permite de una forma muy sencilla poder leer una gran cantidad de códigos, pero nosotros nos centraremos en el código QR y el código de barras.

1D product1D industrial2D
UPC-ACode 39QR Code
UPC-ECode 93Data Matrix
EAN-8Code 128Aztec
EAN-13CodabarPDF 417
UPC/EAN Extension 2/5ITFMaxiCode
RSS-14
RSS-Expanded
Listado de formatos compatibles con Zxing.

Instalando Zxing

Como siempre crearemos un proyecto, yo lo llamaré AppScanner. En esta ocasión no usaré la libraría oficial de Zxing, utilizaremos una versión modificada para Android que simplifica mucho los procesos, se llama ZXing Android Embedded y es básicamente una librería que contiene ZXing y por encima de eso, controla la gestión de este, como los permisos de la cámara y los ciclos de vida de la app.

Lo primero que haremos será añadir la librería, para ello iremos al GitHub de Zxing Android Embedded y buscaremos la dependencia y la última versión estable. En mi caso la última versión estable es la 4.0.2. Puedes ver la última release disponible aquí.

Iremos al fichero Build.Gradle (Module: app), que si tienes puesta la vista project, estará en AppScanner>App>Build.Gradle.

Antes de continuar debemos decidir si nuestra app será compatible con versiones inferiores a la versión de Android SDK API 24 o no, o lo que es lo mismo, si nuestra app soportará versiones inferiores a Android 7. Mi recomendación es siempre usar un API mínima 21, que corresponde a Android 5. ¿Cómo decidimos eso? Pues en este mismo fichero Build.Gradle, veremos que dentro de defaultConfig{} tendremos minSDKVersion, este número definirá la versión mínima de los móviles para que se puedan instalar esta app. Yo lo dejaré en 21.

Android SDK 24

Si queremos limitar el minSDKVersion a 24, entonces solo deberemos instalar una única dependencia. Vamos a dependencies{} y dentro añadimos

    implementation 'com.journeyapps:zxing-android-embedded:4.1.0'

Android SDK versiones anteriores

Si como yo quieres mantener las versiones antiguar, deberemos añadir la librería de Zxing.

 implementation('com.journeyapps:zxing-android-embedded:4.1.0') { transitive = false }
 implementation 'com.google.zxing:core:3.3.0'

Fíjate que ahora, la librería de android embedded está entre paréntesis y lleva la etiqueta transitive a false.

Además de lo anterior debemos ir al fichero AndroidManifest.xml y añadir la siguiente etiqueta encima de la etiqueta <aplication>

<uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />

Configurando Zxing

Ahora que hemos añadido las dependencias, debemos configurar nuestro proyecto independientemente de la versión que hayamos usado.

Dentro del fichero AndroidManifest.xml buscaremos la etiqueta <application que si nos fijamos tiene varios atributos en su interior. Para poder trabajar con la cámara y el escaner, tendremos que añadir el atributo

android:hardwareAccelerated="true"

Lo podemos añadir entre todos esos atributos, por ejemplo yo lo he dejado así.

 <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:hardwareAccelerated="true"
        android:theme="@style/AppTheme">

Este atributo nos permitirá consumir más recursos cuando sea necesario, por ejemplo en la cámara. Tienes más información en la página oficial de Android.

Por último iremos al fichero Build.Gradle (donde añadimos las depencencias) y dentro de las llaves de android{} le diremos a nuestra app que deberá usar la versión 8 de java por defecto.

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }

También dentro de esta etiqueta android{} buscaremos la propiedad buildToolsVersion y nos aseguraremos que sea superior a la versión 28.0.3, pero por defecto ya lo debería tener.

Además tendremos que añadir la opción para trabajar con view binding. Si todavía no te has visto el capítulo te recomiendo que le eches un ojo.

 buildFeatures{
        viewBinding = true
    }

Ahora ya podemos sincronizar el gradle y tendremos nuestro proyecto sincronizado.

Programando nuestro lector de códigos de barras y QR

Haremos la app más sencilla posible, para empezar iremos a activity_main.xml y añadiremos un botón en el centro de la pantalla.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btnScanner"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="Escanear código" />

</FrameLayout>

A continuación vamos a ir a nuestro MainActivity y configuraremos el View Binding.

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }

En el método onCreate() llamaremos al botón previamente creado en el layout y le pondremos un setOnClickListener().

 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.btnScan.setOnClickListener { initScanner() }
    }

El listener llamará a la función initScanner() que todavía no hemos creado.

 private fun initScanner(){
        IntentIntegrator(this).initiateScan()
    }

Con solo estas pequeñas líneas de código, si ejecutamos la aplicación y pulsamos el botón veremos que se abre una pantalla horizontal que nos permitirá escanear códigos. Pero una vez escaneado no hace nada ¿verdad? ¿y qué pasa si quiero que la pantalla no se ponga horizontal? Lo veremos a continuación.

Recuperando resultados

El siguiente paso será recuperar el resultado del valor escaneado. Si nos fijamos bien, cuándo pulsamos el botón del escaner y lo iniciamos, esta función internamente lo que hace es abrir una nueva activity que está dentro de la librería que hemos implementado y cuando escanea un código correctamente vuelve para atrás. Debemos utilizar un método que nos avise cuando vuelve una activity con algún valor. Para ello usaremos onActivityResult().

La función onActivityResult() nos avisa si lanzamos una nueva activity y le decimos al sistema que estamos esperando respuesta. Para ello en vez de usar startActivity como siempre habría que usar startActivityForResult que será lo que tiene la librería por dentro al iniciar el escaner.

  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
  
    }

Fijaros que lleva un override al principio de la función, es una función del sistema que ya genera una lógica por debajo. Cuando la función se llame, traerá tres valores, pero no vamos a entrar en detalle con esto porque nuestra librería tiene una función que nos simplificará también esta parte.

Crearemos dentro de la función onActivityResult una variable que la igualaremos con la clase IntentIntegrator llamando a la función parseActivityResult() que recibirá los tres parámetros que onActivityResult nos manda.

  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
    }

Esta función, que es de la propia librería nos quitará todos los problemas, pues result tendrá un valor o será null, es decir, nulo. Entonces tendremos que comprobar si es null o no, si lo es, dejaremos que la función onActivityResult haga lo que tenga que hacer y no modificaremos su flujo, para ello solo tendremos que llamar a super.onActivityResut, pero si el valor no es nulo, significará que viene de la pantalla del escaner.

 val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
        if (result != null) {
        } else {
            super.onActivityResult(requestCode, resultCode, data)
        }

Para terminar comprobaremos el contenido de result.contents que contendrá el mensaje escaneado si el escaner ha ido bien. Si por el contrario result.contents es null, significará que el usuario ha entrado en la pantalla del escaner, pero no ha escaneado nada y ha vuelto a la pantalla anterior.

   override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
        if (result != null) {
            if (result.contents == null) {
                Toast.makeText(this, "Cancelado", Toast.LENGTH_LONG).show()
            } else {
                Toast.makeText(this, "El valor escaneado es: " + result.contents, Toast.LENGTH_LONG).show()
            }
        } else {
            super.onActivityResult(requestCode, resultCode, data)
        }
    }

Así quedaría la función completa. He añadido dos toast para cuando result.contents es null o no. Si es nulo saldrá un mensaje diciendo «cancelado», para probarlo simplemente pulsa el botón de escanear y vuelve a la pantalla anterior sin escanear nada. Por otro lado si vas al escaner y escaneas un código de barras o un QR, volverás atrás automáticamente y se mostrará el mensaje escaneado. A continuación añado un código QR válido para que puedas comprobarlo por ti mismo.

Zxing qr ejemplo en kotlin
Escanea el QR y comprueba que tu app funciona.

Cambiando la orientación del escaner

Nuestra app funciona, pero no me termina de convencer que la vista del escaner se ponga en horizontal, es por ello que lo vamos a cambiar. Vamos a volver al fichero AndroidManifest.xml y nos colocaremos justo antes de la etiqueta </application>. Vamos a modificar la orientación de la pantalla del escaner, que recordemos es una activity real dentro de la librería que hemos implementado.

<activity
            android:name="com.journeyapps.barcodescanner.CaptureActivity"
            android:screenOrientation="portrait"
            tools:replace="screenOrientation" />

Estamos creando una nueva etiqueta activity dentro de la etiqueta application, esto es así porque la activity que estamos modificando está dentro del proyecto aunque esté en la librería. El atributo android:name recibirá el nombre de la clase que queremos modificar, en este caso esta siempre será la ruta. Luego le estamos diciendo que tenga una orientación portrait, es decir, vertical. Pero como está dentro de la librería tenemos que meter tools:replace=»screenOrientation» para que pueda sobrescribir el valor actual.

Ejecuta tu aplicación y verás que ahora ya tiene la misma orientación que tu MainActivity.

Personalizando el escaner

Aunque es algo limitado, tenemos la posibilidad de personalizar un poco el escaner, para ello tenemos que volver a la función initScanner() y vamos a trabajar con la clase IntentIntegrator que ya usábamos para inicializar es escaneo.

Vamos a ver algunas de las propiedades más interesantes y luego las aplicaremos.

  • setDesiredBarcodeFormats: Podemos definir el tipo de códigos que queremos poder escanear. Simplemente pon IntentIntegrator. (con el punto) y te saldrán todas las opciones disponibles.
  • setPrompt: Añade un mensaje en la parte inferior de la pantalla del escaner.
  • setTorchEnabled: Recibe un booleano, si es true, se activará el flash para escanear, por defecto viene a false.
  • setBeepEnabled: Si lo ponemos a true sonará un pitido cuando se haga un escaneo correctamente.

Es el momento de cambiar un poco la función.

  private fun initScanner() {
        val integrator = IntentIntegrator(this)
        integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
        integrator.setPrompt("Sigue aprendiendo en CursoKotlin.com")
        integrator.setTorchEnabled(true)
        integrator.setBeepEnabled(true)
        integrator.initiateScan()
    }

Con esto ya tendríamos nuestra app completa. Si te ha gustado y quieres participar, deja en los comentarios tu librería favorita.

También quiero recordarte que tienes todo el contenido del curso de Android en Kotlin desde cero totalmente gratuito.