Como criar uma biblioteca de extensões Kotlin

O Android KTX é um conjunto de extensões para as APIs mais usadas do framework do Android, as bibliotecas do Android Jetpack e muito mais. Criamos essas extensões para tornar a chamada de APIs baseadas em linguagem de programação Java pelo código Kotlin mais concisa e idiomática, aproveitando recursos de linguagem como funções e propriedades de extensão, lambdas, parâmetros nomeados e padrão e corrotinas.

O que é uma biblioteca KTX?

"KTX" é a sigla em inglês para "extensões Kotlin". Esse não é um recurso especial de linguagem ou tecnologia do Kotlin em si. É apenas um nome que adotamos para as bibliotecas Kotlin do Google, que ampliam a funcionalidade das APIs criadas originalmente na linguagem de programação Java.

O bom das extensões Kotlin é que qualquer pessoa pode criar uma biblioteca para as próprias APIs ou até mesmo para bibliotecas de terceiros usadas nos seus projetos.

Este codelab mostrará alguns exemplos de como adicionar extensões simples que aproveitam os recursos da linguagem Kotlin. Também veremos como converter uma chamada assíncrona feita em uma API baseada em callback em uma função de suspensão e um Flow (um fluxo assíncrono baseado em corrotinas).

O que você criará

Neste codelab, você trabalhará em um aplicativo simples que acessa e exibe o local atual do usuário. Esse app vai:

  • acessar o local mais recente do provedor de localização;
  • registrar-se para atualizações em tempo real da localização do usuário enquanto o app estiver em execução;
  • exibir o local na tela e processar os estados de erro caso ele não esteja disponível.

O que você aprenderá

  • Como adicionar extensões do Kotlin sobre as classes existentes
  • Como converter uma chamada assíncrona que retorna um único resultado em uma função de suspensão de corrotinas
  • Como usar o Flow para conseguir dados de uma fonte que pode emitir um valor muitas vezes

Pré-requisitos

  • Uma versão recente do Android Studio (recomendamos a 3.6 ou mais recente)
  • O Android Emulator ou um dispositivo conectado via USB
  • Conhecimento básico de desenvolvimento para Android e da linguagem Kotlin
  • Compreensão básica de corrotinas e funções de suspensão

Fazer o download do código

Clique no link abaixo para fazer o download de todo o código para este codelab:

Download do código-fonte

… ou clone o repositório do GitHub pela linha de comando usando o seguinte comando:

$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git

O código para este codelab está no diretório ktx-library-codelab.

No diretório do projeto, você verá várias pastas step-NN, que contêm o estado final desejado de cada etapa deste codelab para referência.

Faremos todo o trabalho de programação no diretório work.

Como executar o app pela primeira vez

Abra a pasta raiz (ktx-library-codelab) no Android Studio e selecione a configuração de execução work-app na lista suspensa, conforme mostrado abaixo:

79c2a2d2f9bbb388.png

Pressione o botão Run 35a622f38049c660.png para testar o app:

58b6a81af969abf0.png

Esse app ainda não faz nada de interessante. Faltam algumas partes para ele poder exibir os dados. Adicionaremos as funcionalidades ausentes nas próximas etapas.

Uma forma mais fácil de verificar permissões

58b6a81af969abf0.png

Mesmo que o app seja executado, ele mostrará um erro, já que ainda não consegue acessar a localização atual.

Isso ocorre porque ele não tem o código necessário para solicitar a permissão de localização do usuário no ambiente de execução.

Abra MainActivity.kt e veja o seguinte código comentado:

//  val permissionApproved = ActivityCompat.checkSelfPermission(
//      this,
//      Manifest.permission.ACCESS_FINE_LOCATION
//  ) == PackageManager.PERMISSION_GRANTED
//  if (!permissionApproved) {
//      requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 0)
//  }

Se você remover a marca de comentário do código e executar o app, ele solicitará a permissão e mostrará o local. No entanto, esse código é difícil de ler por vários motivos:

  • Ele usa uma checkSelfPermission de método estático da classe de utilitário ActivityCompat, que existe apenas para reter métodos para compatibilidade com versões anteriores.
  • Como é impossível adicionar um método a uma classe de framework na linguagem de programação Java, o método sempre usa uma instância de Activity como o primeiro parâmetro.
  • Estamos sempre verificando se a permissão foi concedida (PERMISSION_GRANTED). Por isso, é melhor receber diretamente um booleano true caso ela tenha sido ou, se não tiver, um false.

Queremos converter o código detalhado acima para deixá-lo mais curto, como este:

if (!hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) {
    // request permission
}

Vamos encurtar o código com a ajuda de uma função de extensão na Activity. No projeto, você verá outro módulo chamado myktxlibrary. Abra ActivityUtils.kt nesse módulo e adicione esta função:

fun Activity.hasPermission(permission: String): Boolean {
    return ActivityCompat.checkSelfPermission(
        this,
        permission
    ) == PackageManager.PERMISSION_GRANTED
}

Vamos destrinchar o que está acontecendo:

  • fun no escopo externo (não dentro de uma class) significa que estamos definindo uma função de nível superior no arquivo.
  • Activity.hasPermission define uma função de extensão com o nome hasPermission em um receptor do tipo Activity.
  • Ele usa a permissão como um argumento String e retorna um Boolean que indica se ela foi concedida.

Mas o que exatamente é um "receptor do tipo X"?

Você verá essa expressão com muita frequência na documentação para funções de extensão do Kotlin. Ela significa que essa função será sempre chamada em uma instância de uma Activity (neste caso) ou nas subclasses dela. Dentro do corpo da função, podemos nos referir a essa instância usando a palavra-chave this, que também pode ser implícita, ou seja, totalmente omitida.

É para isso que existem as funções de extensão: adicionar novas funcionalidades sobre uma classe que não podemos ou não queremos mudar.

Vejamos como nós a chamaríamos na nossa MainActivity.kt. Abra-a e mude o código das permissões para:

if (!hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) {
   requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 0)
}

Se você executar o app agora, verá o local exibido na tela.

c040ceb7a6bfb27b.png

Uma ajudinha para formatar o texto da localização

O texto da localização não está com uma boa aparência. Ele está usando o método Location.toString padrão, que não foi feito para ser exibido em uma IU.

Abra a classe LocationUtils.kt na myktxlibrary. Esse arquivo contém extensões para a classe Location. Conclua a função de extensão Location.format para retornar uma String formatada e, em seguida, modifique Activity.showLocation em ActivityUtils.kt para usar a extensão.

Se você tiver problemas, use o código na pasta step-03. O resultado final ficará assim:

b8ef64975551f2a.png

Provedor de localização combinada do Google Play Services

O projeto de app em que estamos trabalhando usa o provedor de localização combinada do Google Play Services para acessar dados de local. A API em si é bem simples, mas, como o acesso à localização de um usuário não é uma operação instantânea, todas as chamadas na biblioteca precisam ser assíncronas, o que complica nosso código com callbacks.

Há duas partes no processo de acesso à localização de um usuário. Nesta etapa, nosso foco será a última localização conhecida, se ela estiver disponível. Na próxima, analisaremos as atualizações periódicas da localização quando o app estiver em execução.

Como acessar a última localização conhecida

Em Activity.onCreate, inicializaremos o FusedLocationProviderClient, que será nosso ponto de entrada para a biblioteca.

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
   fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
}

Em Activity.onStart, invocaremos getLastKnownLocation(), que atualmente tem a seguinte aparência:

private fun getLastKnownLocation() {
   fusedLocationClient.lastLocation.addOnSuccessListener { lastLocation ->
       showLocation(R.id.textView, lastLocation)
   }.addOnFailureListener { e ->
       findAndSetText(R.id.textView, "Unable to get location.")
       e.printStackTrace()
   }
}

Como você pode ver, lastLocation é uma chamada assíncrona que pode ser concluída com êxito ou falha. Para cada um desses resultados, precisamos registrar uma função de callback que definirá o local para a IU ou exibirá uma mensagem de erro.

Esse código pode não parecer muito complicado pelos callbacks agora, mas, em um projeto real, é possível que você queira processar o local, salvá-lo em um banco de dados ou fazer upload para um servidor. Muitas dessas operações também são assíncronas, e a adição de cada vez mais callbacks torna o código rapidamente ilegível, podendo acabar com esta aparência:

private fun getLastKnownLocation() {
   fusedLocationClient.lastLocation.addOnSuccessListener { lastLocation ->
       getLastLocationFromDB().addOnSuccessListener {
           if (it != location) {
               saveLocationToDb(location).addOnSuccessListener {
                   showLocation(R.id.textView, lastLocation)
               }
           }
       }.addOnFailureListener { e ->
           findAndSetText(R.id.textView, "Unable to read location from DB.")
           e.printStackTrace()
       }
   }.addOnFailureListener { e ->
       findAndSetText(R.id.textView, "Unable to get location.")
       e.printStackTrace()
   }
}

Pior ainda: o código acima tem problemas com vazamento de memória e operações, já que os listeners nunca são removidos quando a Activity que os contém é concluída.

Vamos procurar uma forma melhor de resolver esse problema usando corrotinas, que permitem programar código assíncrono que tem a aparência de um bloco de código comum e imperativo de cima para baixo sem fazer chamadas de bloqueio na linha de execução de chamadas. Além disso, as corrotinas também podem ser canceladas, limpando o código sempre que saem do escopo.

Na próxima etapa, adicionaremos uma função de extensão que converte a API callback existente em uma função de suspensão que pode ser chamada em um escopo de corrotinas vinculado à sua IU. Queremos que o resultado final seja parecido com este:

private fun getLastKnownLocation() {
    try {
        val lastLocation = fusedLocationClient.awaitLastLocation();
        // process lastLocation here if needed
        showLocation(R.id.textView, lastLocation)
    } (e: Exception) {
        // we can do regular exception handling here or let it throw outside the function
    }
}

Como criar uma função de suspensão usando suspendCancellableCoroutine

Abra LocationUtils.kt e defina uma nova função de extensão no FusedLocationProviderClient:

suspend fun FusedLocationProviderClient.awaitLastLocation(): Location =
   suspendCancellableCoroutine { continuation ->
    TODO("Return results from the lastLocation call here")
}

Antes de entrar na parte de implementação, vamos analisar esta assinatura de função:

  • Você já conhece a função de extensão e o tipo de receptor das seções anteriores deste codelab: fun FusedLocationProviderClient.awaitLastLocation.
  • suspend significa que essa será uma função de suspensão, um tipo especial de função que só pode ser chamado dentro de uma corrotina ou de outra função suspend.
  • O tipo de resultado da chamada será Location, como se fosse uma maneira síncrona de conseguir um resultado de local da API.

Para criar o resultado, usaremos a suspendCancellableCoroutine (link em inglês), um elemento básico de baixo nível para criar funções de suspensão pela biblioteca de corrotinas.

A suspendCancellableCoroutine executa o bloco de código transmitido para ela como um parâmetro e suspende a execução da corrotina enquanto aguarda um resultado.

Vamos tentar adicionar os callbacks de êxito e falha ao corpo da função, como vimos na chamada lastLocation anterior. Infelizmente, como você pode ver nos comentários abaixo, não é possível fazer o óbvio no corpo do callback, ou seja, retornar um resultado:

suspend fun FusedLocationProviderClient.awaitLastLocation(): Location =
   suspendCancellableCoroutine { continuation ->
    lastLocation.addOnSuccessListener { location ->
        // this is not allowed here:
        // return location
    }.addOnFailureListener { e ->
        // this will not work as intended:
        // throw e
    }
}

Isso ocorre porque o callback acontece muito depois do término da função, e não há para onde retornar o resultado. É aí que entra a suspendCancellableCoroutine (link em inglês) com a continuation fornecida ao nosso bloco de código. Podemos usá-la para retornar um resultado à função suspensa no futuro, com continuation.resume. Processe o caso de erro usando continuation.resumeWithException(e) para propagar corretamente a exceção ao local de chamada.

No geral, você sempre precisa conferir se um resultado ou uma exceção serão retornados em algum momento para não manter a corrotina suspensa para sempre enquanto aguarda um resultado.

suspend fun FusedLocationProviderClient.awaitLastLocation(): Location =
   suspendCancellableCoroutine<Location> { continuation ->
       lastLocation.addOnSuccessListener { location ->
           continuation.resume(location)
       }.addOnFailureListener { e ->
           continuation.resumeWithException(e)
       }
   }

Pronto! Acabamos de expor uma versão de suspensão da API de última localização conhecida, que pode ser consumida pelas corrotinas no nosso app.

Como chamar uma função de suspensão

Vamos modificar nossa função getLastKnownLocation na MainActivity para chamar a nova versão da corrotina da chamada da última localização conhecida:

private suspend fun getLastKnownLocation() {
    try {
        val lastLocation = fusedLocationClient.awaitLastLocation()
        showLocation(R.id.textView, lastLocation)
    } catch (e: Exception) {
        findAndSetText(R.id.textView, "Unable to get location.")
        Log.d(TAG, "Unable to get location", e)
    }
}

Como já mencionado, as funções de suspensão sempre precisam ser chamadas por outras funções de suspensão para garantir que sejam executadas em uma corrotina, ou seja, temos que adicionar um modificador de suspensão à função getLastKnownLocation. Caso contrário, ocorrerá um erro no ambiente de desenvolvimento integrado.

Observe que podemos usar um bloco try-catch normal para processar exceções. Podemos mover esse código dentro do callback de falha porque as exceções da API Location agora são propagadas corretamente, assim como em um programa imperativo comum.

Para iniciar uma corrotina, precisamos de um escopo de corrotina para usar CoroutineScope.launch. Felizmente, as bibliotecas do Android KTX vêm com vários escopos predefinidos para objetos de ciclo de vida comuns, como Activity, Fragment e ViewModel.

Adicione o código a seguir a Activity.onStart:

override fun onStart() {
   super.onStart()
   if (!hasPermission(ACCESS_FINE_LOCATION)) {
       requestPermissions(arrayOf(ACCESS_FINE_LOCATION), 0)
   }

   lifecycleScope.launch {
       try {
           getLastKnownLocation()
       } catch (e: Exception) {
           findAndSetText(R.id.textView, "Unable to get location.")
           Log.d(TAG, "Unable to get location", e)
       }
   }
   startUpdatingLocation()
}

Você poderá executar seu app e verificar se ele funciona antes de passar para a próxima etapa, em que apresentaremos o Flow para uma função que emite resultados de localização várias vezes.

Agora, vamos nos concentrar na função startUpdatingLocation(). No código atual, registramos um listener com o provedor de localização combinada para receber atualizações periódicas de localização sempre que o dispositivo do usuário se move no mundo real.

Para mostrar o que queremos alcançar com uma API baseada em Flow, primeiro vamos analisar as partes da MainActivity que serão removidas nesta seção, movendo-as para os detalhes de implementação da nova função de extensão.

Em nosso código atual, há uma variável para acompanhar o início da detecção de atualizações:

var listeningToUpdates = false

Há também uma subclasse da classe de callback básica e nossa implementação para a função de callback para local atualizada:

private val locationCallback: LocationCallback = object : LocationCallback() {
   override fun onLocationResult(locationResult: LocationResult?) {
       if (locationResult != null) {
           showLocation(R.id.textView, locationResult.lastLocation)
       }
   }
}

Como essa é uma chamada assíncrona, também temos o registro inicial do listener junto aos callbacks, que poderá falhar se o usuário não conceder as permissões necessárias:

private fun startUpdatingLocation() {
   fusedLocationClient.requestLocationUpdates(
       createLocationRequest(),
       locationCallback,
       Looper.getMainLooper()
   ).addOnSuccessListener { listeningToUpdates = true }
   .addOnFailureListener { e ->
       findAndSetText(R.id.textView, "Unable to get location.")
       e.printStackTrace()
   }
}

Por fim, quando a tela não estiver mais ativa, poderemos limpar o código:

override fun onStop() {
   super.onStop()
   if (listeningToUpdates) {
       stopUpdatingLocation()
   }
}

private fun stopUpdatingLocation() {
   fusedLocationClient.removeLocationUpdates(locationCallback)
}

Você pode excluir todos esses snippets de código da MainActivity, deixando apenas uma função startUpdatingLocation() vazia que usaremos mais tarde para começar a coletar o Flow.

callbackFlow: um builder de Flow para APIs baseadas em callback

Abra LocationUtils.kt novamente e defina outra função de extensão no FusedLocationProviderClient:

fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
    TODO("Register a location listener")
    TODO("Emit updates on location changes")
    TODO("Clean up listener when finished")
}

Precisamos fazer algumas coisas para replicar a funcionalidade que acabamos de excluir do código MainActivity. Usaremos callbackFlow(), uma função do builder que retorna um Flow, que é adequado para emitir dados de uma API baseada em callback.

O bloco transmitido para callbackFlow() (link em inglês) é definido com um ProducerScope como receptor.

noinline block: suspend ProducerScope<T>.() -> Unit

ProducerScope encapsula os detalhes de implementação de um callbackFlow, como o fato de haver um Channel apoiando o Flow criado. Sem entrar nos pormenores, Channels são usados internamente por alguns operadores e builders Flow e, a menos que você esteja programando seu próprio builder/operador, não é necessário se preocupar com esses detalhes.

Vamos simplesmente usar algumas funções que ProducerScope expõe para emitir dados e gerenciar o estado do Flow.

Primeiro, criaremos um listener para a API Location:

fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
    val callback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult?) {
            result ?: return
            for (location in result.locations) {
                offer(location) // emit location into the Flow using ProducerScope.offer
            }
        }
    }

    TODO("Register a location listener")
    TODO("Clean up listener when finished")
}

Usaremos o ProducerScope.offer para enviar dados de local pelo Flow à medida que eles forem recebidos.

Em seguida, registre o callback com o FusedLocationProviderClient, tomando cuidado para que os erros sejam resolvidos:

fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
    val callback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult?) {
            result ?: return
            for (location in result.locations) {
                offer(location) // emit location into the Flow using ProducerScope.offer
            }
        }
    }

    requestLocationUpdates(
       createLocationRequest(),
       callback,
       Looper.getMainLooper()
    ).addOnFailureListener { e ->
       close(e) // in case of error, close the Flow
    }

    TODO("Clean up listener when finished")
}

Assim como lastLocation, o FusedLocationProviderClient.requestLocationUpdates é uma função assíncrona que usa callbacks para sinalizar a conclusão ou falha.

Aqui, podemos ignorar o estado de êxito, já que ele significa simplesmente que onLocationResult será chamado em algum momento do futuro, e começar a emitir resultados no Flow.

Em caso de falha, feche o Flow imediatamente com uma Exception.

A última coisa que você sempre precisa chamar dentro de um bloco transmitido para callbackFlow é awaitClose. Ele oferece um lugar conveniente para depositar códigos de limpeza e liberar recursos em caso de conclusão ou cancelamento do Flow, independentemente de ele ter ocorrido com uma Exception ou não:

fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
    val callback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult?) {
            result ?: return
            for (location in result.locations) {
                offer(location) // emit location into the Flow using ProducerScope.offer
            }
        }
    }

    requestLocationUpdates(
       createLocationRequest(),
       callback,
       Looper.getMainLooper()
    ).addOnFailureListener { e ->
       close(e) // in case of exception, close the Flow
    }

    awaitClose {
       removeLocationUpdates(callback) // clean up when Flow collection ends
    }
}

Agora que temos todas as partes (registro de um listener, detecção de atualizações e limpeza), vamos voltar à MainActivity para usar o Flow para exibir o local.

Como coletar o Flow

Vamos modificar a função startUpdatingLocation na MainActivity para invocar o builder Flow e começar a coleta. Uma implementação simples pode ter esta aparência:

private fun startUpdatingLocation() {
    lifecycleScope.launch {
        fusedLocationClient.locationFlow()
        .conflate()
        .catch { e ->
            findAndSetText(R.id.textView, "Unable to get location.")
            Log.d(TAG, "Unable to get location", e)
        }
        .collect { location ->
            showLocation(R.id.textView, location)
            Log.d(TAG, location.toString())
        }
    }
}

Flow.collect() é um operador de terminal que inicia a operação do Flow. Nele, receberemos todas as atualizações de localização emitidas pelo nosso builder callbackFlow. Como collect é uma função de suspensão, é necessário executá-la em uma corrotina, que é iniciada com o lifecycleScope.

Observe também os operadores intermediários conflate() e catch() (links em inglês), que são chamados no Flow. Há muitos operadores na biblioteca de corrotinas que permitem filtrar e transformar fluxos de maneira declarativa.

A combinação de um fluxo significa que só queremos receber a atualização mais recente sempre que as atualizações são emitidas mais rápido do que o coletor pode processá-las. Isso se adequa ao nosso exemplo porque só queremos mostrar a localização mais recente na IU.

catch permitirá que você processe todas as exceções que foram geradas upstream, neste caso, no builder locationFlow. Considere como upstream as operações que são aplicadas antes da atual.

Então qual é o problema no snippet acima? Ainda que o app não falhe e a limpeza seja feita corretamente depois que a atividade é DESTROYED (destruída), graças ao lifecycleScope, a interrupção da atividade não é levada em consideração, por exemplo, quando ela não está visível.

Ou seja, além de atualizarmos a IU quando isso não é necessário, o Flow mantém a assinatura dos dados de local ativa e desperdiça ciclos de bateria e CPU.

Uma maneira de corrigir isso é converter o Flow em um LiveData usando a extensão Flow.asLiveData da biblioteca LiveData KTX. O LiveData sabe quando observar e quando pausar a assinatura e reiniciará o Flow conforme necessário.

private fun startUpdatingLocation() {
    fusedLocationClient.locationFlow()
        .conflate()
        .catch { e ->
            findAndSetText(R.id.textView, "Unable to get location.")
            Log.d(TAG, "Unable to get location", e)
        }
        .asLiveData()
        .observe(this, Observer { location ->
            showLocation(R.id.textView, location)
            Log.d(TAG, location.toString())
        })
}

O lifecycleScope.launch explícito não é mais necessário, porque o asLiveData fornecerá o escopo necessário para executar o Flow. A chamada observe vem do LiveData e não tem relação com corrotinas ou o Flow. Ela é apenas a maneira padrão de observar um LiveData com um LifecycleOwner. O LiveData coletará o Flow e emitirá os locais para o observador.

Como a recriação e a coleta do fluxo serão feitas automaticamente, precisamos mover nosso método startUpdatingLocation() de Activity.onStart, que pode executá-lo muitas vezes, para Activity.onCreate:

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
   fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
   startUpdatingLocation()
}

Agora você pode executar o app e conferir como ele responde à rotação pressionando os botões home e "Voltar". Confira o logcat para ver se novos locais estão sendo exibidos quando o app está em segundo plano. Se a implementação estiver correta, o app será pausado e reiniciará a coleta do Flow quando você pressionar home e voltar para ele.

Você criou sua primeira biblioteca KTX

Parabéns! O que você conseguiu fazer neste codelab é muito parecido com o que normalmente aconteceria ao criar uma biblioteca de extensões Kotlin para uma API baseada em Java.

Para recapitular o que fizemos:

  • Você adicionou uma função de conveniência para conferir as permissões de uma Activity.
  • Você forneceu uma extensão de formatação de texto no objeto Location.
  • Você expôs uma versão de corrotina das APIs Location para acessar a última localização conhecida e atualizações periódicas de localização usando o Flow.
  • Se você quiser, é possível limpar o código ainda mais, adicionar alguns testes e distribuir a biblioteca location-ktx para outros desenvolvedores da sua equipe. Assim, eles poderão aproveitá-la.

Para criar um arquivo AAR para distribuição, execute a tarefa :myktxlibrary:bundleReleaseAar.

Você pode seguir etapas parecidas para qualquer outra API que possa se beneficiar das extensões Kotlin.

Como refinar a arquitetura do aplicativo com fluxos

Já mencionamos que nem sempre é bom iniciar operações pela Activity como fizemos neste codelab. Siga este codelab para aprender a observar os fluxos de ViewModels na sua IU, ver como os fluxos podem interoperar com LiveData e aprender a desenhar seu app usando fluxos de dados.