Cómo compilar una biblioteca de extensiones de Kotlin

Android KTX es un conjunto de extensiones para las API del framework de Android y las bibliotecas de Android Jetpack más usadas, entre otras. Compilamos estas extensiones para hacer que las llamadas a las API basadas en lenguaje de programación Java desde código Kotlin sean más concisas e idiomáticas aprovechando las funciones del lenguaje, como las propiedades y funciones de las extensiones, las lambdas, las corrutinas y los parámetros con nombre y sus valores predeterminados.

¿Qué es una biblioteca de KTX?

KTX significa extensiones de Kotlin y no es una tecnología ni función especial de ese lenguaje. Tan solo se trata de un nombre que adoptamos para las bibliotecas de Kotlin de Google que extienden la funcionalidad de las API realizadas originalmente en el lenguaje de programación Java.

Lo bueno de las extensiones de Kotlin es que cualquiera puede compilar una biblioteca para sus propias API o incluso para las bibliotecas de terceros que uses en tus proyectos.

En este codelab, encontrarás algunos ejemplos que muestran cómo agregar extensiones simples que aprovechan las funciones del lenguaje Kotlin. También analizaremos cómo convertir una llamada asíncrona de una API basada en devoluciones de llamada en una función de suspensión y un Flow (un flujo asíncrono basado en corrutinas).

Qué compilarás

En este codelab, trabajarás en una aplicación simple que obtiene y muestra la ubicación actual del usuario. Tu app hará lo siguiente:

  • Obtendrá la ubicación más reciente informada por el proveedor de ubicación.
  • Se registrará para recibir actualizaciones en tiempo real de la ubicación del usuario mientras la app esté en ejecución.
  • Mostrará la ubicación en la pantalla y se ocupará de los estados de error en caso de que la ubicación no esté disponible.

Qué aprenderás

  • Cómo agregar extensiones de Kotlin a las clases existentes.
  • Cómo convertir una llamada asíncrona que muestra un solo resultado en una función de suspensión de corrutinas.
  • Cómo usar el Flow para obtener datos de una fuente que puede emitir un valor muchas veces.

Requisitos

  • Una versión reciente de Android Studio (se recomienda la versión 3.6 o una posterior)
  • Android Emulator o un dispositivo conectado a través de USB
  • Conocimientos básicos sobre el desarrollo de Android y el lenguaje Kotlin
  • Conocimientos básicos de las corrutinas y las funciones de suspensión

Descarga el código

Haz clic en el siguiente vínculo a fin de descargar todo el código de este codelab:

Descarga el código fuente

… o clona el repositorio de GitHub desde la línea de comandos con el siguiente comando:

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

El código de este codelab se encuentra en el directorio ktx-library-codelab.

En el directorio del proyecto, encontrarás varias carpetas step-NN que contienen el estado final deseado de cada paso de este codelab a modo de referencia.

Haremos todo tu trabajo de codificación en el directorio work.

Cómo ejecutar la app por primera vez

Abre la carpeta raíz (ktx-library-codelab) en Android Studio y, luego, selecciona la configuración de ejecución work-app en el menú desplegable, como se muestra a continuación:

79c2a2d2f9bbb388.png

Presiona el botón Run 35a622f38049c660.png a fin de probar tu app:

58b6a81af969abf0.png

Esta app aún no está haciendo nada interesante. Le faltan algunas partes de modo que se puedan mostrar datos. Agregaremos la funcionalidad que falta en los pasos subsiguientes.

Una forma más fácil de verificar los permisos

58b6a81af969abf0.png

Si bien la app se ejecuta, solo muestra un error: no puede obtener la ubicación actual.

Eso se debe a que le falta el código para solicitar al usuario el permiso de ubicación del tiempo de ejecución.

Abre MainActivity.kt y busca el siguiente 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)
//  }

Si quitas los comentarios del código y ejecutas la app, esta te solicitará el permiso y procederá a mostrarte la ubicación. Sin embargo, este código resulta difícil de leer por varios motivos:

  • Usa un método estático checkSelfPermission de la clase de utilidad ActivityCompat, que solo existe a fin de contener métodos para la retrocompatibilidad.
  • El método siempre toma una instancia Activity como primer parámetro, ya que es imposible agregar un método a una clase del framework en el lenguaje de programación Java.
  • Siempre estamos verificando si el permiso es un PERMISSION_GRANTED, por lo que sería bueno obtener directamente un booleano true si el permiso se otorgó y uno falso de lo contrario.

Queremos convertir el código detallado que se muestra arriba en algo más corto, como el siguiente:

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

Reduciremos el código con la ayuda de una función de extensión en la Actividad. En el proyecto, encontrarás otro módulo llamado myktxlibrary. Abre ActivityUtils.kt desde ese módulo y agrega la siguiente función:

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

Veamos aquí lo que sucede:

  • fun en el alcance más externo (no dentro de una class) significa que definimos una función de nivel superior en el archivo.
  • Activity.hasPermission define una función de extensión con el nombre hasPermission en un receptor de tipo Activity.
  • Toma el permiso como un argumento String y muestra un Boolean que indica si se otorgó el permiso.

Entonces, ¿qué es un "receptor de tipo X"?

Verás esto con mucha frecuencia cuando leas la documentación de las funciones de extensión de Kotlin. Significa que siempre se llamará a esta función en una instancia de una Activity (en nuestro caso) o de sus subclases y, dentro del cuerpo de la función, nos referimos a esa instancia mediante la palabra clave this (que también puede ser implícita; es decir, podemos omitirla por completo).

En realidad, este es el objetivo de las funciones de extensión: agregar nuevas funcionalidades a una clase que no podemos o no queremos cambiar de otra manera.

Veamos cómo la llamaremos en nuestra MainActivity.kt. Ábrelo y cambia el código de los permisos por:

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

Si ejecutas la app ahora, podrás ver la ubicación en la pantalla.

c040ceb7a6bfb27b.png

Un asistente para darle formato al texto de la Ubicación

Sin embargo, el texto de la ubicación no se ve demasiado bien. Está usando el método Location.toString predeterminado, que no se creó para mostrarse en una IU.

Abre la clase LocationUtils.kt en myktxlibrary. Este archivo contiene extensiones para la clase Location. Completa la función de extensión Location.format para mostrar una String con formato y, luego, modifica Activity.showLocation en ActivityUtils.kt a fin de usar la extensión.

Puedes revisar el código de la carpeta step-03 si tienes problemas. El resultado final debería tener el siguiente aspecto:

b8ef64975551f2a.png

Proveedor de ubicación combinada de Servicios de Google Play

El proyecto de app en el que estamos trabajando usa el proveedor de ubicación combinada de Servicios de Google Play para obtener datos de ubicación. La API en sí es bastante simple, pero debido a que la obtención de la ubicación del usuario no es una operación instantánea, todas las llamadas a la biblioteca deberán ser asincrónicas, lo que complicará nuestro código con devoluciones de llamada.

Hay dos aspectos relacionados con la obtención de la ubicación del usuario. En este paso, nos enfocaremos en obtener la última ubicación conocida, si está disponible. En el paso siguiente, veremos las actualizaciones periódicas de ubicación cuando se esté ejecutando la app.

Obtén la última ubicación conocida

En Activity.onCreate, inicializaremos el FusedLocationProviderClient, que será nuestro punto de entrada a la biblioteca.

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

En Activity.onStart, invocaremos getLastKnownLocation(), que ahora tiene el siguiente aspecto:

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 puedes ver, lastLocation es una llamada asíncrona que puede completarse con éxito o arrojar un error. Para cada uno de esos resultados, deberemos registrar una función de devolución de llamada que establezca la ubicación en la IU o muestre un mensaje de error.

En este momento, el código no necesariamente se verá muy complicado a raíz de las devoluciones de llamadas, pero en un proyecto real tal vez desees procesar la ubicación, guardarla en una base de datos o subirla a un servidor. Muchas de estas operaciones también son asíncronas, y agregar devoluciones de llamada una tras otra haría que, en poco tiempo, nuestro código no se pueda leer, el cual podría verse de la siguiente manera:

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()
   }
}

Aún peor, el código anterior tiene problemas de fuga de memoria y operaciones, ya que los objetos de escucha nunca se quitan cuando finaliza la Activity que los contiene.

Buscaremos una mejor manera de resolver esto mediante corrutinas, que te permitirán escribir código asíncrono que se parezca a un bloque normal de código imperativo de arriba hacia abajo sin realizar llamadas de bloqueo en el subproceso de llamadas. Además de esto, las corrutinas también se pueden cancelar, lo que nos permitirá hacer una limpieza cada vez que salgan del alcance.

En el siguiente paso, agregaremos una función de extensión que convierta la API de devolución de llamada existente en una función de suspensión que se pueda llamar desde un alcance de corrutinas vinculado a tu IU. Queremos que el resultado final se vea similar al siguiente:

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
    }
}

Crea una función de suspensión con suspendCancellableCoroutine

Abre LocationUtils.kt y define una nueva función de extensión en el FusedLocationProviderClient:

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

Antes de pasar a la parte de implementación, descomprimamos esta firma de la función:

  • Ya conoces la función de extensión y el tipo de receptor de las partes anteriores de este codelab: fun FusedLocationProviderClient.awaitLastLocation
  • suspend significa que se trata de una función de suspensión, un tipo de función especial que solo se puede llamar dentro de una corrutina o de otra función suspend.
  • El tipo de resultado de su llamada será Location, como si fuera una manera síncrona de obtener una ubicación resultante de la API.

A fin de compilar el resultado, usaremos suspendCancellableCoroutine, un bloque de compilación de bajo nivel que crea funciones de suspensión desde la biblioteca de corrutinas.

suspendCancellableCoroutine ejecuta el bloque de código que se le pasó como parámetro y, luego, suspende la ejecución de la corrutina mientras se espera un resultado.

Intentemos agregar a nuestro cuerpo de funciones las devoluciones de llamada exitosas y las que muestras un error, tal como hemos visto en la llamada de lastLocation anterior. Lamentablemente, como puedes ver en los comentarios a continuación, esa cosa obvia que queremos hacer (mostrar un resultado) no resulta posible en el cuerpo de la devolución de llamada:

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
    }
}

Eso se debe a que la devolución de llamada se produce mucho después de que finaliza la función circundante, y no hay dónde mostrar el resultado. Aquí es donde aparecerá suspendCancellableCoroutine con la continuation que se proporciona a nuestro bloque de código. Podemos usarlo para brindar un resultado a la función de suspensión en el futuro mediante continuation.resume. Maneja el caso de error con continuation.resumeWithException(e) a fin de propagar correctamente la excepción al sitio de la llamada.

En general, siempre debes asegurarte de que, en algún momento, muestres un resultado o arrojes una excepción de modo que la corrutina no quede suspendida de forma indefinida mientras se espera un resultado.

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

Eso es todo. Acabamos de exponer una versión de suspensión de la API de la última ubicación conocida que se puede consumir a partir de corrutinas en nuestra app.

Cómo llamar a una función de suspensión

Modifiquemos nuestra función getLastKnownLocation en MainActivity a fin de llamar a la nueva versión de corrutina de la llamada correspondiente a la última ubicación conocida:

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 mencionamos antes, las funciones de suspensión siempre deben llamarse desde otras funciones de suspensión para asegurar que se ejecuten dentro de una corrutina, lo que significa que tendremos que agregar un modificador de suspensión a la función getLastKnownLocation. De lo contrario, obtendremos un error en el IDE.

Ten en cuenta que podemos usar un bloque try-catch normal para el manejo de excepciones. Podemos mover este código desde la devolución de llamada fallida, ya que las excepciones provenientes de la API de Location ahora se propagan correctamente, como ocurre en un programa imperativo y normal.

A los efectos de iniciar una corrutina, en general usaríamos CoroutineScope.launch, para el que necesitamos un alcance de corrutinas. Afortunadamente, las bibliotecas de Android KTX incluyen varios alcances predefinidos para objetos de ciclo de vida comunes, como Activity, Fragment y ViewModel.

Agrega el siguiente código 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()
}

Debes poder ejecutar tu app y verificar que funcione antes de continuar con el siguiente paso, en el que presentaremos el Flow de una función que emite resultados de ubicación varias veces.

Ahora nos enfocaremos en la función startUpdatingLocation(). En el código actual, registramos un objeto de escucha con el proveedor de ubicación combinada a fin de obtener actualizaciones de ubicación periódicas cada vez que el dispositivo del usuario se mueve en el mundo real.

Para mostrar lo que queremos lograr con una API basada en un Flow, veamos primero las partes de MainActivity que quitaremos de esta sección y, en su lugar, moveremos los detalles de implementación de nuestra nueva función de extensión.

En nuestro código actual, hay una variable para hacer un seguimiento de si comenzamos a escuchar actualizaciones:

var listeningToUpdates = false

También existe una subclase de la clase de devolución de llamada base y nuestra implementación para la función de devolución de llamada actualizada de la ubicación:

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

También tenemos el registro inicial del objeto de escucha (que puede fallar si el usuario no otorgó los permisos necesarios) junto con devoluciones de llamadas, ya que se trata de una llamada asíncrona:

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 último, cuando la pantalla ya no esté activa, realizamos una limpieza:

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

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

Puedes borrar todos esos fragmentos de código de MainActivity y dejar solo una función startUpdatingLocation() vacía que usaremos más adelante para comenzar a recopilar nuestro Flow.

callbackFlow: un compilador de Flow para API basadas en devoluciones de llamada

Vuelve a abrir LocationUtils.kt y define otra función de extensión en FusedLocationProviderClient:

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

Hay algunas acciones que debemos tomar aquí a los efectos de replicar la funcionalidad que acabamos de borrar del código MainActivity. Utilizaremos callbackFlow(), una función de compilador que muestra un Flow, que es adecuado para emitir datos desde una API basada en devoluciones de llamada.

El bloque que se pasa a callbackFlow() se define con un ProducerScope como su receptor.

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

ProducerScope encapsula los detalles de implementación de un callbackFlow, como el hecho de que hay un Channel que respalda el Flow creado. Sin entrar en detalles, algunos compiladores y operadores de Flow usan Channels internamente y, a menos que escribas tu propio operador o compilador, no necesitarás preocuparte por estos detalles de bajo nivel.

Solo usaremos algunas funciones que ProducerScope expone a fin de emitir datos y administrar el estado del Flow.

Para comenzar, creemos un objeto de escucha para la API de ubicación:

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 ProducerScope.offer a fin de enviar datos de ubicación al Flow a medida que se reciben.

A continuación, registra la devolución de llamada con el FusedLocationProviderClient y asegúrate de controlar cualquier error:

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")
}

FusedLocationProviderClient.requestLocationUpdates es una función asíncrona (como lastLocation) que usa devoluciones de llamada para indicar cuándo se completó de forma correcta y cuándo falló.

Aquí, podemos ignorar el estado de éxito, ya que simplemente significa que, en el futuro, se llamará a onLocationResult y comenzaremos a generar resultados en Flow.

En caso de falla, cerraremos de forma inmediata el Flow con una Exception.

Lo último que siempre debes llamar dentro de un bloque que pasado a callbackFlow es awaitClose. Este proporciona un lugar conveniente para colocar cualquier código de limpieza a fin de liberar recursos en caso de que se complete o cancele el Flow (sin importar si ocurrió con una Exception o no):

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
    }
}

Ahora que tenemos todas las partes (registrar un objeto de escucha, escuchar las actualizaciones y realizar la limpieza), volvamos a MainActivity para efectivamente usar el Flow de modo que se muestre la ubicación.

Recopila el Flow

Modifiquemos nuestra función startUpdatingLocation en MainActivity a los efectos de invocar el compilador del Flow y comenzar a recopilarlo. Una implementación básica puede tener un aspecto como el siguiente:

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() es un operador de terminal que inicia la operación real del Flow. En él, recibiremos todas las actualizaciones de ubicación emitidas por nuestro compilador de callbackFlow. Debido a que collect es una función de suspensión, se deberá ejecutar dentro de una corrutina, que lanzaremos mediante el lifecycleScope.

También puedes observar que los operadores intermedios conflate() y catch() se invocan en el Flow. Hay muchos operadores que vienen con la biblioteca de corrutinas que te permiten filtrar y transformar flujos de manera declarativa.

Combinar un flujo significa que solo querremos recibir la actualización más reciente, siempre que las actualizaciones se emitan más rápido de lo que el recopilador puede procesarlas. Se adapta bien a nuestro ejemplo, ya que solo queremos mostrar la ubicación más reciente en la IU.

catch, como el nombre sugiere, te permitirá controlar cualquier excepción que se haya arrojado de manera ascendente, en este caso, en el compilador de locationFlow. Puedes considerar que las operaciones ascendentes son aquellas que se aplican antes de la actual.

Entonces, ¿cuál es el problema del fragmento anterior? Si bien no ocasionará una falla en la app y se limpiará correctamente después de que la actividad se DESTRUYA (gracias a lifecycleScope), no tiene en cuenta cuándo se detiene la actividad (p. ej., cuando no está visible).

Esto significa que no solo actualizaremos la IU cuando no sea necesario, sino que el Flow mantendrá activa la suscripción a los datos de ubicación y desperdiciará batería y ciclos de CPU.

Una forma de solucionar este problema es convertir el Flow en un LiveData mediante la extensión Flow.asLiveData de la biblioteca de LiveData KTX. LiveData sabe cuándo observar y cuándo pausar la suscripción, y reiniciará el Flow subyacente según sea necesario.

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())
        })
}

Ya no se necesita el objeto lifecycleScope.launch explícito porque asLiveData proporcionará el alcance necesario para ejecutar el Flow. La llamada de observe en realidad proviene de LiveData y no está relacionada con las corrutinas o el Flow; solo es la manera estándar de observar un LiveData con un LifecycleOwner. LiveData recopilará el Flow subyacente y emitirá las ubicaciones a su observador.

Dado que la recreación y recopilación del Flow se manejarán de forma automática ahora, deberíamos mover nuestro método startUpdatingLocation() de Activity.onStart (que puede ejecutarse muchas veces) a Activity.onCreate:

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

Ahora podrás ejecutar tu app y verificar cómo reacciona a la rotación si presionas los botones Inicio y Atrás. Consulta el logcat a fin de ver si se imprimirán nuevas ubicaciones cuando la app esté en segundo plano. Si la implementación se hizo de forma correcta, la app debería pausar y reiniciar la recopilación de Flows cuando se presione Inicio y, luego, regresar a la app.

Acabas de compilar tu primera biblioteca de KTX.

¡Felicitaciones! Lo que lograste en este codelab es muy similar a lo que normalmente ocurriría cuando se compila una biblioteca de extensiones de Kotlin para una API basada en Java existente.

En resumen, hiciste lo siguiente:

  • Agregaste una función conveniente a los efectos de verificar los permisos de una Activity.
  • Proporcionaste una extensión de formato de texto en el objeto Location.
  • Expusiste una versión de corrutinas de las API de Location a fin de obtener la última ubicación conocida y actualizaciones periódicas de la ubicación mediante Flow.
  • Si quieres, puedes limpiar el código, agregar algunas pruebas y distribuir la biblioteca de location-ktx a otros desarrolladores de tu equipo de modo que puedan beneficiarse de ella.

A fin de compilar un archivo AAR para la distribución, ejecuta la tarea :myktxlibrary:bundleReleaseAar.

Puedes seguir pasos similares para cualquier otra API que podría beneficiarse de las extensiones de Kotlin.

Define mejor la arquitectura de la aplicación por medio de Flows

Como mencionamos antes, no es conveniente lanzar operaciones desde la Activity como lo hicimos en este codelab. Puedes seguir este codelab para aprender a observar flujos desde ViewModels en tu IU, así como la manera en que los flujos interoperan con LiveData y aquella en la que puedes diseñar tu app usando flujos de datos.