Introdução

Uma das principais prioridades para criar uma experiência de usuário perfeita para seu aplicativo é garantir que a IU esteja sempre responsiva e funcione sem problemas. Uma maneira de melhorar o desempenho da IU é mover tarefas de longa execução, como operações de banco de dados, para o segundo plano.

Neste tutorial, você implementa a parte voltada para o usuário do aplicativo TrackMySleepQuality, usando corrotinas Kotlin para executar operações de banco de dados longe da thread principal.

O que você já deveria saber

Você deve estar familiarizado com:

O que aprenderá

O que fará

Neste tutorial, você constrói o modelo de vista, corrotinas e porções de exibição de dados do aplicativo TrackMySleepQuality.

O aplicativo possui duas telas, representadas por fragmentos, conforme mostrado na figura abaixo.

A primeira tela, mostrada à esquerda, possui botões para iniciar e parar o rastreamento. A tela mostra todos os dados de sono do usuário. O botão Clear exclui permanentemente todos os dados que o aplicativo coletou para o usuário.

A segunda tela, mostrada à direita, é para selecionar uma classificação de qualidade do sono. No aplicativo, a classificação é representada numericamente. Para fins de desenvolvimento, o aplicativo mostra os ícones de rosto e seus equivalentes numéricos.

O fluxo do usuário é o seguinte:

Este aplicativo usa uma arquitetura simplificada, conforme mostrado abaixo no contexto da arquitetura completa. O aplicativo usa apenas os seguintes componentes:

Nesta tarefa, você usa um TextView para exibir os dados de rastreamento do sono formatados. (Esta não é a interface final. Você aprenderá uma maneira melhor em outro tutorial).

Você pode continuar com o aplicativo TrackMySleepQuality que você construiu no tutorial anterior ou baixar o aplicativo inicial para este tutorial.

Etapa 1: Baixe e execute o aplicativo inicial

  1. Baixe o aplicativo TrackMySleepQuality-Coroutines-Starter do GitHub.
  2. Construa e execute o aplicativo. O aplicativo mostra a IU para o fragmento SleepTrackerFragment, mas nenhum dado. Os botões não respondem aos toques.

Etapa 2: Inspecione o código

O código inicial para este codelab é o mesmo que o código da solução para o tutorial Criar um Banco de Dados Room.

  1. Abra res/layout/activity_main.xml. Este layout contém o fragmento nav_host_fragment. Além disso, observe a etiqueta <merge>.

    A etiqueta merge pode ser usada para eliminar layouts redundantes ao incluir layouts, e é uma boa ideia usá-la. Um exemplo de layout redundante seria ConstraintLayout> LinearLayout> TextView, onde o sistema pode ser capaz de eliminar o LinearLayout. Esse tipo de otimização pode simplificar a hierarquia de vistas e melhorar o desempenho do aplicativo.
  2. Na pasta navigation, abra navigation.xml. Você pode ver dois fragmentos e as ações de navegação que os conectam.
  3. Na pasta layout, clique duas vezes no fragmento do rasthreador de sono para ver seu layout XML. Observe o seguinte:

O aplicativo inicial também fornece dimensões, cores e estilo para a IU. O aplicativo contém um banco de dados Room, um DAO e uma entidade SleepNight. Se você não concluiu o tutorial anterior, certifique-se de explorar esses aspectos do código por conta própria.

Agora que você tem um banco de dados e uma IU, precisa coletar dados, adicionar os dados ao banco de dados e exibir os dados. Todo esse trabalho é feito no modelo de vista. Seu modelo de vistas do sleep-tracker tratará com cliques de botão, interagirá com o banco de dados por meio do DAO e fornecerá dados à IU por meio do LiveData. Todas as operações de banco de dados terão que ser executadas fora da thread de interface do usuário principal, e você fará isso usando corrotinas.

Etapa 1: Adicione SleepTrackerViewModel

  1. No pacote sleeptracker, abra SleepTrackerViewModel.kt.
  2. Inspecione a classe SleepTrackerViewModel, que é fornecida no aplicativo inicial e também é mostrada abaixo. Observe que a classe estende AndroidViewModel(). Esta classe é igual a ViewModel, mas pega o contexto do aplicativo como parâmetro e o disponibiliza como propriedade. Você precisará disso mais tarde.
class SleepTrackerViewModel(
       val database: SleepDatabaseDao,
       application: Application) : AndroidViewModel(application) {
}

Etapa 2: Adicione SleepTrackerViewModelFactory

  1. No pacote sleeptracker, abra SleepTrackerViewModelFactory.kt.
  2. Examine o código fornecido para a fábrica, que é mostrado abaixo:
class SleepTrackerViewModelFactory(
       private val dataSource: SleepDatabaseDao,
       private val application: Application) : ViewModelProvider.Factory {
   @Suppress("unchecked_cast")
   override fun <T : ViewModel?> create(modelClass: Class<T>): T {
       if (modelClass.isAssignableFrom(SleepTrackerViewModel::class.java)) {
           return SleepTrackerViewModel(dataSource, application) as T
       }
       throw IllegalArgumentException("Unknown ViewModel class")
   }
}

Observe o seguinte:

Etapa 3: Atualize SleepTrackerFragment

  1. No SleepTrackerFragment, obtenha uma referência ao contexto do aplicativo. Coloque a referência em onCreateView(), abaixo de binding. Você precisa de uma referência ao aplicativo ao qual este fragmento está anexado, para passar para o provedor de fábrica do modelo de exibição.

    A função requireNotNull Kotlin lança uma IllegalArgumentException se o valor for null.
val application = requireNotNull(this.activity).application
  1. Você precisa de uma referência à sua fonte de dados por meio de uma referência ao DAO. Em onCreateView(), antes do return, defina uma dataSource. Para obter uma referência ao DAO do banco de dados, use SleepDatabase.getInstance(application).sleepDatabaseDao.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  1. Em onCreateView(), antes do return, crie uma instância de viewModelFactory. Você precisa passar o dataSource e o application.
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)
  1. Agora que você tem uma fábrica, obtenha uma referência para o SleepTrackerViewModel. O parâmetro SleepTrackerViewModel::class.java refere-se à classe Java de tempo de execução deste objeto.
val sleepTrackerViewModel =
       ViewModelProvider(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)
  1. Seu código finalizado deve ficar assim:
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

val sleepTrackerViewModel =
       ViewModelProvider(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)

Este é o método onCreateView() até agora:

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

        val binding: FragmentSleepTrackerBinding = DataBindingUtil.inflate(
                inflater, R.layout.fragment_sleep_tracker, container, false)

        val application = requireNotNull(this.activity).application

        val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao

        val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

        val sleepTrackerViewModel =
                ViewModelProvider(
                        this, viewModelFactory).get(SleepTrackerViewModel::class.java)

        return binding.root
    }

Etapa 4: Adicione vinculação de dados para o modelo de vista

Com o ViewModel básico instalado, você precisa terminar de configurar a vinculação de dados no SleepTrackerFragment para conectar o ViewModel à IU.


No arquivo de layout fragment_sleep_tracker.xml:

  1. Dentro do bloco <data>, crie uma <variable> que faz referência à classe SleepTrackerViewModel.
<data>
   <variable
       name="sleepTrackerViewModel"
       type="com.example.android.trackmysleepquality.sleeptracker.SleepTrackerViewModel" />
</data>

Em SleepTrackerFragment:

  1. Defina a atividade atual como o proprietário do ciclo de vida da vinculação. Adicione este código dentro do método onCreateView(), antes da instrução return:
binding.setLifecycleOwner(this)
  1. Atribua a variável de vinculação sleepTrackerViewModel ao sleepTrackerViewModel. Coloque este código dentro de onCreateView(), abaixo do código que cria o SleepTrackerViewModel:
binding.sleepTrackerViewModel = sleepTrackerViewModel
  1. Você provavelmente verá um erro, pois terá que recriar o objeto de vinculação. Limpe e reconstrua o projeto para se livrar do erro.
  2. Finalmente, como sempre, certifique-se de que seu código seja compilado e executado sem erros.

No Kotlin, as corrotinas são a maneira de tratar com tarefas de longa duração com elegância e eficiência. As corrotinas Kotlin permitem converter código baseado em retorno de chamada em código sequencial. O código escrito sequencialmente é normalmente mais fácil de ler e pode até usar recursos de linguagem, como exceções. No final, corrotinas e retornos de chamada fazem a mesmo: Eles esperam até que um resultado de uma tarefa de longa execução esteja disponível e continuam a execução.

As corrotinas possuem as seguintes propriedades:

Corrotinas são assíncronas.

Uma corrotina é executada independentemente das etapas principais de execução de seu programa. Isso pode ser em paralelo ou em um processador separado. Também pode ser que, enquanto o resto do aplicativo está esperando por uma entrada, você se intromete um pouco no processamento. Um dos aspectos importantes do assíncrono é que você não pode esperar que o resultado esteja disponível, até que você espere explicitamente por ele.

Por exemplo, digamos que você tenha uma pergunta que exija pesquisa e peça a um colega para encontrar a resposta. Eles saem e trabalham nisso, o que é como se estivessem fazendo o trabalho "de forma assíncrona" e "em uma thread separado". Você pode continuar a fazer outro trabalho que não dependa da resposta, até que seu colega volte e lhe diga qual é a resposta.

Corrotinas não bloqueiam.

Sem bloqueio significa que uma corrotina não bloqueia a thread principal ou IU. Portanto, com as corrotinas, os usuários sempre possuem a experiência mais tranquila possível, pois a interação da IU sempre tem prioridade.

Corrotinas usam funções de suspensão para tornar o código assíncrono sequencial.

A palavra-chave suspend é a maneira de Kotlin de marcar uma função, ou tipo de função, como estando disponível para corrotinas. Quando uma corrotina chama uma função marcada com suspend, em vez de bloquear até que a função retorne como uma chamada de função normal, a corrotina suspende a execução até que o resultado esteja pronto. Então, a corrotina continua de onde parou, com o resultado.

Enquanto a corrotina está suspensa e aguardando um resultado, ela desbloqueia a thread em que está sendo executado. Dessa forma, outras funções ou corrotinas podem ser executadas.

A palavra-chave suspend não especifica a thread em que o código é executado. Uma função de suspensão pode ser executada em uma thread de segundo plano ou na thread principal.

Para usar corrotinas em Kotlin, você precisa de três argumentos:

Job: Basicamente, um trabalho é algo que pode ser cancelada. Cada corrotina tem um trabalho e você pode usar o trabalho para cancelar a corrotina. Os trabalhos podem ser organizados em hierarquias pai-filho. Cancelando um trabalho pai cancela imediatamente todos os filhos do trabalho, o que é muito mais conveniente do que cancelar cada corrotina manualmente

Dispatcher: O dispatcher envia corrotinas para rodar em várias threads. Por exemplo, Dispatcher.Main executa tarefas na thread principal e Dispatcher.IO descarrega as tarefas de E/S de bloqueio para um pool compartilhado de threads.

Scope: O escopo de uma corrotina define o contexto no qual a corrotina é executada. Um escopo combina informações sobre o trabalho e o despachante de uma corrotina. Os osciloscópios monitoram as corrotinas. Quando você inicia uma corrotina, ela está "em um escopo", o que significa que você indicou qual escopo manterá o controle da corrotina.

Você deseja que o usuário seja capaz de interagir com os dados do sono das seguintes maneiras:

Essas operações de banco de dados podem levar muito tempo, portanto, devem ser executadas em uma thread separado.

Etapa 1: Configure corrotinas para operações de banco de dados

Quando o botão Start no aplicativo Sleep Tracker é tocado, você deseja chamar uma função no SleepTrackerViewModel para criar uma instância de SleepNight e armazene a instância no banco de dados.

Tocando em qualquer um dos botões aciona uma operação de banco de dados, como criar ou atualizar um SleepNight. Por esse e outros motivos, você usa corrotinas para implementar tratadores de cliques para os botões do aplicativo.

  1. Abra o arquivo build.gradle de nível de aplicativo e encontre as dependências para corrotinas. Para usar corrotinas, você precisa destas dependências, que foram adicionadas.

    A $ coroutine_version é definida no arquivo build.gradle do projeto como coroutine_version ='1.0.0'.
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"
  1. Abra o arquivo SleepTrackerViewModel.
  2. No corpo da classe, defina viewModelJob e atribua a ele uma instância de Job. Este viewModelJob permite cancelar todas as corrotinas iniciadas por este modelo de vistas quando o modelo de vistas não é mais usado e é destruído. Dessa forma, você não acaba com corrotinas que não têm para onde voltar.
private var viewModelJob = Job()
  1. No final do corpo da classe, substitua onCleared() e cancele todas as corrotinas. Quando o ViewModel é destruído, onCleared() é chamado.
override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}
  1. Logo abaixo da definição de viewModelJob, defina um uiScope para as corrotinas. O escopo determina em qual thread a corrotina será executada, e o escopo também precisa saber sobre o trabalho. Para obter um escopo, peça uma instância de CoroutineScope e passe um despachante e um trabalho.

Usando Dispatchers.Main significa que as corrotinas lançadas no uiScope serão executadas na thread principal. Isso é sensato para muitas corrotinas iniciadas por um ViewModel, pois depois que essas corrotinas executam algum processamento, elas resultam em uma atualização da IU.

private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
  1. Abaixo da definição de uiScope, defina uma variável chamada tonight para manter a noite atual. Faça a variável MutableLiveData, pois você precisa ser capaz de observar os dados e alterá-los.
private var tonight = MutableLiveData<SleepNight?>()
  1. Para inicializar a variável tonight o mais rápido possível, crie um bloco init abaixo da definição de tonight e chame initializeTonight(). Você define initializeTonight() na próxima etapa.
init {
   initializeTonight()
}
  1. Abaixo do bloco init, implemente initializeTonight(). No uiScope, inicie uma corrotina. Dentro, obtenha o valor para tonight do banco de dados chamando getTonightFromDatabase() e atribua o valor a tonight.value. Você define getTonightFromDatabase() na próxima etapa.
private fun initializeTonight() {
   uiScope.launch {
       tonight.value = getTonightFromDatabase()
   }
}
  1. Implemente getTonightFromDatabase(). Defina-o como uma função private suspend que retorna um SleepNight anulável, se não houver SleepNight inicializado. Isso deixa você com um erro, pois a função deve retornar algo.
private suspend fun getTonightFromDatabase(): SleepNight? { }
  1. Dentro do corpo da função de getTonightFromDatabase(), retorne o resultado de uma corrotina que é executada no contexto Dispatchers.IO. Use o distribuidor de E/S, pois obter dados do banco de dados é uma operação de E/S e não tem nada a ver com a IU.
  return withContext(Dispatchers.IO) {}
  1. Dentro do bloco de retorno, deixe a corrotina obter esta noite (a noite mais recente) do banco de dados. Se os horários de início e término não forem iguais, o que significa que a noite já foi concluída, retorna null. Caso contrário, volte à noite.
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       night

Sua função de suspensão getTonightFromDatabase() concluída deve ter a seguinte aparência. Não deve haver mais erros.

private suspend fun getTonightFromDatabase(): SleepNight? {
   return withContext(Dispatchers.IO) {
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       night
   }
}

Etapa 2: Adicione o tratador de cliques para o botão Iniciar

Agora você pode implementar onStartTracking(), o tratador de clique para o botão Start. Você precisa criar um SleepNight, inseri-lo no banco de dados e atribuí-lo a tonight. A estrutura de onStartTracking() será muito parecida com initializeTonight().

  1. Comece com a definição da função para onStartTracking(). Você pode colocar os tratadores de cliques acima de onCleared() no arquivo SleepTrackerViewModel.
fun onStartTracking() {}
  1. Dentro de onStartTracking(), inicie uma corrotina no uiScope, pois você precisa deste resultado para continuar e atualizar a IU.
uiScope.launch {}
  1. Dentro do lançamento da corrotina, crie um SleepNight, que captura a hora atual como a hora de início.
        val newNight = SleepNight()
  1. Ainda dentro do lançamento da corrotina, chame insert() para inserir newNight no banco de dados. Você verá um erro, pois ainda não definiu esta função de suspensão insert(). (Esta não é a função DAO com o mesmo nome).
       insert(newNight)
  1. Também dentro do lançamento da corrotina, atualize tonight.
       tonight.value = getTonightFromDatabase()
  1. Abaixo de onStartTracking(), defina insert() como uma função private suspend que leva um SleepNight como argumento.
private suspend fun insert(night: SleepNight) {}
  1. Para o corpo de insert(), lance uma corrotina no contexto de E/S e insira a noite no banco de dados chamando insert() do DAO.
   withContext(Dispatchers.IO) {
       database.insert(night)
   }
  1. No arquivo de layout fragment_sleep_tracker.xml, adicione o tratador de cliques para onStartTracking() ao start_button usando a magia da vinculação de dados que você configurou mais cedo. A notação de função @{() -> cria uma função lambda que não aceita argumentos e chama o tratador de cliques no sleepTrackerViewModel.
android:onClick="@{() -> sleepTrackerViewModel.onStartTracking()}"
  1. Construa e execute seu aplicativo. Toque no botão Start. Esta ação cria dados, mas você não pode ver nada ainda. Você conserta isso a seguir.
fun someWorkNeedsToBeDone {
   uiScope.launch {

        suspendFunction()

   }
}

suspend fun suspendFunction() {
   withContext(Dispatchers.IO) {
       longrunningWork()
   }
}

Etapa 3: Exiba os dados

No SleepTrackerViewModel, a variável nights faz referência a LiveData, pois getAllNights() no DAO retorna LiveData.

É um recurso de Room que sempre que os dados no banco de dados mudam, o LiveData nights é atualizado para mostrar os dados mais recentes. Você nunca precisa definir explicitamente o LiveData ou atualizá-lo. Room atualiza os dados para corresponder ao banco de dados.

No entanto, se você exibir nights em uma vista de texto, ele mostrará a referência do objeto. Para ver o conteúdo do objeto, transforme os dados em uma string formatada. Use um mapa de Transformation que é executado toda vez que nights recebe novos dados do banco de dados.

  1. Abra o arquivo Util.kt e descomente o código para a definição de formatNights() e as instruções import associadas. Para descomentar o código no Android Studio, selecione todo o código marcado com // e pressione Cmd+/ ou Control+/.
  2. Observe que formatNights() retorna um tipo Spanned, que é uma string formatada em HTML.
  3. Abra strings.xml. Observe o uso de CDATA para formatar os recursos de string para exibir os dados de sono.
  4. Abra SleepTrackerViewModel. Na classe SleepTrackerViewModel, abaixo da definição de uiScope, defina uma variável chamada nights. Obtenha todas as noites do banco de dados e atribua-as à variável nights.
private val nights = database.getAllNights()
  1. Logo abaixo da definição de nights, adicione o código para transformar nights em uma nightsString. Use a função formatNights() do Util.kt.

    Passe nights para o mapa map() função da classe Transformations. Para obter acesso aos seus recursos de string, defina a função de mapeamento como chamando formatNights(). Forneça nights e um objeto Resources.
val nightsString = Transformations.map(nights) { nights ->
   formatNights(nights, application.resources)
}
  1. Abra o arquivo de layout fragment_sleep_tracker.xml. Em TextView, na propriedade android:text, agora você pode substituir a string de recurso por uma referência a nightsString.
"@{sleepTrackerViewModel.nightsString}"
  1. Reconstrua seu código e execute o aplicativo. Todos os seus dados de sono com horários de início devem ser exibidos agora.
  2. Toque no botão Start mais algumas vezes e você verá mais dados.

Na próxima etapa, você habilita a funcionalidade do botão Stop.

Etapa 4: Adicione o tratador de cliques para o botão Parar

Usando o mesmo padrão da etapa anterior, implemente o tratador de cliques para o botão Stop em SleepTrackerViewModel.

  1. Adicione onStopTracking() ao ViewModel. Inicie uma corrotina no uiScope. Se a hora de término ainda não foi definida, defina o endTimeMilli para a hora do sistema atual e chame update() com os dados noturnos.

    Em Kotlin, a sintaxe return@label especifica a função a partir da qual esta instrução retorna, entre várias funções aninhadas.
fun onStopTracking() {
   uiScope.launch {
       val oldNight = tonight.value ?: return@launch
       oldNight.endTimeMilli = System.currentTimeMillis()
       update(oldNight)
   }
}
  1. Implemente update() usando o mesmo padrão que usou para implementar insert().
private suspend fun update(night: SleepNight) {
   withContext(Dispatchers.IO) {
       database.update(night)
   }
}
  1. Para conectar o tratador de cliques à IU, abra o arquivo de layout fragment_sleep_tracker.xml e adicione o tratador de cliques ao stop_button.
android:onClick="@{() -> sleepTrackerViewModel.onStopTracking()}"
  1. Construa e execute seu aplicativo.
  2. Toque em Start e, em seguida, toque em Stop. Você vê a hora de início, hora de término, qualidade do sono sem valor e tempo de sono.

Etapa 5: Adicione o tratador de cliques para o botão Limpar

  1. Da mesma forma, implemente onClear() e clear().
fun onClear() {
   uiScope.launch {
       clear()
       tonight.value = null
   }
}

suspend fun clear() {
   withContext(Dispatchers.IO) {
       database.clear()
   }
}
  1. Para conectar o tratador de cliques à IU, abra fragment_sleep_tracker.xml e adicione o tratador de cliques ao clear_button.
android:onClick="@{() -> sleepTrackerViewModel.onClear()}"
  1. Construa e execute seu aplicativo.
  2. Toque em Clear para se livrar de todos os dados. Em seguida, toque em Start e Stop para criar novos dados.

Projeto Android Studio: TrackMySleepQualityCoroutines

Para iniciar uma corrotina, você precisa de um trabalho, um despachante e um escopo:

Para implementar tratadores de cliques que acionam operações de banco de dados, siga este padrão:

  1. Inicie uma corrotina que é executada na thread principal ou IU, pois o resultado afeta a IU.
  2. Chame uma função de suspensão para fazer o trabalho de longa duração, de modo que você não bloqueie a thread de IU enquanto espera pelo resultado.
  3. O trabalho de longa duração não tem nada a ver com a IU, então mude para o contexto de E/S. Dessa forma, o trabalho pode ser executado em um pool de threads otimizado e reservado para esses tipos de operações.
  4. Em seguida, chame a função de banco de dados para fazer o trabalho.

Use um mapa de Transformations para criar uma string de um objeto LiveData sempre que o objeto for alterado.

Documentação do desenvolvedor Android:

Outra documentação e artigos:

Esta seção lista as possíveis tarefas de casa para os alunos que estão trabalhando neste tutorial como parte de um curso ministrado por um instrutor.

Responda a estasperguntas

Pergunta 1

Quais das seguintes são vantagens das corrotinas:

Pergunta 2

O que é uma função de suspensão?

Pergunta 3

Qual é a diferença entre bloquear e suspender uma thread? Marque tudo o que é verdade.

Comece para a próxima lição: 06.3: Usando LiveData para controlar os estados do botão

Para obter enlaces para outros tutoriais neste curso, consulte a página de destino dos tutoriais Fundamentos de Android em Kotlin.