Cronet Basics

1. Introducción

1ee223bf9e1b75fb.png

Última actualización: 06/05/2022

Cronet es la pila de red de Chromium que está disponible para las apps de Android como una biblioteca. Cronet aprovecha las múltiples tecnologías que reducen la latencia y aumentan la capacidad de procesamiento de las solicitudes de red que tu app necesita para funcionar.

La Biblioteca de Cronet controla las solicitudes de apps que usan millones de personas a diario, como YouTube, Google app, Google Fotos y Maps: Navigation y Google Transit. Cronet es la biblioteca de herramientas de redes de Android más utilizada con compatibilidad con HTTP3.

Para obtener más información, consulta la página de Funciones de Cronet.

Qué compilarás

En este codelab, agregarás compatibilidad con Cronet a una aplicación de visualización de imágenes. Tu app hará lo siguiente:

  • Cargar Cronet desde los Servicios de Google Play o recurrir a la seguridad si Cronet no está disponible
  • Enviar solicitudes y recibir y procesar respuestas con Cronet
  • Mostrar los resultados en una IU simple

28b0fcb0fed5d3e0.png

Qué aprenderás

  • Cómo incluir Cronet como dependencia en tu app
  • Cómo configurar el motor Cronet
  • Cómo usar Cronet para enviar solicitudes
  • Cómo escribir devoluciones de llamada de Cronet para procesar las respuestas

Este codelab está enfocado en el uso de Cronet. La mayor parte de la aplicación ya está implementada y podrás completar el codelab con poca experiencia previa en el desarrollo de Android. Dicho esto, para aprovechar al máximo este codelab, debes comprender los conceptos básicos del Desarrollo de Android y la biblioteca de Jetpack Compose.

Requisitos

2. Obtén el código

Todo lo que necesitas con relación a este proyecto se encuentra en un repositorio de Git. Para comenzar, clona el repositorio y abre el código en Android Studio.

git clone https://github.com/android/codelab-cronet-basics

3. Establece un modelo de referencia

¿Cuál es nuestro punto de partida?

Nuestro punto de partida es una app básica de visualización de imágenes diseñada para este codelab. Si haces clic en el botón Agregar una imagen, verás que se agrega una imagen nueva a la lista junto con los detalles de cuánto tiempo tardó en recuperarla desde Internet. La aplicación usa una biblioteca HTTP integrada que proporciona Kotlin y no admite funciones avanzadas.

En este codelab, extenderemos la aplicación para usar Cronet y algunas de sus funciones.

4. Agrega dependencias a tu secuencia de comandos de Gradle

Puede integrar Cronet como una biblioteca independiente incluida en su aplicación, o bien utilizar Cronet como se proporciona en la plataforma. El equipo de Cronet recomienda utilizar el proveedor de los Servicios de Google Play. Si usas el proveedor de los Servicios de Google Play, tu aplicación no tiene que pagar el costo de tamaño del objeto binario por transportar Cronet (aproximadamente 5 megabytes), y la plataforma garantiza que se proporcionen las actualizaciones y correcciones de seguridad más recientes.

Independientemente de cómo decidas importar la implementación, también deberás agregar una dependencia cronet-api para incluir las API de Cronet.

Abre el archivo build.gradle y agrega las siguientes dos líneas a la sección dependencies.

implementation 'com.google.android.gms:play-services-cronet:18.0.1'
implementation 'org.chromium.net:cronet-api:101.4951.41'

5. Instala el proveedor de Cronet de Servicios de Google Play

Como se explicó en la sección anterior, Cronet puede agregarse a su aplicación de varias formas. Cada uno de estos métodos se abstrae de un Provider, que garantiza que se establezcan los vínculos necesarios entre la biblioteca y tu aplicación. Cada vez que creas un nuevo motor de Cronet, este analiza todos los proveedores activos y selecciona el mejor para crear una instancia del motor.

El proveedor de Servicios de Google Play no suele estar disponible de inmediato, por lo que primero debes instalarlo. Ubica el TODO en MainActivity y pega el siguiente fragmento:

val ctx = LocalContext.current
CronetProviderInstaller.installProvider(ctx)

Esto inicia una Tarea de Servicios de Play que instala el proveedor de forma asíncrona.

6. Controla el resultado de la instalación del proveedor

Instalaste correctamente el proveedor… ¿verdad? El Task es asíncrono y no controlaste el resultado de ninguna manera. Sin embargo, podemos solucionarlo. Reemplaza la invocación installProvider por el siguiente fragmento:

CronetProviderInstaller.installProvider(ctx).addOnCompleteListener {
   if (it.isSuccessful) {
       Log.i(LOGGER_TAG, "Successfully installed Play Services provider: $it")
       // TODO(you): Initialize Cronet engine
   } else {
       Log.w(LOGGER_TAG, "Unable to load Cronet from Play Services", it.exception)
   }
}

A los fines de este codelab, continuaremos usando la herramienta de descarga de imágenes nativa si falla la carga de Cronet. Si el rendimiento de las herramientas de redes es fundamental para tu aplicación, te recomendamos instalar o actualizar los Servicios de Play. Para obtener más detalles, consulta la documentación CronetProviderInstaller.

Ejecuta la aplicación ahora. Si todo funciona bien, deberías ver una instrucción de registro de que el proveedor se instaló correctamente.

7. Crea un motor de Cronet

Un motor Cronet es el objeto principal que utilizarás para enviar solicitudes con Cronet. El motor se crea con el patrón de compilador, que te permite configurar varias opciones de Cronet. Por ahora, seguiremos usando las opciones predeterminadas. Para crear una instancia de un nuevo motor de Cronet, reemplaza TODO por el siguiente fragmento:

val cronetEngine = CronetEngine.Builder(ctx).build()
// TODO(you): Initialize the Cronet image downloader

8. Implementa una devolución de llamada de Cronet

La naturaleza asíncrona de Cronet significa que el manejo de las respuestas se controla mediante devoluciones de llamada, es decir, instancias de UrlRequest.Callback. En esta sección, implementarás una devolución de llamada de ayuda que lee toda la respuesta a la memoria.

Crea una nueva clase abstracta llamada ReadToMemoryCronetCallback, haz que extienda UrlRequest.Callback y deja que Android Studio genere automáticamente los stubs de los métodos. La clase nueva debería ser similar al siguiente fragmento:

abstract class ReadToMemoryCronetCallback : UrlRequest.Callback() {
   override fun onRedirectReceived(
       request: UrlRequest,
       info: UrlResponseInfo,
       newLocationUrl: String?
   ) {
       TODO("Not yet implemented")
   }

   override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
       TODO("Not yet implemented")
   }

   override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: CronetException) {
       TODO("Not yet implemented")
   }

   override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) {
       TODO("Not yet implemented")
   }

   override fun onReadCompleted(
       request: UrlRequest,
       info: UrlResponseInfo,
       byteBuffer: ByteBuffer
   ) {
       TODO("Not yet implemented")
   }
}

Los métodos onRedirectReceived, onSucceeded y onFailed se explican a sí mismos, por lo que no repasaremos los detalles ahora y nos enfocaremos en onResponseStarted y onReadCompleted.

Se llama a onResponseStarted después de que Cronet envía la solicitud y recibe todos los encabezados de respuesta, pero antes de que comience a leer el cuerpo. Cronet no lee automáticamente todo el cuerpo como algunas otras bibliotecas (por ejemplo, Volley). En su lugar, usa UrlRequest.read() para leer el siguiente fragmento del cuerpo en un búfer que proporciones. Cuando Cronet termina de leer el fragmento de cuerpo de la respuesta, llama al método onReadCompleted. El proceso se repite hasta que no haya más datos para leer.

39d71a5e85f151d8.png

Comencemos a implementar el ciclo de lectura. Primero, crea una instancia de un nuevo flujo de salida de array de bytes y un canal que lo use. Usaremos el canal como receptor para el cuerpo de la respuesta.

private val bytesReceived = ByteArrayOutputStream()
private val receiveChannel = Channels.newChannel(bytesReceived)

A continuación, implementa el método onReadCompleted para copiar los datos del búfer de bytes en nuestro receptor y, luego, invocar la siguiente lectura.

// The byte buffer we're getting in the callback hasn't been flipped for reading,
// so flip it so we can read the content.
byteBuffer.flip()
receiveChannel.write(byteBuffer)

// Reset the buffer to prepare it for the next read
byteBuffer.clear()

// Continue reading the request
request.read(byteBuffer)

Para finalizar el bucle de lectura del cuerpo, invoca la lectura inicial desde el método de devolución de llamada onResponseStarted. Ten en cuenta que debes usar un búfer de bytes directos con Cronet. Si bien la capacidad del búfer no importa para el objetivo del codelab, 16 KiB es un buen valor predeterminado para la mayoría de los usos de producción.

request.read(ByteBuffer.allocateDirect(BYTE_BUFFER_CAPACITY_BYTES))

Terminemos el resto de la clase. Los redireccionamientos no te interesan, por lo que simplemente seguí el redireccionamiento tal como lo haría tu navegador web.

override fun onRedirectReceived(
   request: UrlRequest, info: UrlResponseInfo?, newLocationUrl: String?
) {
   request.followRedirect()
}

Por último, debemos controlar los métodos onSucceeded y onFailed. onFailed coincide con la firma que deseas proporcionar a los usuarios de la devolución de llamada de ayuda para que puedas borrar la definición y permitir que la extensión de clases anule el método. onSucceeded debe pasar el cuerpo de manera descendente como un array de bytes. Agrega un nuevo método abstracto con el cuerpo en su firma.

abstract fun onSucceeded(
   request: UrlRequest, info: UrlResponseInfo, bodyBytes: ByteArray)

Luego, asegúrate de que se llame correctamente al nuevo método onSucceeded cuando la solicitud se complete correctamente.

final override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
   val bodyBytes = bytesReceived.toByteArray()
   onSucceeded(request, info, bodyBytes)
}

¡Bravo! Aprendiste a implementar una devolución de llamada de Cronet.

9. Implementa una herramienta de descarga de imágenes

Usemos la devolución de llamada que creamos en la sección anterior para implementar una herramienta de descarga de imágenes basada en Cronet.

Crea una clase nueva llamada CronetImageDownloader mediante la implementación de la interfaz ImageDownloader y acepta una CronetEngine como su parámetro de constructor.

class CronetImageDownloader(val engine: CronetEngine) : ImageDownloader {
   override suspend fun downloadImage(url: String): ImageDownloaderResult {
       TODO("Not yet implemented")
   }
}

Para implementar el método downloadImage, debes aprender a crear solicitudes de Cronet. Es fácil: llama al método newUrlRequestBuilder() de tu CronetEngine. Este método toma la URL, una instancia de tu clase de devolución de llamada, y un ejecutor para hacer que se ejecuten los métodos de tu devolución de llamada.

val request = engine.newUrlRequestBuilder(url, callback, executor)

La URL es conocida por el parámetro downloadImage. Para el ejecutor, crearemos un campo para toda la instancia.

private val executor = Executors.newSingleThreadExecutor()

Por último, usamos la implementación de devolución de llamada auxiliar de la sección anterior para implementar callback. No entraremos en detalles sobre su implementación, ya que se trata más de un tema de corrutinas de Kotlin. Puedes pensar en cont.resume como una return del método downloadImage.

En conjunto, tu implementación de downloadImage debe parecerse al siguiente fragmento.

override suspend fun downloadImage(url: String): ImageDownloaderResult {
   val startNanoTime = System.nanoTime()
   return suspendCoroutine {
       cont ->
       val request = engine.newUrlRequestBuilder(url, object: ReadToMemoryCronetCallback() {
       override fun onSucceeded(
           request: UrlRequest,
           info: UrlResponseInfo,
           bodyBytes: ByteArray) {
           cont.resume(ImageDownloaderResult(
               successful = true,
               blob = bodyBytes,
               latency = Duration.ofNanos(System.nanoTime() - startNanoTime),
               wasCached = info.wasCached(),
               downloaderRef = this@CronetImageDownloader))
       }

       override fun onFailed(
           request: UrlRequest,
           info: UrlResponseInfo,
           error: CronetException
       ) {
           Log.w(LOGGER_TAG, "Cronet download failed!", error)
           cont.resume(ImageDownloaderResult(
               successful = false,
               blob = ByteArray(0),
               latency = Duration.ZERO,
               wasCached = info.wasCached(),
               downloaderRef = this@CronetImageDownloader))
       }
   }, executor)
       request.build().start()
   }
}

10. Cableado final

Regresemos al elemento MainDisplay componible y abordemos el último TODO usando la herramienta de descarga de imágenes que acabamos de crear.

imageDownloader = CronetImageDownloader(cronetEngine)

¡Eso es todo! Prueba ejecutar la aplicación. Deberías ver que tus solicitudes se enrutan a través de la herramienta de descarga de imágenes de Cronet.

11. Personalización

Puedes personalizar el comportamiento de las solicitudes a nivel de la solicitud y del motor. Lo demostraremos con el almacenamiento en caché, pero hay muchas más opciones. Para obtener más información, consulta la documentación de UrlRequest.Builder y CronetEngine.Builder.

Para habilitar el almacenamiento en caché a nivel del motor, usa el método enableHttpCache del compilador. En el siguiente ejemplo, usamos una caché en la memoria. Para conocer otras opciones disponibles, consulta la documentación. A continuación, se crea el motor Cronet:

val cronetEngine = CronetEngine.Builder(ctx)
   .enableHttpCache(CronetEngine.Builder.HTTP_CACHE_IN_MEMORY, 10 * 1024 * 1024)
   .build()

Ejecuta la aplicación y agrega algunas imágenes. Las que se agreguen con frecuencia deben tener una latencia significativamente más corta y la IU debe indicar que se almacenaron en caché.

Esta funcionalidad se puede anular por solicitud. Pongamos un pequeño hack en nuestra herramienta de descarga de Cronet y, luego, inhabilitaremos el almacenamiento en caché para la imagen del sol, que es la primera de la lista de URL.

if (url == CronetCodelabConstants.URLS[0]) {
   request.disableCache()
}

request.build().start()

Ahora vuelve a ejecutar la aplicación. Ten en cuenta que las imágenes de sol no se almacenan en caché.

d9d0163c96049081.png

12. Conclusión

¡Felicitaciones! Llegaste al final del codelab. En el proceso, aprendiste los conceptos básicos para usar Cronet.

Si desea obtener más información sobre Cronet, consulte la guía para desarrolladores y el código fuente. Suscríbete al Blog para desarrolladores de Android para ser la primera persona en conocer las noticias generales y de Cronet.