Noções básicas da Cronet

1. Introdução

1ee223bf9e1b75fb.png

Última atualização: 06/05/2022

A Cronet é a pilha de rede do Chromium disponibilizada para os apps Android como uma biblioteca. Ela usa várias tecnologias para diminuir a latência e aumentar a capacidade de processamento das solicitações de rede necessárias para o app funcionar.

A biblioteca Cronet processa as solicitações de apps usados por milhões de pessoas diariamente, como o YouTube, o Google app, o Google Fotos e o Maps. Essa é a biblioteca de rede do Android com suporte para HTTP3 mais usada.

Para mais detalhes, acesse a página Recursos da Cronet.

O que você vai criar

Neste codelab, você vai adicionar suporte para a Cronet a um app de exibição de imagem. Esse app vai:

  • Carregar a Cronet pelo Google Play Services ou retornar com segurança se ela não estiver disponível.
  • Enviar solicitações, além de receber e processar respostas usando a Cronet.
  • Mostrar os resultados em uma IU simples.

28b0fcb0fed5d3e0.png

O que você vai aprender

  • Como incluir a Cronet como uma dependência no seu app.
  • Como configurar o mecanismo da Cronet.
  • Como usar a Cronet para enviar solicitações.
  • Como criar callbacks da Cronet para processar as respostas.

O foco deste codelab é o uso da Cronet. A maior parte do app foi pré-implementada, e você vai conseguir terminar o codelab mesmo com pouca experiência em desenvolvimento para Android. Dito isso, para aproveitar ao máximo este codelab, você precisa entender os conceitos básicos de desenvolvimento para Android e da biblioteca do Jetpack Compose.

O que vai ser necessário

2. Buscar o código

Colocamos tudo o que você precisa para este projeto em um repositório Git. Para começar, clone o repositório e abra o código no Android Studio.

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

3. Definir um valor de referência

Qual é nosso ponto de partida?

Nosso ponto de partida é um app básico de exibição de imagens desenvolvido para este codelab. Se você clicar no botão Add an image, uma nova imagem vai ser adicionada à lista, assim como detalhes do tempo necessário para buscar essa imagem na Internet. O app usa uma biblioteca HTTP integrada fornecida pelo Kotlin, que não tem suporte a nenhum recurso avançado.

Neste codelab, vamos ampliar o aplicativo para usar a Cronet e alguns dos recursos dela.

4. Adicionar dependências ao script do Gradle

Você pode integrar a Cronet como uma biblioteca independente transferida com seu app ou usá-la conforme fornecida pela plataforma. A equipe da Cronet recomenda o uso do provedor do Google Play Services. Ao usar esse provedor, seu app não precisa pagar o custo do tamanho binário de carregar a Cronet (cerca de 5 megabytes). Além disso, a plataforma garante que as atualizações e correções de segurança mais recentes sejam entregues.

Independentemente de como você decidir importar a implementação, também vai ser necessário adicionar uma dependência cronet-api para incluir as APIs da Cronet.

Abra o arquivo build.gradle e adicione as duas linhas a seguir à seção dependencies.

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

5. Instalar o provedor Cronet do Google Play Services

Como discutido na seção anterior, a Cronet pode ser adicionada ao seu app de várias maneiras. Cada uma dessas formas é abstraída por um Provider, que garante a existência dos links necessários entre a biblioteca e o app. Toda vez que você cria um novo mecanismo, a Cronet analisa todos os provedores ativos e escolhe o melhor para fazer a instanciação.

O provedor do Google Play Services normalmente não está pronto para uso. Portanto, ele precisa ser instalado primeiro. Localize o código TODO em MainActivity e cole o seguinte snippet:

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

Isso inicia uma API Task do Play Services, que instala o provedor de forma assíncrona.

6. Processar o resultado da instalação do provedor

Você instalou o provedor… Espere! Será que instalou mesmo? A Task é assíncrona e você não processou o resultado de forma alguma. Vamos corrigir isso. Substitua a invocação installProvider pelo seguinte snippet:

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)
   }
}

Para os fins deste codelab, vamos continuar usando a ferramenta de download de imagem nativa se o carregamento da Cronet falhar. Se a performance da rede for essencial para o app, recomendamos que você instale ou atualize o Google Play Services. Para ver mais detalhes, consulte a documentação CronetProviderInstaller.

Execute o app agora. Se tudo funcionar, um log statement vai ser mostrado informando que o provedor foi instalado.

7. Criar um mecanismo da Cronet

O mecanismo da Cronet é o objeto principal que você vai usar para enviar solicitações com essa biblioteca. O mecanismo é construído usando o padrão Builder (link em inglês), que permite configurar várias opções da Cronet. Por enquanto, vamos continuar usando as opções padrão. Instancie um novo mecanismo da Cronet substituindo o TODO pelo seguinte snippet:

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

8. Implementar um callback da Cronet

A natureza assíncrona da Cronet significa que o processamento da resposta é controlado usando callbacks, ou seja, instâncias de UrlRequest.Callback. Nesta seção, você vai implementar um callback auxiliar que lê toda a resposta para a memória.

Crie uma nova classe abstrata com o nome ReadToMemoryCronetCallback, faça com que ela estenda o UrlRequest.Callback e deixe o Android Studio gerar automaticamente os stubs de método. A nova classe vai ser semelhante a este snippet:

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")
   }
}

Os métodos onRedirectReceived, onSucceeded e onFailed são autoexplicativos (redirecionamento recebido, êxito e falha, respectivamente), então não entraremos em detalhes agora. Vamos nos concentrar no onResponseStarted (resposta recebida) e no onReadCompleted (leitura concluída).

O onResponseStarted é chamado depois que a Cronet envia a solicitação e recebe todos os cabeçalhos de resposta, mas antes de ela começar a ler o corpo. A Cronet não lê automaticamente todo o corpo como algumas outras bibliotecas (por exemplo, a Volley). Use UrlRequest.read() para ler a próxima parte do corpo em um buffer fornecido por você. Quando a Cronet terminar de ler a parte do corpo da resposta, ele vai chamar o método onReadCompleted. O processo se repete até que não haja mais dados para ler.

39d71a5e85f151d8.png

Vamos começar a implementar o ciclo de leitura. Primeiro, instancie um novo stream de saída da matriz de bytes e um canal que o use. Vamos utilizar o canal como um coletor para o corpo da resposta.

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

Em seguida, implemente o método onReadCompleted para copiar os dados do buffer de bytes para nosso coletor e invocar a próxima leitura.

// 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 concluir a repetição de leitura do corpo, invoque a leitura inicial do método de callback onResponseStarted. É necessário usar um buffer de byte direto com a Cronet. A capacidade do buffer não importa para os fins do codelab, mas 16 KiB é um bom valor padrão para a maioria dos usos de produção.

request.read(ByteBuffer.allocateDirect(BYTE_BUFFER_CAPACITY_BYTES))

Vamos concluir o restante da aula. Os redirecionamentos não são muito interessantes aqui, então basta seguir o redirecionamento como em um navegador da Web.

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

Por fim, precisamos processar os métodos onSucceeded e onFailed. O onFailed é igual à assinatura que você quer fornecer aos usuários do callback auxiliar, então você pode excluir a definição e permitir que as classes estendidas modifiquem o método. O onSucceeded precisa transmitir o corpo downstream como uma matriz de bytes. Adicione um novo método abstrato com o corpo na assinatura.

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

Em seguida, verifique se o novo método onSucceeded é chamado corretamente quando a solicitação é concluída.

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

Isso aí! Você aprendeu a implementar um callback da Cronet.

9. Implementar uma ferramenta de download de imagem

Vamos usar o callback que criamos na seção anterior para implementar uma ferramenta de download de imagem baseada na Cronet.

Crie uma nova classe com o nome CronetImageDownloader implementando a interface ImageDownloader e aceitando um CronetEngine como parâmetro do construtor.

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

Para implementar o método downloadImage, você precisa aprender a criar solicitações da Cronet. É fácil: basta chamar o método newUrlRequestBuilder() do seu CronetEngine. Esse método usa o URL, uma instância da sua classe de callback e um executor que executa os métodos do callback.

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

O URL é mostrado pelo parâmetro downloadImage. Para o executor, vamos criar um campo para toda a instância.

private val executor = Executors.newSingleThreadExecutor()

Por fim, usamos a implementação de callback auxiliar da seção anterior para implementar o callback. Não vamos entrar em detalhes sobre a implementação, já que esse tópico é mais relacionado às corrotinas de Kotlin (link em inglês). Considere cont.resume como um return do método downloadImage.

Juntando tudo, a implementação de downloadImage vai ser semelhante ao snippet a seguir.

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. Toques finais

Vamos voltar ao elemento combinável MainDisplay e abordar o último TODO usando a ferramenta de download de imagem que acabamos de criar.

imageDownloader = CronetImageDownloader(cronetEngine)

E pronto! Tente executar o app. As solicitações vão ser encaminhadas pela ferramenta de download de imagem da Cronet.

11. Personalização

É possível personalizar o comportamento das solicitações, tanto no nível da solicitação quanto no do mecanismo. Vamos demonstrar isso com o armazenamento em cache, mas há várias outras opções. Para mais detalhes, consulte a documentação UrlRequest.Builder e CronetEngine.Builder.

Para ativar o armazenamento em cache no nível do mecanismo, use o método enableHttpCache do builder. No exemplo abaixo, usamos um cache na memória. Para ver outras opções disponíveis, consulte a documentação. A criação do mecanismo da Cronet vai ficar assim:

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

Execute o app e adicione algumas imagens. Aquelas que foram adicionadas repetidamente precisam ter uma latência significativamente menor, e a IU deve indicar que elas foram armazenadas em cache.

Essa funcionalidade pode ser substituída em cada solicitação. Vamos fazer uma pequena mudança na nossa ferramenta de download da Cronet e desativar o armazenamento em cache da imagem do sol, que é a primeira da lista de URLs.

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

request.build().start()

Agora, execute o aplicativo novamente. Você vai perceber que as imagens do sol não estão sendo armazenadas em cache.

d9d0163c96049081.png

12. Conclusão

Parabéns! Você chegou ao fim do codelab. No processo, você aprendeu as noções básicas de como usar a Cronet.

Para saber mais sobre essa biblioteca, consulte o guia para desenvolvedores e o código-fonte. Além disso, inscreva-se no Blog de desenvolvedores Android (link em inglês) para ficar por dentro das novidades da Cronet e das notícias gerais sobre o Android.