Formatos de String en Kotlin

Este artículo es un complemento a los cursos gratuitos de Java para Principiantes y Kotlin para Principiantes. La explicación la daré en Kotlin pero en Java aplica prácticamente lo mismo.

En Kotlin (Y la mayoría de los lenguajes de programación) podemos utilizar formatos para reemplazar variables dentro de los String, supongamos que queremos imprimir nuestro nombre, edad y peso, podríamos hacer algo así

val name: String = "Walter White"
val age: Int = 50
val weight: Double = 75.5

val description = "Me llamo " + name + ", tengo " + age + " años y peso " + weight " kg."
println(description)

// O mejor aún, en Kotlin podemos hacer esto
val description = "Me llamo $name, tengo $age años y peso $weight kg"
println(description)

// Ahora, si usamos el String format
val description = String.format("Me llamo %s, tengo %d años y peso %.2f kg", name, age, weight)
println(description)


Las 3 soluciones son correctas e imprimen lo mismo, pero la tercera es la más útil al usar Strings en Android donde tendrás que reemplazar variables porque te ayuda a tener código más limpio y soportar diferentes idiomas.

Si observas en la última opción, lo que hacemos es poner ciertos formatos dentro del String y luego al final, separados por coma, ponemos las variables que queremos que se reemplacen en el String: %s para otros strings como name, %d para enteros como age y %f para doubles y floats, en el .2 que ves en el %f el 2 es la cantidad de decimales que queremos que se impriman, así por ejemplo en este caso se imprimiría el peso como 75.50.

Aquí te dejo una tabla con los formatos más comunes, también pueden ser usados en Java:

FormatoTipo de dato
%bBoolean
%cChar
%dInteger
%eFloat en notación científica
%fFloat y Double (Agrega %.nf para forzar n decimales)
%oFormato Octal
%sStrings
%xFormato Hexadecimal

Hay otros 3 o 4 formatos más pero son tan poco usuales que prefiero no abrumarte.

Si tienes dudas te invito a dejarlas en los comentarios, también te invito a suscribirte al blog, canal de Youtube, redes y todo lo que te venga en gana 🤪.

Todogs: Un side project que se convirtió en algo más.

Hace alrededor de 2 meses me propuse actualizarme en lo último de Android y aprender temas que por algún tiempo había querido pero que no me había dado a la tarea de empezar, en concreto quería aprender los siguientes temas:

  • CameraX: Como hacer cosas interesantes con la cámara.
  • Machine Learning con Android y Tensorflow: Conocía ambas cosas pero por separado y quería juntarlas.
  • Dependency Injection: Este tema me había eludido por mucho tiempo, lo consideraba algo complejo y no sabía exactamente su utilidad.
  • Probar usar solo infraestructura de Firebase y Serverless.
  • In-App purchases: No había tenido la oportunidad de trabajar con ventas dentro de una app.

Llevo años en los que siempre estoy creando side projects, puesto que el fin que quiero para mi carrera además de Hackaprende es crear otra empresa que haga algo genial que llegue a muchas personas, y creo que para esto no hay como estar intentando sin parar. Entonces empecé a pensar “Si quiero aprender esas cosas ¿qué side project podría hacer?”.

Empecé a pensar en cosas que me gustan, que serían divertidas de hacer y que además incluyeran al menos algunos de los temas que quería aprender, y después de un par de semanas se me ocurrió Todogs: Un Pokédex de perros donde pudieras reconocer sus razas con la cámara de tu celular y coleccionarlos todos. Aquí un pequeño video de su funcionamiento:

Cómo suelo hacer, no lo pensé mucho y empecé rápidamente a desarrollar, ya el tiempo (Y los usuarios) me dirá si es buena idea o no, lo importante es empezar y después de todo aquí el enfoque era aprender los temas que puse arriba, resultó que con la app pude aprender TODOS los temas, necesita la cámara, Machine Learning para reconocer a las razas, aproveché para usar Hilt para Dependency Injection y resultó que es una maravilla para hacer testing y para tener código bien estructurado. Todo el backend lo hice con Firebase y compañía y al final me gustó tanto el proyecto que decidí lanzarlo al mundo e incluso después de probarlo creo que es tan bueno y divertido que me atreví a incluir una versión premium por la que con una muy pequeña cantidad desbloqueas todas las características de la app, por lo que también pude poner en práctica los In-App purchases 😎.

Aunque no es el primer side project que hago, al hacer este pequeño proyecto he aprendido hasta ahora algunas cosas:

  • Algo que empezó como un proyecto para aprender y como hobby se está convirtiendo en un proyecto al que le veo futuro, que estoy dispuesto a difundir y del que me siento orgulloso.
  • No hay mejor forma de aprender algo que haciendo cosas nuevas y que te parezcan divertidas.
  • Empieza ya, no te tardes decidiendo qué hacer, ni tampoco te tardes en lanzar, yo tardé 2 meses en desarrollar y lanzar Todogs y ya aprendí mucho, no hay nada que perder y mucho que ganar. Aquí es muy cierta la frase que dice “Si no te avergüenzas de tu producto es porque ya lanzaste muy tarde”.
  • Siempre permanece construyendo cosas geniales por tu cuenta, si no es para emprender al menos si para aprender, si algo no funciona ve por la siguiente idea. Alguna tendrá que funcionar.
  • Todogs parecía intimidante al inicio, ¡combinar Android con Machine Learning para identificar perros con la cámara y luego configurar un backend para guardarlos! Parecía una tarea enorme, y de hecho lo es, pero una vez que empiezas te das cuenta que al dividir la tarea en cosas más pequeñas deja de ser tan abrumadora. Normalmente las cosas son más fáciles de lo que parecen una vez que las analizas con la mente fría y entras al ruedo con buena actitud y seguridad.

Por cierto hace mucho que quiero volver a repasar los temas de iOS y está app también me servirá para eso.

Algo contra-intuitivo de Todogs con respecto a lo que pienso que es lo ideal es que este proyecto es más un gain que un pain, es decir, no resuelve un problema que yo tenga, sino que es algo divertido relacionado con algo que me gusta mucho que son los perros, pero como mencioné arriba, esto no empezó con la idea de convertirse en producto, sino de aprender y hacer lago genial, así que por esta vez lo dejaré pasar 😜.

En fin, seguiré compartiendo cómo me va con esta aplicación, siempre da un poco de vergüenza compartir cuando las cosas no funcionan (espero no sea el caso) pero ¡hey! Como dice Anton Ego: Cualquier basura ya es mejor que la mejor crítica que pueda recibir. Hacer algo, aunque resulte ser un mal producto, siempre será mejor que no hacer nada.

Con mucha humildad te presento Todogs. Espero que la disfrutes y toda la retroalimentación que tengas déjala en los comentarios, te la agradeceré muchísimo.

https://play.google.com/store/apps/details?id=com.hackaprende.todogs

Si eres parte de la comunidad Hackaprende en Slack mándame un mensaje y con gusto te paso un código para que puedas ser usuario premium sin costo alguno.

5 Consejos para formar un buen JSON

No hay como recibir un JSON bonito para hacer las cosas fáciles al momento de usarlo, si usamos una API pública no hay mucho que hacer, pero si tienes poder para decidir cómo quieres que te lleguen los datos (O enviarlos si tu eres de backend), te recomiendo estos consejos que les ahorrarán dolores de cabeza a ti y a tu equipo.

  1. Usa siempre un JSON object como padre, aunque los datos sean una lista de objetos. Esto es porque con librerías como Volley o Retrofit que se usan en Android se complica más si recibes JSONArray en comparación con recibir JSONObject. Por ejemplo, supongamos que vas a recibir una lista de películas, en vez de recibir esto:

[
{
// Película 1
},
{
// Película 2
},
...
]


Te recomiendo hacer esto:

{
movie_list: [
{
// Película 1
},
{
// Película 2
},
...
]
}

2. Maneja un estándar al nombrar las variables del JSON: En lo personal prefiero snake_case pero en realidad no importa si usas camelCase o lo que sea mientras sea así siempre. Ejemplo:

No recomendable:

{
"first_name": "Ted", // Esta llave es snake_case
"lastName": "Mosby", // Esta llave es camelCase
"age": 30,
...
}

Recomendable:

{
"first_name": "Ted",
"last_name": "Mosby", // Ambas son snake_case
"age": 30,
...
}

Usa el mismo formato en TODOS los JSON que se usen en el proyecto y de preferencia en todos los proyectos de la empresa.

3. Evita los null: Los valores null son causantes del problema del millón de dólares (NullPointerException), es muy molesto, lleva mucho trabajo y es muy proclive a errores porque se tienen que estar validando todas las variables del JSON. Si es posible en vez de usar nulls para Strings usa un String vacío, para enteros o doubles puedes usar números negativos o flags booleanos como enabled.

No recomendable:

{
"first_name": null,
"lastName": "null",
"age": null,
...
}

Recomendable:

{
"first_name": "",
"last_name": "",
"age": 0,

"enabled": false,
...
}

4. Evita datos que no llegan en ciertas condiciones: Muy relacionado con el punto anterior, supongamos que tienes un dato para vehículos que es “load_capacity” pero solo se usa cuando el “vehicle_type”: “pickup”, aunque este dato no se use para motocicletas debería siempre llegar una load_capacity aunque sea en 0, esto hace que el JSON sea más consistente y de nuevo evita que tengas que estar validando si el dato existe o no, en el peor de los casos muestras un dato erróneo pero la app no va a tronar (NOTA: hay algunas excepciones, por ejemplo si es una app bancaria es preferible que truene a mostrar una cantidad errónea).

No recomendable:

{
"vehicle_type": "motorcycle", // No hay load_capacity
...
}

Recomendable (Que siempre lleguen los datos aunque lleguen vacíos en vez de que a veces lleguen y a veces no):

{
"vehicle_type": "motorcycle",

"load_capacity": 0,
...
}

5. Especialmente para móviles es preferible comerse una galleta grande que muchas pequeñas: Android por ejemplo enciende su antena para recibir datos cuando haces un request para traer datos de internet para ahorrar batería, una vez que el request termina la antena queda en modo de espera (Idle), es preferible traer todos los datos en un solo request aunque sea un JSON grande que estar haciendo muchas conexiones y encendiendo esta antena constantemente.

Aquí lamentablemente es cuestión de feeling, un ejemplo es una lista de restaurantes como en Uber Eats. Para ver el menú de los restaurantes la opción recomendable es que cuando entras a un restaurante descargas todo el menú del restaurante en un solo JSON (Galleta grande), así no tienes que hacer más requests. La opción que no recomiendo sería descargar cada sección del menú por separado (Galletas pequeñas) porque el usuario tiende a moverse constantemente entres secciones.

¿Estás de acuerdo con estos consejos? ¿Tienes algún otro que también hayas aprendido a golpes de la vida? Te invito a suscribirte y comentar.

La mejor forma de aprender un nuevo lenguaje.

Actualmente tengo 2 cursos gratuitos de lenguajes de programación puros, llamados Java para Principiantes y Kotlin para Principiantes, y pienso lanzar algunos otros próximamente, creo que Python será el siguiente.

La forma en que organizo y explico mis cursos parte de la idea que tengo de que la mejor manera de aprender un lenguaje de programación es esta:

1.- Aprende lo básico rápidamente, no te metas a ver cosas complejas o te aburrirás antes de usar el lenguaje en algo práctico.

2.- Usa el lenguaje con un framework (Java -> Android, Python -> Django, Javascript -> NodeJS, etc) para crear algo divertido, una web o app. Sigue el mismo principio aprendiendo solo lo necesario.

3.- Aprende cosas más complejas del lenguaje conforme las vayas necesitando.

4.- Ahora sí poco a poco profundiza más en los detalles y temas avanzados y vuélvete experto(a).

Debido a que en este tipo de cursos he recibido comentarios como “No profundiza lo suficiente” o “Faltan temas por ver” aquí te menciono que vas y que no vas a encontrar en mis cursos donde enseño lenguajes de programación puros para Principiantes ANTES de que avances más, para que sepas de antemano si el curso es para ti y no te lleves una decepción si no es lo que esperas.

Qué encontrarás en estos cursos:

  • La sintaxis y partes principales del lenguaje: Cómo implementar variables, métodos, clases, interfaces, etc.
  • Principales estructuras de datos para el lenguaje.
  • Consejos y prácticas útiles de programación.

Qué NO encontrarás en los cursos:

  • Temas teóricos avanzados como patrones de diseño. Este tipo de temas los iré agregando en el canal de Youtube y este blog.
  • Implementación del lenguaje en un framework: para implementación de Java en Android está el curso de Android completo con Java.
  • Todo lo que no sea absolutamente necesario para empezar a usar el lenguaje cuanto antes.

Los cursos “para Principiantes” son para ti si eres nuevo(a) en el lenguaje y quieres aprender rápidamente lo más importante del lenguaje para empezar a usarlo cuanto antes sin gastar mucho tiempo y abrumarte. Directo y al grano.

Ahora lo contrario, estos cursos NO son para ti si ya sabes usar bien el lenguaje o si lo que quieres es dominar todo hasta un nivel experto y con un enorme detalle.

En resumen, la idea es que aprendas lo más rápido posible, por lo que trato de que los cursos sean 100% prácticos y te enseñen justo lo necesario para especializarte en lo que quieras, evitando que tengas que tomar un curso de 30 horas solo para empezar a usar el lenguaje.

Tal como menciono arriba, en este blog y el canal de Youtube Hackaprende iré añadiendo temas complementarios y/o más avanzados. Todas las ideas y recomendaciones son más que bien recibidas, por ejemplo si crees que debería agregar algún tema que piensas que es esencial y actualmente no incluyo. También si crees que algún tema actual no está claro te agradecería mucho que me lo comunicaras. Por ahora te felicito por iniciar uno de los cursos y ¡Te deseo mucho éxito!

Android: executePendingBindings() en RecyclerViews

Como nada es perfecto, data binding tiene una desventaja con respecto a findViewById().

Android “pinta” los views en la pantalla cada 16ms en promedio, esta actualización de la pantalla puede suceder en menos tiempo, pero si se hacen procesos pesados en el hilo principal (Main Thread) podría causar que la actualización sea de más de 16ms y empezamos a ver lags en la pantalla. Cuando usamos binding para asignar valores a un view, por ejemplo al asignar un texto a un TextView con binding.myTextView.text = “Some text”, el binding no se ejecuta al instante, sino que espera a que pase ese tiempo de 16ms para medir cuanto el área del nuevo texto y luego pintarlo. Es como cuando juegas con tu perro a lanzar la pelota y va por ella pero luego no la suelta hasta que pasas un rato tratando de quitársela (16ms en promedio).

Esto por lo general no nos afecta, por ejemplo cuando abrimos una nueva Activity y pintamos todo con binding, 16ms no son nada. Lanzaste la pelota a tu perro una vez y se la quitas, no pasa nada.

Pero ahora imagina un recyclerView con decenas de elementos, cuando haces scroll rápidamente y viene el siguiente elemento, puede ser que todavía no hayan pasado los 16ms recomendados, pero podría suceder que cuando aparece el siguiente elemento el binding no haya alcanzado a medir y pintar el anterior, entonces podrían verse lags extraños. Es como si quisieras romper un record de lanzar pelotas y ya viene la siguiente pero tu perro todavía no suelta la anterior.

Esto es poco común, y de hecho si no implementas lo que estoy a punto de mencionarte muchas veces no tendrás problemas, pero es mejor prevenir.

La solución para prevenir esto es que después de pintar los views de tu ViewHolder, mandes llamar binding.executePendingBindings(), así:

inner class EqViewHolder(private val binding: ItemBinding): RecyclerView.ViewHolder(binding.root) {
fun bind(earthquake: Earthquake) {
binding.myTextView.text = someString
binding.myImageView.src = someImage
// Más cosas que hagas con tus views
...
// JUSTO AQUÍ VA 👇
binding.executePendingBindings()
}
}

De esta manera, vas a forzar al binding a que no se espere los 16ms, sino que pinte todo inmediatamente. Sencillo, es solo agregar una linea de código, aunque Android no explica mucho para qué sirve en su documentación oficial y por eso a veces es difícil de recordar.

Espero que los de Android manejen esto automáticamente en el futuro. Mientras tanto te invito a dejar tus comentarios.

Curso Android Completo con Kotlin ¡Ya disponible!

Finalmente después de tanto planear, grabar, editar, corregir y revisar, el nuevo curso de Android Completo con Kotlin ya está listo.

Este curso incluye todos los temas necesarios e importantes para empezar a trabajar como desarrollador Android, usando las mejores prácticas recomendadas por Google. Traté de hacer el curso lo más ameno posible, incluyendo solo los temas importantes, como siempre yendo directo al grano y dejando fuera todo lo que no sea esencial para que puedas aprender en el menor tiempo posible, como rayo ⚡️.

Sin más te invito a que le des un vistazo, aquí encontrarás más información sobre el curso, lo que incluye y lo que necesitas para empezar.

https://hackaprende.com/pagina-de-inicio/cursos/android-completo-con-kotlin/

¡Para los suscriptores del blog hay un súper descuento del 60%!, envíame un correo a hackaprende@gmail.com y si eres suscriptor te mandaré el super cupón.

¡Saludos y mucho éxito!

Comentarios claros en Kotlin.

Recientemente me di a la tarea de documentar el código de una aplicación que estoy desarrollando y seamos sinceros, documentar código es aburrido, tal vez por esto no pude encontrar muy buena información, en particular en la parte de cómo comentarlo. Incluso en la página oficial de Kotlin no vienen ejemplo claros de cómo comentar el código. Después de un buen rato de prueba y error descubrí algunas cosas que creo que vale la pena compartir.

Vamos a verlo con el ejemplo de un método que obtenga el subtotal de una lista del supermercado donde cada elemento tiene un precio, una cantidad y un posible descuento. Además al final le vamos a agregar un impuesto 😭 al ticket. El método podría ser el siguiente.

private fun getTicketTotal(productList: MutableList<Product>, 
taxPercentage: Double): Double {
    var total = 0.0
    for (product in productList) {
        val subtotal = (product.quantity * product.price)
        total += subtotal * ((100 - discount)/100)
    }

    val taxAmount = (total * taxPercentage)/100
    total -= taxAmount

    return total
}

Empecemos por lo más básico, un simple comentario en Kotlin se inserta con dos lineas diagonales // así:

private fun getTicketTotal(productList: MutableList<Product>, 
taxPercentage: Double): Double {
    // Creamos total en 0
    var total = 0.0
    for (product in productList) {
        // Subtotal es el la multiplicación de cantidad por precio
        val subtotal = (product.quantity * product.price)
        total += subtotal * ((100 - discount)/100)
    }

    // Sumamos el impuesto al total
    val taxAmount = (total * taxPercentage)/100
    total -= taxAmount

    return total
}

Para comentarios más largos que ocupen varios renglones podemos insertarlos con
/* ... */, aunque también podríamos tener simplemente varios renglones con //:

private fun getTicketTotal(productList: MutableList<Product>, 
taxPercentage: Double): Double {
    // Creamos total en 0
    var total = 0.0
    for (product in productList) {
        // Subtotal es el la multiplicación de cantidad por precio
        val subtotal = (product.quantity * product.price)
        /* Restamos el posible descuento al subtotal, por ejemplo si 
           el subtotal es de $150 el descuento es de 30%, el total 
           será de 150 * ((100 - 30)/100) = 150 * 0.7 = 105 */
        total += subtotal * ((100 - discount)/100)
    }

    // Obtenemos el monto de impuestos, por ejemplo si el total es 
    // 150 y el impuesto es 15%: 
    // taxAmount = (150 * 15)/100 = 22.5
    val taxAmount = (total * taxPercentage)/100
    total -= taxAmount

    return total
}

Finalmente, para comentar un método completo podemos utilizar /** ... */, aquí podemos indicar también cuales son los parámetros que necesita el método y qué es lo que regresa, así:

/**
* Este método obtiene el total de una lista de [Productos] teniendo
* en cuenta [Producto.precio], [Producto.cantidad], 
* [Producto.descuento] y un posible impuesto.
*
* @param productList La lista de productos de la cual se obtiene el * total
* @param taxPercentage El porcentaje, el cual puede ir de 0 a 100
*
* @return El total después de impuestos.
**/
private fun getTicketTotal(productList: MutableList<Product>, 
taxPercentage: Double): Double {
    // Creamos total en 0
    var total = 0.0
    for (product in productList) {
        // Subtotal es el la multiplicación de cantidad por precio
        val subtotal = (product.quantity * product.price)
        /* Restamos el posible descuento al subtotal, por ejemplo si 
           el subtotal es de $150 el descuento es de 30%, el total 
           será de 150 * ((100 - 30)/100) = 150 * 0.7 = 105 */
        total += subtotal * ((100 - discount)/100)
    }

    // Obtenemos el monto de impuestos, por ejemplo si el total es 
    // 150 y el impuesto es 15%: 
    // taxAmount = (150 * 15)/100 = 22.5
    val taxAmount = (total * taxPercentage)/100
    total -= taxAmount

    return total
}

Como puedes observar hay palabras que encerré entre [] esto es para que puedas dar option + clic en esa clase o incluso en un método o campo dentro de la clase y te lleve directamente a ella. Ahora es mucho más claro lo que hace el método, sí es aburrido, pero tus compañeros (y tu yo del futuro) te lo agradecerán.

NOTA: No todos los métodos e instrucciones necesitan comentarios, hay unos que son auto explicativos.

Si te gustó el artículo compártelo suscríbete y sígueme en mis redes sociales o donde quieras. También te invito a dejar tus comentarios. ¡Saludos y mucho éxito!

Tensorflow en 90 segundos

Recientemente he retomado mi interés por el machine learning y su combinación con las aplicaciones móviles, estaré posteando acá lo que vaya aprendiendo e implementando, para iniciar quiero compartir este pequeño video acerca de qué se trata una de las tecnologías más usadas hoy en día para Machine Learning, se llama Tensorflow y te lo presento en tan solo 90 segundos.

Te invito a suscribirte al canal y al blog para más cursos, artículos, tutoriales y todo tipo de contenido relacionado al diseño, programación y emprendimiento tecnológico.

Usando Corrutinas con Firebase en Android MVVM

Estoy desarrollando un proyecto alterno que utiliza la arquitectura MVVM y Firebase/Firestore, para todo el manejo de los hilos y tareas en segundo plano estoy usando corrutinas (coroutines).

Me acabo de topar con una situación que no me había sucedido, al usar Firebase, este trae sus propios listeners para saber cuando termina un proceso, el cual regresa un DocumentSnapshot que es el que necesitas para extraer los datos. Algo así:

class MyRepository(private val firestoreDb: FirestoreDb) {
    
    suspend fun downloadSomething(listener: (DocumentSnapshot) -> Unit){
        firestoreDb.collection(collectionName).get()
            .addOnCompleteListener {
                task ->
            if (task.isSuccessful) {
               // Do something ...
               listener(myDocumentSnapshot)
            } else {
               // Show error
            }
        }
    }
}

Esto es llamado desde el ViewModel el cual se vería así.

viewModelScope.launch {
    repository.downloadSomething {
        documentSnapshot ->
        // Do Something
    }
}

Pero uno de los puntos principales de las corrutinas es que no necesitemos integrar interfaces y/o listeners para devolver los datos, sino que los podamos devolver en la misma linea para continuar con nuestro código, es decir, en vez de esto:

fun downloadSomething() {
repository.downloadSomething {
result ->
processResult(result)
}
}

Con las corrutinas podemos hacer esto:

suspend fun downloadSomething() {
val result = repository.downloadSomething()
processResult(result)
}

Pero tal y como funciona normalmente Firebase no se puede porque como dije antes, ya trae ese listener .addOnCompleteListener integrado, entonces ¿Cómo lo manejamos para poder usar el resultado con corrutinas? La respuesta que encontré fue usar .await()

await() nos ayuda a esperar a que un proceso se complete sin bloquear otros hilos (Como el Main Thread), haciendo que evitemos tener que llamar un listener una vez que se complete, ya que al terminar se ejecutará la siguiente linea de código del hilo. Pero await() no viene con Firebase sino con las Kotlin Coroutines, por lo que tenemos que agregarlo como dependencia:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.1.1'

Ahora que ya podemos usar await() el código de Firebase quedaría así:

class MyRepository(private val firestoreDb: FirestoreDb) {

    suspend fun downloadSomething(): DocumentSnapshot {
        return withContext(Dispatchers.IO) {
            firestoreDb.collection(collectionName).get().await()
        }
    }
}

Con esto podemos regresar el DocumentSnapshot y continuar con nuestro trabajo de forma secuencial en el ViewModel:

viewModelScope.launch {
    val documentSnapshot = repository.downloadSomething()
    // Continuar secuencialmente
}

Esto trae ventajas para hacer testing, para manejar excepciones, para cumplir con el MVVM y para manejar los LiveData.

NOTA: Esto solo funciona para cuando usas get().addOnCompleteListener(), todavía no he encontrado la manera de hacerlo funcionar con cambios en tiempo real con addSnapshotListener(), buscaré la manera para postear.

Como siempre todos los comentarios y dudas son más que bienvenidos.

¿Qué es JSON? – En palabras simples

Algunos de mis alumnos me han pedido que les pase un artículo sobre JSON y como funciona, es más sencillo de lo que crees, aquí te dejo un video para que conozcas JSON en 3 minutos.

Como siempre los comentarios o dudas son bienvenidos. Inscríbete al canal y al blog para recibir los últimos artículos, videos, cursos, etc que voy creando.