Noções básicas do WorkManager, como usar o WorkManager com Rxjava2 e Kotlin Coroutines

Tempo de leitura: 7 minutes

Aprenda a lidar com o trabalho em tempo real com o gerenciador de trabalho.

 

Visão geral

Uma coisa que muda constantemente no ambiente Android é como lidamos com tarefas em segundo plano. Isso ocorre porque quando o número de aplicativos aumenta no telefone, o número de tarefas em segundo plano também aumenta, a maioria dos aplicativos não verifica a integridade da bateria antes de iniciar seu trabalho, o que leva ao esgotamento da bateria em um curto espaço de tempo.

 

Problema

Para controlar as atividades de drenagem da bateria, o Android lançou recursos de economia de bateria como App stand by, limite de transmissões implícitas, limites de serviço em segundo plano, mais limites de transmissão, Economia de bateria e muito mais. Como um desenvolvedor Android, você deve trabalhar com esses recursos de economia de bateria em todos os níveis de API. Se você não fizer isso corretamente, você corre o risco de suas tarefas em segundo plano não rodarem em todos os dispositivos.

 

Solução

É aqui que entra o WorkManager; O WorkManager fornece uma solução unificada para executar suas tarefas em segundo plano. WorkManager é uma biblioteca jetpack para gerenciar o trabalho em segundo plano adiável e garantido.

Adiável: a tarefa pode ser executada mais tarde e ainda ser útil, como enviar análises para o seu servidor.

Garantido: a tarefa pode ser executada mesmo se seu aplicativo não estiver em execução ou mesmo após a reinicialização do telefone, como enviar arquivos grandes para o servidor.

 

WorkManager é

  1. Compatível com API de nível 14.
  2. Funciona com/sem os serviços do Google Play.

Com todos os recursos acima do gerenciador de trabalho, ele se tornou uma solução viável para fazer tarefas em segundo plano. Agora que sabemos por que usar o workmanager, vamos ver como usá-lo.

 

Integração WorkManager

WorkManager é uma biblioteca jetpack e tem sido constantemente atualizada para novas versões quando este artigo foi escrito “2.1.0” é a versão estável mais recente.

api "androidx.work:work-runtime-ktx:2.1.0"

 

O que você deve saber antes de usar o WorkManager

Há duas coisas que você deve saber antes de usar o gerenciador de trabalho: são as restrições e os tipos de solicitações de trabalho.

Constraints: as Constraints nada mais são do que uma forma de comunicar ao gerente de trabalho que o sistema deve atender a certas condições para executar uma solicitação de trabalho. Essas condições podem ser qualquer coisa como o sistema deve ter uma conexão com a Internet, o sistema não deve estar no modo de economia de bateria, etc.

val constraints = Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build()

Work Requests: Existem dois tipos de solicitação de trabalho, são solicitações de trabalho único e periódico.

One-Time Work Request: pelo próprio nome, podemos dizer que esta solicitação é executada apenas uma vez.

val workerinstance = OneTimeWorkRequest.Builder(SampleWorker::class.java)
            .setConstraints(constraints)
            .build()

Periodic Work Request : essa solicitação é executada periodicamente assim que iniciamos o trabalhador. Temos que mencionar um certo intervalo de tempo entre as execuções durante a construção de um objeto trabalhador.

val workerInstance = PeriodicWorkRequest.Builder(
              SampleWorker::class.java, 12, TimeUnit.HOURS)
             .setConstraints(constraints)
             .build()

 

Basic Worker

Para criar um trabalhador, estenda a classe com trabalhador e importe o método doWork() que é executado em segundo plano fornecido pelo workmanager. O exemplo abaixo é um trabalhador simples cuja tarefa é fazer upload de uma imagem para o servidor.

class UploadWorker(appContext: Context, workerParams: WorkerParameters)
    : Worker(appContext, workerParams) {

    override fun doWork(): Result {
        // Faça o trabalho aqui - neste caso, carregue as imagens.
       
        try {
            uploadImages()
        } catch (e: Exception) {
            e.printStackTrace()
            return Result.failure()
        }

        // Indica se a tarefa terminou com sucesso com o Resultado
        return Result.success()
    }
}

O tipo de retorno Result de doWork () informa ao gerente de trabalho se o trabalho é

  • terminou com sucesso via Result.success()
  • falhou via Result.failure()
  • precisa ser tentado novamente mais tarde por meio de Result.retry()

Depois de criar o trabalhador, com base em sua necessidade, crie uma solicitação de trabalho periódico ou de uma hora e enfileire com o gerenciador de trabalho conforme mostrado abaixo.

// cria um objeto de restrição com as condições necessárias para iniciar o trabalhador
val constraints = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)
        .build()
        
// Criar trabalhador periódico com restrições e intervalo de 12 horas
val uploadWorker = PeriodicWorkRequest.Builder(
        UploadWorker::class.java, 12, TimeUnit.HOURS)
        .setConstraints(constraints)
        .build()

WorkManager.getInstance().enqueue(uploadWorker)

 

Recursos úteis do WorkManager

1. Delay

Se o seu trabalho não tiver restrições ou se todas as restrições forem atendidas quando o seu trabalho for colocado na fila, o sistema pode escolher executar a tarefa imediatamente. Se você não quiser que a tarefa seja executada imediatamente, você pode especificar que seu trabalho comece após um atraso inicial mínimo.

val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
        .setInitialDelay(10, TimeUnit.MINUTES)
        .build()

 

2. Definição de entrada para sua tarefa

E se o trabalhador precisar de alguns dados para iniciar, como o URL da imagem necessário para o trabalhador acima fazer o download da imagem.

Os valores de entrada e saída são armazenados como pares de valores-chave em um objeto de dados. O código abaixo mostra como você pode definir dados de entrada em seu WorkRequest.

// workDataOf (parte do KTX) converte uma lista de pares em um objeto [Dados].
val imageData = workDataOf(Constants.KEY_IMAGE_URI , imageUriString)

val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
        .setInputData(imageData)
        .build()

Podemos acessar os dados enviados ao trabalhador usando getInputData() conforme mostrado abaixo

val imageUriInput = getInputData().getString(Constants.KEY_IMAGE_URI)

3. Tag a workrequest

Você pode marcar uma solicitação com uma string longa usando a função addTag. Isso será útil para agrupar várias solicitações com uma string. Isso permite que você opere em todas as tarefas com uma tag específica. Você também pode cancelar todas as tarefas com uma única tag usando WorkManager.cancelAllWorkByTag(String).

val cacheCleanupTask =
        OneTimeWorkRequestBuilder<CacheCleanupWorker>()
    .setConstraints(constraints)
    .addTag("cleanup")
    .build()

 

4. Observe o status do gerente de trabalho

Para entender isso, você deve saber como o LiveData funciona no Android. O WorkManager fornece uma função getWorkInfoByIdLiveData() com tipo de retorno como Livedata <WorkInfo>, portanto, observando esse liveata, obtemos informações do trabalhador que contém o status da solicitação e muito mais.

WorkManager.getInstance().getWorkInfoByIdLiveData(uploadWorker.id).observe(this, Observer {

         it?.let {
               if (it.state == WorkInfo.State.SUCCEEDED) {
               }
           }
        })

Alternativamente, você também pode observar o status do trabalhador usando tags.

 

WorkManager com RxJava2

O Workmanager oferece suporte à programação reativa fora da caixa. Tudo que você precisa fazer é importar a linha a seguir para o arquivo build.grade().

api "android.arch.work:work-rxjava2:2.1.0"

Agora, em vez de estender a classe com Worker, estenda-a com RxWorker e substitua createWork() em vez de doWork() para retornar um único <Result> indicando o Resultado de sua execução, como segue:

public class RxDownloadWorker extends RxWorker {

    public RxDownloadWorker(Context context, WorkerParameters params) {
        super(context, params);
    }

    @Override
    public Single<Result> createWork() {
        return Observable.range(0, 1)
            .flatMap { download("https://www.google.com") }
            .toList()
            .map { Result.success() };
    }
}

É apenas um Rxworker básico com uma única operação. Enquanto em tempo real você pode querer fazer várias operações, algumas podem ser chamadas de serviço, salvando dados no banco de dados local ou cálculos de dados. Vamos ver como podemos fazer isso

public class RxDownloadWorker extends RxWorker {

    public RxDownloadWorker(Context context, WorkerParameters params) {
        super(context, params);
    }

    @Override
    public Single<Result> createWork() {
        return Observable.range(0, 1)
            .flatMap { 
                 getShowsDetailsFromServer() 
            }
            .map { showServerdata->
                formateShowDetailsfromserver(showServerdata) 
            }
            .flatMapSingle { showFromatedData->
                saveShowDetailsToLocalDB(showFromatedData) 
            }
            .toList()
            .map { Result.success() };
    }
}

 

 

WorkManager com Kotlin Coroutines

O Workmanager oferece suporte imediato às corrotinas Kotlin. Tudo que você precisa fazer é importar a linha a seguir para o arquivo build.grade().

implementation "androidx.work:work-runtime-ktx:$2.2.0"

Agora, em vez de estender a classe com Worker, estenda-a com CoroutineWorker e substitua doWork(). Observe que doWork é uma função de suspensão aqui e, se necessário, podemos substituir a variável coroutineContext e atribuir um contexto a ela.

class CoroutineDownloadWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {

    override val coroutineContext = Dispatchers.IO

    override suspend fun doWork(): Result = coroutineScope {
        val jobs = (0 until 100).map {
            async {
                downloadSynchronously("https://www.google.com")
            }
        }

        // awaitAll lançará uma exceção se um download falhar, o que o CoroutineWorker tratará como uma falha
        jobs.awaitAll()
        Result.success()
    }
}

 

 

Trabalho em tempo real com o gerente de trabalho

Agora que sabemos como usar o gerenciador de trabalho com as corrotinas Rxjava2 e Kotlin, vamos ver como podemos usar esse conhecimento para resolver problemas em tempo real.

Vamos considerar o problema muito comum que qualquer aplicativo com um servidor de back-end enfrentará, ou seja, sincronizar dados do banco de dados local para o servidor remoto.

Aqui temos um aplicativo de rastreamento de programas de TV com suporte offline. Isso significa que este aplicativo pode funcionar sem internet se carregar os dados de seus servidores pelo menos uma vez na vida. Portanto, considere uma situação em que o usuário está no modo offline e ele marcou alguns dos episódios como assistidos, nesta situação salvamos as alterações no banco de dados local e aguardamos a conexão com a internet. Mas e se o usuário fechasse o aplicativo, o telefone fosse reiniciado ou qualquer outra coisa pudesse acontecer.

Esta será a situação perfeita onde a Solicitação de Trabalho Periódico será útil. Digamos que uma solicitação de trabalho periódica seja acionada quando o usuário fizer login pela primeira vez. Esta solicitação de trabalho sincroniza os dados do banco de dados local com seus servidores remotos a cada 12 horas. Vamos ver como podemos iniciar um gerenciador de trabalho

Requisitos: conexão com a Internet necessária, a carga da bateria do telefone não deve ser menor e deve ser acionada a cada intervalo de 12 horas.

// cria um objeto de restrição com as condições necessárias para iniciar o trabalhador
val constraints = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)
        .setRequiresBatteryNotLow(true)
        .build()
        
// Criar trabalhador periódico com restrições e intervalo de 12 horas 
val dataSyncWorker = PeriodicWorkRequest.Builder(
        DataSyncWorker::class.java, 12, TimeUnit.HOURS)
        .setConstraints(constraints)
        .build()

WorkManager.getInstance().enqueue(dataSyncWorker)

O código acima certifique-se de que um trabalhador será acionado a cada 12 horas com as condições especificadas. Não vamos ver como fazemos as coisas nesse trabalhador

Aqui, primeiro precisamos obter dados do banco de dados local, digamos que usamos o banco de dados de sala para o banco de dados local. O código abaixo mostra como obter dados do banco de dados da Room.

fun getAllshowsFromDB(): Single<List<ShowDetails>> {
        if (applicationDatabase == null) {
            applicationDatabase = ApplicationDatabase.getInstance(applicationContext)
        }
        return applicationDatabase.allShowsDataDao()
                .getAllShowsAllData()
    }

Depois disso, devemos filtrar quais programas precisam ser atualizados.

private fun filterShowsToBeUpdated(arraylistShows : List<ShowDetails>) 
                  : List<ShowDetails> {
    return arraylistShows.filter { it.isUpdateRequired }
}

Agora precisamos atualizar esses programas para o servidor remoto.

fun updateShowsToServer(list: List<ShowDetails>): Single<ShowsUpdateResponse> {
    if (repo == null) {
        repo = TvShowsRepoImpl()
    }
     return repo.updateShows(list)
}

Agora é hora de juntar as peças

class DataSyncWorker(context: Context,
                     workerParams: WorkerParameters) :
        RxWorker(context, workerParams) {

    var arraylistShowToUpdate : ArrayList<ShowDetails> = ArrayList()

    override fun createWork(): Single<Result> {
        return Observable.range(0, 1)

                /**
                 * Recuperando todos os programas no banco de dados local
                 */
                .flatMapSingle {
                    getAllshowsFromDB()
                }

                 /**
                 * Filtragem de programas de atualização necessária
                 */
                .map{
                    arraylistShowToUpdate = filterShowsToBeUpdated(it)
                }

                 /**
                 * Disparar atualização para servidor remoto apenas se houver
                 * programas necessários para sincronizar
                 */
                .filter{
                    return@filter arraylistShowToUpdate.size > 0
                }

                /**
                 * Atualizar programas para servidor remoto
                 */
                .flatMapSingle {
                    updateShowsToServer(arraylistShowToUpdate)
                }

                .toList()

                /**
                 * sucesso de retorno
                 */
                .map {
                    Result.success()
                }

                /**
                 * falha de retorno
                 */
                .onErrorReturn {
                    Result.failure()
                }
    }
}

 

Links Úteis:

Se você quer saber mais ou ainda tem alguma dúvida

Leia a documentação oficial
Se você deseja integrar o Dagger ao Workmanager, leia este artigo

Sinta-se à vontade para comentar se houver sugestões ou melhorias.

Obrigado por ler.