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.
Você deve estar familiarizado com:
safeArgs
para passar dados simples entre fragmentos.LiveData
. Room
, criar um DAO e definir entidades. TextView
.LiveData
para acionar a navegação e a exibição de uma snackbar. LiveData
para ativar e desativar os botões.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:
LiveData
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.
SleepTrackerFragment
, mas nenhum dado. Os botões não respondem aos toques.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.
nav_host_fragment
. Além disso, observe a etiqueta <merge>
. 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.<layout>
para habilitar a vinculação de
dados. ConstraintLayout
e as outras vistas são organizadas dentro do elemento
<layout>
.<data>
. 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.
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) {
}
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:
SleepTrackerViewModelFactory
fornecido leva o mesmo argumento que o ViewModel
e
estende ViewModelProvider.Factory
.create()
, que pega qualquer tipo de classe como
argumento e retorna um ViewModel
.create()
, o código verifica se há uma classe SleepTrackerViewModel
disponível e, se houver, retorna uma instância dela. Caso contrário, o código lançará uma exceção.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.requireNotNull
Kotlin lança uma IllegalArgumentException
se o valor for null
. val application = requireNotNull(this.activity).application
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
onCreateView()
, antes do return
, crie uma instância de
viewModelFactory
. Você precisa passar o dataSource
e o application
.
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)
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)
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
}
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
:
<data>
, crie uma <variable>
que faz referência à
classe SleepTrackerViewModel
.<data>
<variable
name="sleepTrackerViewModel"
type="com.example.android.trackmysleepquality.sleeptracker.SleepTrackerViewModel" />
</data>
Em SleepTrackerFragment
:
onCreateView()
, antes da instrução return
:binding.setLifecycleOwner(this)
sleepTrackerViewModel
ao sleepTrackerViewModel
.
Coloque este código dentro de onCreateView()
, abaixo do código que cria o
SleepTrackerViewModel
:binding.sleepTrackerViewModel = sleepTrackerViewModel
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.
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.
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.$ 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"
SleepTrackerViewModel
. 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()
onCleared()
e cancele todas as corrotinas. Quando o
ViewModel
é destruído, onCleared()
é chamado. override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
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)
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?>()
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()
}
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()
}
}
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? { }
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) {}
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
}
}
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()
.
onStartTracking()
. Você pode colocar os tratadores de
cliques acima de onCleared()
no arquivo SleepTrackerViewModel
.fun onStartTracking() {}
onStartTracking()
, inicie uma corrotina no uiScope
, pois você precisa
deste resultado para continuar e atualizar a IU.uiScope.launch {}
SleepNight
, que captura a hora atual como a hora de
início. val newNight = SleepNight()
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)
tonight
. tonight.value = getTonightFromDatabase()
onStartTracking()
, defina insert()
como uma função
private suspend
que leva um SleepNight
como argumento. private suspend fun insert(night: SleepNight) {}
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)
}
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()}"
fun someWorkNeedsToBeDone { uiScope.launch { suspendFunction() } } suspend fun suspendFunction() { withContext(Dispatchers.IO) { longrunningWork() } }
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.
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+/
.formatNights()
retorna um tipo Spanned
, que é uma string formatada em
HTML. CDATA
para formatar os recursos de string para exibir os dados de sono.
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()
nights
, adicione o código para transformar nights
em
uma nightsString
. Use a função formatNights()
do Util.kt
.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)
}
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}"
Na próxima etapa, você habilita a funcionalidade do botão Stop.
Usando o mesmo padrão da etapa anterior, implemente o tratador de cliques para o botão Stop em
SleepTrackerViewModel
.
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.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)
}
}
update()
usando o mesmo padrão que usou para implementar insert()
.private suspend fun update(night: SleepNight) {
withContext(Dispatchers.IO) {
database.update(night)
}
}
fragment_sleep_tracker.xml
e
adicione o tratador de cliques ao stop_button
.android:onClick="@{() -> sleepTrackerViewModel.onStopTracking()}"
onClear()
e clear()
.fun onClear() {
uiScope.launch {
clear()
tonight.value = null
}
}
suspend fun clear() {
withContext(Dispatchers.IO) {
database.clear()
}
}
fragment_sleep_tracker.xml
e adicione o tratador
de cliques ao clear_button
.android:onClick="@{() -> sleepTrackerViewModel.onClear()}"
Projeto Android Studio: TrackMySleepQualityCoroutines
ViewModel
, ViewModelFactory
e vinculação de dados para configurar a
arquitetura de IU para o aplicativo. suspend
para tornar o código
assíncrono sequencial.suspend
, em vez de bloquear até que a função
retorne como uma chamada de função normal, ela suspende a execução até que o resultado esteja pronto. Em
seguida, ele continua de onde parou com o resultado.Para iniciar uma corrotina, você precisa de um trabalho, um despachante e um escopo:
Dispatcher.Main
executa
tarefas na thread principal, e Dispartcher.IO
é para descarregar tarefas de E/S de bloqueio para
um pool compartilhado de threads.Para implementar tratadores de cliques que acionam operações de banco de dados, siga este padrão:
Use um mapa de Transformations
para criar uma string de um objeto LiveData
sempre que
o objeto for alterado.
Documentação do desenvolvedor Android:
RoomDatabase
ViewModelProvider.Factory
SimpleDateFormat
HtmlCompat
Outra documentação e artigos:
Dispatchers
Job
launch
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.
Quais das seguintes são vantagens das corrotinas:
O que é uma função de suspensão?
suspend
. Qual é a diferença entre bloquear e suspender uma thread? Marque tudo o que é verdade.
Comece para a próxima lição:
Para obter enlaces para outros tutoriais neste curso, consulte a página de destino dos tutoriais Fundamentos de Android em Kotlin.