Pular para o conteúdo principal

Executar uma tarefa em background no Android usando React Native

Pegue um café e se ajeite na cadeira, pois vou contar uma história um pouco longa a respeito de como executar serviços em segundo plano com React-Native e imagino que vai servir mais como um norte na hora de desenvolver esse tipo de funcionalidade.
Primeiro, pesquisei na documentação oficial do React, e lá é indicado um recurso chamado Headless JS, que permite criar uma ponte entre o JS e o código nativo, e executar tarefas em background.
Em seguida, fiz uma pesquisa sobre como funcionam as tarefas em segundo plano no Android para entender quais são as possibilidades e prós e contras de cada abordagem. Sobre as abordagens, temos:

# WorkManager
A API WorkManager facilita a programação de tarefas adiáveis e assíncronas que precisam ser executadas mesmo se o app fechar ou o dispositivo reiniciar.
Principais recursos:

  • Compatível com versões anteriores até a API 14
  • Usa o JobScheduler em dispositivos com a API 23 ou posteriores
  • Usa uma combinação de BroadcastReceiver + AlarmManager em dispositivos com API 14 a 22
  • Adiciona restrições de trabalho, como disponibilidade de rede ou status de carregamento
  • Programa tarefas pontuais ou periódicas assíncronas
  • Monitora e gerencia tarefas programadas
  • Encadeia tarefas
  • Garante a execução de tarefas, mesmo que o app ou dispositivo seja reiniciado
  • Tem recursos de economia de energia, como o modo "Soneca"

O WorkManager é destinado a tarefas que sejam adiáveis, ou seja, que não precisem ser executadas imediatamente, e que precisem ser executadas de forma confiável, mesmo que o app feche ou o dispositivo reinicie. Por exemplo: Envio de registros ou análises para serviços de back-end; ou sincronização periódica de dados do app com um servidor.
O WorkManager não se destina ao trabalho em segundo plano em andamento que possa ser encerrado  com segurança se o processo do app é encerrado ou para tarefas que exijam execução imediata.

# Serviços de primeiro plano
Para o trabalho iniciado pelo usuário que precisa ser executado imediatamente até o fim, use um serviço em primeiro plano. Usar um serviço em primeiro plano informa ao sistema que o app está fazendo algo importante que não pode ser eliminado. Os serviços em primeiro plano ficam visíveis para os usuários por meio de uma notificação não dispensável na bandeja de notificações.

# AlarmManager
Se você precisar executar um job em um horário específico, use o AlarmManager. O AlarmManager inicia seu app, se necessário, para fazer o job no horário especificado. No entanto, se o job não recisar ser executado em um horário específico, o WorkManager é uma opção melhor. O WorkManager consegue equilibrar melhor os recursos do sistema. Por exemplo, se você precisar executar um job a cada hora, mas não precisar que ele seja executado em um horário específico, use o WorkManager para configurar o job recorrente.

# DownloadManager
Se o app estiver realizando downloads HTTP de longa duração, use o DownloadManager. Os clientes podem solicitar o download de um URI para um arquivo de destino específico que pode estar fora do processo do app. O Gerenciador de downloads realizará o download em segundo plano, cuidando das interações HTTP e tentando refazer os downloads após falhas ou em todas as mudanças de conectividade e reinicializações do sistema.

Diante de todas essas informações, estudei os cenários de execução do meu problema e com base nas regras de negócio, a melhor opção foi criar um serviço e executá-lo em primeiro plano, isso garantiria que as diretrizes que precisavam ser verificadas constantemente seriam devidamente atendidas. De qualquer forma, você pode combinar o uso dos recursos como lhe for conveniente, tendo em vista que podem existir cenários diferentes dentro de uma mesma aplicação.
Para termos uma referência da implementação apresento como os serviços foram implementados.

# Estrutura
Dentro de '~/android/app/src/main/java/com/nome_do_projeto', criei os arquivos:
- modules
  |-RoutinesModule.java
- packages
  |-RoutinesPackage.java
- receivers
  |-BootupReceiver.java
- services
  |-RoutineService.java
  |-RoutinesServiceRunner.java
Não vou entrar em detalhes de implementação, mas de um modo geral:

  • RoutinesModule - É a implementação da API de recursos nativos para o React e basicamente serve de interface para executar os serviços que desejamos;
  • RoutinesPackage - É o arquivo que empacota nosso módulo;
  • BootupReceiver - Implementa um BroadcastReceiver que no boot do SO inicia o processo;
  • RoutineService - É uma interface que vai permitir o bind o código que desenvolvermos em JS ser executado dentro do contexto do serviço;
  • RoutinesServiceRunner - Ele não precisaria, contudo, criei para gerenciar um handler que permitiria o serviço ser restartado dentro de um ciclo temporal.
Além desses arquivos, você poderá observar que adicionei permissões no AndroidManifest.xml:
-------------------
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
-------------------


E inseri a referência dos serviços e do receiver implementados:
-------------------
<service 
android:name="com.example.services.RoutinesServiceRunner" 
android:enabled="true" 
android:exported="false" />
<service android:name="com.example.services.RoutinesService" />
<receiver
  android:name="com.example.receivers.BootupReceiver"
  android:enabled="true"
  android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
  <intent-filter>
    <action android:name="android.intent.action.BOOT_COMPLETED" />
    <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
</receiver>
-------------------


E por último, inseri o load do nosso package no arquivo 'MainApplication.java':
-------------------
@Override
protected List<ReactPackage> getPackages() {
   @SuppressWarnings("UnnecessaryLocalVariable")
   List<ReactPackage> packages = new PackageList(this).getPackages();
   packages.add(new RoutinesPackage());
   return packages;
}
-------------------

Agora você já conseguirá acessar a interface do módulo.


# Como usar?

Primeiro, é necessário registrar a tarefa no contexto de execução do app. Pra isso, é só adicionar usando registerHeadlessTask, no arquivo './index.js' da sua aplicação. Veja:
-------------------
mport {AppRegistry} from 'react-native';
import App from './src';
import {name as appName} from './app.json';
import {headlessTask} from '~/tasks/BackgroundRoutines';

AppRegistry.registerHeadlessTask('BackgroundRoutines', () => headlessTask);
AppRegistry.registerComponent(appName, () => App);
-------------------


Observe que abstraí o código que será executado dentro do domínio de '~/tasks'. Veja:

-------------------
import {NativeModules} from 'react-native';
const {BackgroundRoutines= NativeModules;

const headlessTask = async () => {
  const time = new Date().toLocaleTimeString('pt-BR', {
    timeZone: 'America/Sao_Paulo',
  });
  const status = `Executou a tarefa às ${time}`;
  console.log('BACKGROUND > 'status);
};

export {BackgroundRoutinesheadlessTask};
-------------------

Perceba que a função headlessTask é de fato o código que será executado. Para iniciar o serviço, é só importar essa task e usá-la, veja:

-------------------
import {NativeModules} from 'react-native';
import BackgroundRoutines from '~/tasks/BackgroundRoutines';

//... depois use como desejar, dessa forma
BackgroundRoutines.start();

-------------------

Deixei esse guia aqui pra servir de orientação inicial, mas recomendo fortemente ler as documentações oficiais das tecnologias envolvidas e analisar sempre o cenário em que vai utilizar, para fazer uma escolha mais adequada. Criei um gist com as implementações que você pode conferir aqui.

REFERÊNCIAS


Comentários

Postagens mais visitadas deste blog

Como serializar todos os dados de um formulário em um objeto JSON usando jQuery? #fastTip

Um dos pilares de um código bem escrito é: DRY (Dont Repeat Yourself - não se repita). Acontece que muitas vezes nos deparamos com situações que por conta da necessidade de entrega expressa (famoso código miojo - feito em 2 minutos), acabamos deixando de aplicar boas práticas. O que difere um amador de um profissional é a sua capacidade de avaliar o impacto que um código mal escrito causa e identificar os caminhos pra resolver esses impasses. Enfim, vamos ao que interessa: O Problema Desejo criar um objeto JSON à partir de um formulário, para ser enviado em uma requisição assíncrona. Atualmente, o objeto é criado assim: var data = { name: $('input[name="name"]').val(), email: $('input[name="email"]').val(), }; Olhando pra esse código, vemos diversos problemas: Se mudar o name do input, precisará ir lá no código para alterar a atribuição. Se houver um formulário com 20 campos, terá que fazer 20 atribuições, e assim por diante. A Soluçã

Prefira exceções à códigos de erros #FastTip #CleanCode

Gostaria de compartilhar com vocês uma técnica que aprendi a me acostumar ao longo dos anos, mas que conheci pelo livro do Robert C. Martin, o Clean Code . De forma bem sintetizada, o autor relata que uma boa prática é retornar exceções, ao invés de códigos de erro, pois isso evita a escrita de códigos de verificação, e mantém ele enxuto, mais confiável, sem aberturas para bugs relacionados à erros não previstos. Esse conteúdo você encontrará no capítulo 3 (página 46 - 47) e no capítulo 7 (página 103 - 112), do livro supracitado. Aí como exemplo, desenvolvi um pequeno código Javascript que pode servir de referência para os demais, por ser simples de entender. // ----------------------------- // Criei uma classe genérica que permite acesso ao objeto // Error, padrão do JS, e escrevo a stack trace nomeada // à partir de um Error genérico class GenericError extends Error { constructor(message) { super(message); this.name = this.constructor.name; Error.captureStackTrace(t