Introducción a la depuración

1. Antes de comenzar

Es probable que los usuarios de software hayan encontrado un error. Un error es un problema que se genera en una parte del software y que causa un comportamiento no deseado, por ejemplo, la app falla o una función no hace lo esperado. Todos los desarrolladores, sin importar su experiencia, introducen errores cuando escriben código; una de las habilidades más importantes para un desarrollador de Android es poder identificarlos y corregirlos. Es común ver versiones completas de apps dedicadas a corregir errores. Por ejemplo, consulta los detalles de la versión de Google Maps a continuación:

9d5ec1958683e173.png

El proceso de corrección de errores se denomina depuración. El famoso científico informático Brian Kernighan dijo una vez que "la herramienta de depuración más eficaz sigue siendo prestar atención, junto con sentencias de impresión ubicadas correctamente". Si bien es posible que ya te hayas familiarizado con la sentencia println() de Kotlin en codelabs anteriores, las personas que desarrollan profesionalmente para Android usan registros para organizar mejor el resultado de su programa. En este codelab, aprenderás a usar los registros en Android Studio y cómo se pueden usar como herramienta de depuración. También aprenderás a leer registros de mensajes de error, llamados seguimientos de pila, para identificar y resolver errores. Por último, aprenderás a investigar errores por tu cuenta, así como a capturar resultados desde Android Emulator, ya sea como una captura de pantalla o un GIF de tu app en ejecución.

Requisitos previos

  • Debes saber cómo navegar por un proyecto en Android Studio.

Qué aprenderás

Una vez que completes el codelab, podrás hacer lo siguiente:

  • Escribir registros mediante android.util.Logger
  • Saber cuándo usar diferentes niveles de registro
  • Usar los registros como una herramienta de depuración sencilla y potente
  • Cómo encontrar información significativa en un seguimiento de pila
  • Buscar mensajes de error para resolver fallas de la aplicación
  • Tomar capturas de pantalla y GIF animados desde Android Emulator

Requisitos

  • Una computadora que tenga Android Studio instalado

2. Creación de un proyecto nuevo

En lugar de usar una app grande y compleja, comenzaremos con un proyecto en blanco para demostrar las instrucciones de registro y su uso para la depuración.

Crea un nuevo proyecto de Android Studio, como se muestra a continuación.

  1. En la pantalla New Project, selecciona Empty Activity.

72a0bbf2012bcb7d.png

  1. Asígnale a la app el nombre Debugging. Asegúrate de que el lenguaje esté configurado en Kotlin y no modifiques nada más.

60a1619c07fae8f5.png

Una vez que hayas creado el proyecto, aparecerá un nuevo proyecto de Android Studio que mostrará un archivo llamado MainActivity.kt.

e3ab4a557c50b9b0.png

3. Resultado de registro y depuración

En las lecciones anteriores, usaste la sentencia println() de Kotlin para producir resultados de texto. En una app para Android, la práctica recomendada a la hora de registrar resultados es usar Log. Hay varias funciones para registrar resultados, que tienen el formato Log.v(), Log.d(), Log.i(), Log.w() o Log.e(). Estos métodos tienen dos parámetros: el primero, llamado "tag", es una cadena que identifica el origen del mensaje de registro (como el nombre de la clase que registró el texto). El segundo es el mensaje de registro real.

Realiza los siguientes pasos para comenzar a usar el registro en tu proyecto en blanco.

  1. En MainActivity.kt, antes de la declaración de clase, agrega una constante llamada TAG y establece su valor como el nombre de la clase, MainActivity.
private const val TAG = "MainActivity"
  1. Agrega una función nueva a la clase MainActivity llamada logging(), como se muestra.
fun logging() {
    Log.v(TAG, "Hello, world!")
}
  1. Llama a logging() en onCreate(). El nuevo método onCreate() debería verse de la siguiente manera.
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    logging()
}
  1. Ejecuta la app para ver los registros en acción. Los registros aparecen en la ventana de Logcat, en la parte inferior de la pantalla. Debido a que Logcat mostrará el resultado de otros procesos en el dispositivo (o el emulador), puedes seleccionar tu app (com.example.debug) en el menú desplegable para filtrar los registros que no sean relevantes para ella.

199c65d11ee52b5c.png

Deberías poder ver la frase "Hello, world!" como resultado. Si es necesario, escribe "hello" en el cuadro de búsqueda de la parte superior de la ventana de Logcat para hacer una búsqueda en todos los registros.

92f258013bc15d12.png

Niveles de registro

Existen diferentes funciones de registro que se nombran con letras distintas porque se corresponden con niveles de registro distintos. Según el tipo de información que quieras generar, debes usar un nivel de registro diferente para filtrarla en el resultado Logcat. Existen cinco niveles de registro principales que usarás con frecuencia.

Nivel de registro

Caso de uso

Ejemplo

ERROR

Los registros de ERROR informan que se produjo un error grave, como el motivo por el que falló una app.

Log.e(TAG, "The cake was left in the oven for too long and burned.").

WARN

Los registros de WARN son menos graves que un error, pero aun así informan sobre algo que debería corregirse para evitar un error más grave. Por ejemplo, si llamas a una función que está obsoleta, no se recomienda su uso para priorizar una alternativa más nueva.

Log.w(TAG, "This oven does not heat evenly. You may want to turn the cake around halfway through to promote even browning.")

INFO

Los registros de INFO proporcionan información útil, como que una operación se completó con éxito.

Log.i(TAG, "The cake is ready to be served.").println("The cake has cooled.")

DEBUG

Los registros de DEBUG contienen información que puede resultar útil a la hora de investigar un problema. Estos registros no están presentes en las compilaciones de lanzamiento, como una que publicarías en Google Play Store.

Log.d(TAG, "Cake was removed from the oven after 55 minutes. Recipe calls for the cake to be removed after 50 - 60 minutes.")

VERBOSE

Como su nombre lo indica, la verbosidad es el nivel de registro menos específico. Lo que se considera un registro de depuración, en comparación con un registro detallado, es un poco subjetivo, pero, en general, un registro detallado es algo que se puede quitar después de implementar una función, mientras que un registro de depuración puede ser útil para la depuración. Estos registros tampoco se incluyen en las compilaciones de lanzamiento.

Log.v(TAG, "Put the mixing bowl on the counter.")Log.v(TAG, "Grabbed the eggs from the refrigerator.")Log.v(TAG, "Plugged in the stand mixer.")

Ten en cuenta que no hay reglas establecidas respecto de cuándo usar cada tipo de nivel de registro, en particular, cuándo usar DEBUG y VERBOSE. Los equipos de desarrollo de software pueden crear sus propios lineamientos sobre cuándo usar cada nivel de registro o decidir no usar algunos de ellos, como VERBOSE. Lo importante que debes recordar sobre estos dos niveles de registro es que no están presentes en las compilaciones de lanzamiento, por lo que el uso de registros para depurar no afectará el rendimiento de las apps publicadas, mientras que las sentencias println() permanecen en las compilaciones de lanzamiento y tienen un rendimiento negativo.

Veamos cómo son estos diferentes niveles de registro en Logcat.

  1. En MainActivity.kt, reemplaza el contenido del método logging() con lo siguiente:
fun logging() {
    Log.e(TAG, "ERROR: a serious error like an app crash")
    Log.w(TAG, "WARN: warns about the potential for serious errors")
    Log.i(TAG, "INFO: reporting technical information, such as an operation succeeding")
    Log.d(TAG, "DEBUG: reporting technical information useful for debugging")
    Log.v(TAG, "VERBOSE: more verbose than DEBUG logs")
}
  1. Ejecuta tu app y observa el resultado en Logcat. Si es necesario, filtra el resultado para mostrar solo los registros del proceso com.example.debug. También puedes filtrar el resultado para mostrar solo los registros con la etiqueta "MainActivity". Para ello, selecciona Edit Filter Configuration en el menú desplegable de la parte superior derecha de la ventana de Logcat.

383ec6d746bb72b1.png

  1. Luego, escribe "MainActivity" en Log Tag y crea un nombre para el filtro, como se muestra.

e7ccfbb26795b3fc.png

  1. Ahora, solo deberías ver mensajes de registro con la etiqueta "MainActivity".

4061ca006b1d278c.png

Observa que hay una letra antes del nombre de la clase, por ejemplo, W/MainActivity, que corresponde al nivel de registro. Además, el registro WARN se muestra en azul, mientras que el registro ERROR se muestra en rojo, al igual que el error irrecuperable en el ejemplo anterior.

  1. Así como puedes filtrar el resultado de depuración por proceso, también puedes filtrar el resultado por nivel de registro. De forma predeterminada, este parámetro está configurado en Verbose, que mostrará registros VERBOSE y niveles de registro superiores. Selecciona Warn en el menú desplegable y observa que ahora solo se muestran los registros de nivel WARN y ERROR.

c4aa479a8dd9d4ca.png

  1. Una vez más, cambia el menú desplegable a Assert y observa que no se muestra ningún registro. Esto filtra todo el nivel ERROR o inferior.

ee3be7cfaa0d8bd1.png

Aunque parezca que te tomas demasiado en serio las sentencias println(), a medida que compiles apps más grandes, habrá muchos más resultados de Logcat, y usar diferentes niveles de registro te permitirá seleccionar la información más útil y relevante. El uso de registros se considera una práctica recomendada y es preferible antes que println() en el desarrollo en Android, ya que la depuración y los registros detallados no afectarán el rendimiento en las compilaciones de lanzamiento. También puedes filtrar registros según diferentes niveles. Elegir el nivel de registro correcto beneficiará a otros miembros de tu equipo de desarrollo que quizás no estén tan familiarizados con el código como tú, y facilitará la identificación y resolución de errores.

4. Registros con mensajes de error

Ingresa un error

Un proyecto en blanco no requiere mucha depuración. Muchos de los errores que encontrarás como desarrollador de Android implican fallas en las apps, lo que obviamente no brinda una experiencia del usuario óptima. Agreguemos código que provoque la falla de esta app.

Tal vez recuerdes haber aprendido en la clase de matemáticas que no se puede dividir un número por cero. Veamos qué sucede cuando intentamos dividir por cero en el código.

  1. Agrega la siguiente función a tu MainActivity.kt sobre la función logging(). Este código comienza con dos números y usa repeat para registrar el resultado de dividir el numerador por el denominador cinco veces. Cada vez que se ejecuta el código en el bloque repeat, el valor del denominador se reduce en uno. En la quinta y última iteración, la app intenta dividir por cero.
fun division() {
    val numerator = 60
    var denominator = 4
    repeat(5) {
        Log.v(TAG, "${numerator / denominator}")
        denominator--
    }
}
  1. Después de la llamada a logging() en onCreate(), agrega una llamada a la función division().
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    logging()
    division()
}
  1. Vuelve a ejecutar la app y observa que falla. Si te desplazas hacia abajo hasta los registros de tu clase MainActivity.kt, verás los de la función logging() que definiste antes, los registros detallados de la función division() y, luego, un registro de error rojo que explica por qué falló la app.

12d87f287661a66.png

Anatomía de un seguimiento de pila

El registro de errores que describe la falla (también llamada excepción) se denomina seguimiento de pila. El seguimiento de pila muestra todas las funciones a las que se llamó hasta llegar a la excepción. Comienza por la última llamada que se realizó. El resultado completo se muestra a continuación.

Process: com.example.debugging, PID: 14581
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.debugging/com.example.debugging.MainActivity}: java.lang.ArithmeticException: divide by zero
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3449)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
     Caused by: java.lang.ArithmeticException: divide by zero
        at com.example.debugging.MainActivity.division(MainActivity.kt:21)
        at com.example.debugging.MainActivity.onCreate(MainActivity.kt:14)
        at android.app.Activity.performCreate(Activity.java:8000)
        at android.app.Activity.performCreate(Activity.java:7984)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1309)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3422)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

Hay mucho texto. Por lo general, solo necesitas algunas partes para limitarlo al error exacto. Comencemos por la parte superior.

  1. java.lang.RuntimeException:
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.debugging/com.example.debugging.MainActivity}: java.lang.ArithmeticException: divide by zero

La primera línea indica que la app no pudo iniciar la actividad, por lo que falló. La siguiente línea proporciona un poco más de información. Específicamente, no se pudo iniciar la actividad debido a una ArithmeticException. Específicamente, el tipo de ArithmeticException fue "divide by zero".

  1. Caused by:
Caused by: java.lang.ArithmeticException: divide by zero
        at com.example.debugging.MainActivity.division(MainActivity.kt:21)

Si te desplazas hacia abajo hasta la línea "Caused by", vuelve a decir que hubo un error "divide by zero". Esta vez, también se muestra la función exacta en la que se produjo el error (division()) y el número de línea específico (21). El nombre del archivo y el número de línea en la ventana Logcat tienen un hipervínculo. El resultado también muestra el nombre de la función en la que se produjo el error, division(), y la función que la llamó, onCreate().

Esto no debería sorprendernos, ya que el error se introdujo intencionalmente. Sin embargo, si necesitas determinar la causa de un error desconocido, saber el tipo exacto de excepción, el nombre de la función y el número de línea brinda información muy útil.

¿Por qué usar un "seguimiento de pila"?

El término "seguimiento de pila" puede sonar extraño para la salida de texto de un error. Para comprender mejor cómo funciona, debes conocer un poco más sobre la pila de funciones.

Cuando una función llame a otra, el dispositivo no ejecutará ningún código desde la primera función hasta que finalice la segunda. Una vez que la segunda función termina de ejecutarse, se reanuda la primera función desde donde la dejó. Lo mismo sucede con las funciones que llama la segunda función. La segunda función no se reanudará hasta que finalice la tercera (y cualquier otra función a la que llame) y no se reanudará la primera hasta que la segunda función termine de ejecutarse. Es similar a una pila del mundo real, como una de platos o de tarjetas. Si deseas tomar un plato, vas a tomar el que se encuentra más arriba. Es imposible bajar un plato de la pila sin primero quitar todos los que estén arriba.

La pila de funciones se puede ilustrar con el siguiente código:

val TAG = ...

fun first() {
    second()
    Log.v(TAG, "1")
}

fun second() {
    third()
    Log.v(TAG, "2")
    fourth()
}

fun third() {
    Log.v(TAG, "3")
}

fun fourth() {
    Log.v(TAG, "4")
}

Si llamas a first(), los números se registrarán en el siguiente orden:

3
2
4
1

¿A qué se debe? Cuando se llama a la primera función, esta llama inmediatamente a second(), por lo que el número 1 no se puede registrar de inmediato. La pila de funciones se ve de la siguiente manera:

second()
first()

Luego, la segunda función llama a third(), que la agrega a la pila de funciones.

third()
second()
first()

Luego, la tercera función muestra el número 3. Una vez que termina de ejecutarse, se quita de la pila de funciones.

second()
first()

Luego, la función second() registra el número 2 y, luego, llama a fourth(). Hasta ahora, se registraron los números 3 y, luego, 2, y la pila de funciones es la siguiente:

fourth()
second()
first()

La función fourth() muestra el número 4 y se quita (aparecen) de la pila de funciones. La función second() termina de ejecutarse y se quita de la pila de funciones. Ahora que second() y todas las funciones a las que llamó finalizaron, el dispositivo ejecuta el código restante en first(), que muestra el número 1.

Por lo tanto, los números se registran en el siguiente orden: 4, 2, 3, 1.

Si dedicas unos minutos a revisar el código y te imaginas la pila de funciones, podrás ver exactamente qué código se ejecuta y en qué orden. Esto solo puede ser una técnica de depuración potente para errores como el ejemplo anterior de división por cero. Seguir el código también te permite saber dónde colocar las instrucciones de registro para depurar los problemas más complejos.

5. Uso de registros para identificar y corregir el error

En la sección anterior, examinaste el seguimiento de pila, específicamente esta línea.

Caused by: java.lang.ArithmeticException: divide by zero
        at com.example.debugging.MainActivity.division(MainActivity.kt:21)

Aquí vemos que la falla ocurrió en la línea 21 y se relacionaba con la división por cero. Por lo tanto, en algún lugar antes de que se ejecute este código, el denominador era 0. Si bien puedes tratar de revisar el código por tu cuenta, lo que podría funcionar bien en el caso de un ejemplo pequeño como este, también puedes imprimir el valor del denominador antes de que se produzca la división por cero y utilizar instrucciones de registro para ahorrar tiempo.

  1. Antes de la sentencia Log.v(), agrega una llamada Log.d() que registre el denominador. Log.d() se usa porque este registro es específicamente para la depuración y para que puedas filtrar los registros detallados.
Log.d(TAG, "$denominator")
  1. Vuelve a ejecutar tu app. Aunque esta todavía falle, el denominador debería registrarse varias veces. Puedes usar una configuración de filtro para mostrar solo los registros con la etiqueta "MainActivity".

d6ae5224469d3fd4.png

  1. Puedes ver que se imprimen varios valores. Parece que el bucle se ejecuta algunas veces antes de fallar la quinta iteración cuando el denominador es 0. Esto tiene sentido, porque el denominador es 4, y el bucle lo disminuye por 1 durante 5 iteraciones. Para corregir el error, puedes cambiar la cantidad de iteraciones en el bucle de 5 a 4. Si vuelves a ejecutar la app, ya no debería fallar.
fun division() {
    val numerator = 60
    var denominator = 4
    repeat(4) {
        Log.v(TAG, "${numerator / denominator}")
        denominator--
    }
}

6. Ejemplo de depuración: Accede a un valor que no existe

De forma predeterminada, la plantilla Blank Activity que usaste para crear el proyecto agrega una sola actividad, con un TextView centrado en la pantalla. Como aprendiste anteriormente, puedes hacer referencia a vistas desde código configurando un ID en el editor de diseño y accediendo a la vista con findViewById(). Cuando se llama a onCreate() en una clase de actividad, primero es necesario llamar a setContentView() para cargar un archivo de diseño (como activity_main.xml). Si intentas llamar a findViewById() antes de llamar a setContentView(), la app fallará porque la vista no existe. Intentemos acceder a la vista para ilustrar otro error.

  1. Abre activity_main.xml, selecciona Hello, World! TextView y establece el id en hello_world.

c94be640d0e03e1d.png

  1. Luego, de nuevo en ActivityMain.kt, en onCreate(), agrega código para obtener TextView y cambia tu texto a "Hello, debugging!" antes de llamar a setContentView().
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val helloTextView: TextView = findViewById(R.id.hello_world)
    helloTextView.text = "Hello, debugging!"
    setContentView(R.layout.activity_main)
    division()
}
  1. Vuelve a ejecutar la app y observa que falla de inmediato cuando se inicia. Es posible que debas quitar el filtro del ejemplo anterior para ver los registros sin la etiqueta "MainActivity". 840ddd002e92ee46.png

La excepción debe ser uno de los últimos elementos que aparecerán en Logcat (de lo contrario, puedes buscar RuntimeException). El resultado debe verse de la siguiente manera.

Process: com.example.debugging, PID: 14896
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.debugging/com.example.debugging.MainActivity}: java.lang.NullPointerException: findViewById(R.id.hello_world) must not be null
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3449)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
     Caused by: java.lang.NullPointerException: findViewById(R.id.hello_world) must not be null
        at com.example.debugging.MainActivity.onCreate(MainActivity.kt:14)
        at android.app.Activity.performCreate(Activity.java:8000)
        at android.app.Activity.performCreate(Activity.java:7984)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1309)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3422)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

Al igual que antes, en la parte superior, dice "Unable to start activity". Esto tiene sentido, ya que la app falló antes de que se iniciara MainActivity. La siguiente línea ofrece un poco más de información sobre el error.

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.debugging/com.example.debugging.MainActivity}: java.lang.NullPointerException: findViewById(R.id.hello_world) must not be null

Más abajo en el seguimiento de pila, también verás esta línea, que muestra la llamada a función exacta y el número de línea.

Caused by: java.lang.NullPointerException: findViewById(R.id.hello_world) must not be null

¿Qué significa exactamente este error y qué es exactamente un valor "nulo"? Si bien este es un ejemplo forzado y es posible que ya tengas una idea de por qué falló la app, inevitablemente verás mensajes de error que no viste antes. Cuando esto sucede, es probable que no seas el primero en ver el error. Además, incluso los desarrolladores más experimentados buscarán en Google el mensaje de error para ver cómo resolvieron el problema otros. Cuando buscas este error, obtienes varios resultados de StackOverflow, un sitio en el que los desarrolladores pueden preguntar y responder sobre el código con errores o temas de programación más generales.

Como puede haber muchas preguntas con respuestas similares, pero no exactamente las mismas, ten en cuenta las siguientes sugerencias cuando busques respuestas por tu cuenta.

  1. ¿Qué antigüedad tiene la respuesta? Es posible que las respuestas de hace varios años ya no sean relevantes, o bien que la versión que se menciona del lenguaje o el framework esté desactualizada.
  2. ¿En la respuesta se usa Java o Kotlin? ¿El problema está relacionado con un lenguaje o un framework específicos?
  3. Las respuestas marcadas como "aceptadas" o con más votos a favor pueden ser de mejor calidad, pero ten en cuenta que otras respuestas también pueden brindar información valiosa.

1636a21ff125a74c.png

El número indica la cantidad de votos a favor (o en contra) y una marca de verificación verde indica una respuesta aceptada.

Si no encuentras una pregunta, puedes publicar una nueva. Cuando hagas una pregunta en StackOverflow (o en cualquier sitio), es una buena idea tener en cuenta estos lineamientos.

Busca el error.

a60ba40e5247455e.png

Si lees algunas de las respuestas, descubrirás que el error puede tener varias causas diferentes. Sin embargo, como llamaste a findViewById() de forma deliberada antes de setContentView(), algunas de las respuestas a esta pregunta parecen ser relevantes. Por ejemplo, la segunda respuesta más votada dice lo siguiente:

"¿Es posible que estés llamando a findViewById antes de llamar a setContentView? Si es así, intenta llamar a findViewById DESPUÉS de llamar a setContentView".

Después de ver esta respuesta, puedes comprobar en el código que sí, la llamada a findViewById() es demasiado anticipada, antes de setContentView(), y que, en su lugar, debes llamarla después de setContentView().

Actualiza el código para corregir el error.

  1. Mueve la llamada a findViewById() y la línea que establece el texto de helloTextView debajo de la llamada a setContentView(). El nuevo método onCreate() debería verse como se muestra a continuación. También puedes agregar registros para verificar que se haya corregido el error.
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    Log.d(TAG, "this is where the app crashed before")
    val helloTextView: TextView = findViewById(R.id.hello_world)
    Log.d(TAG, "this should be logged if the bug is fixed")
    helloTextView.text = "Hello, debugging!"
    logging()
    division()
}
  1. Vuelve a ejecutar la app. Observa que la app ya no falla y el texto se actualiza según lo esperado.

9ff26c7deaa4a7cc.png

Toma capturas de pantalla

Es probable que ya hayas visto varias capturas de pantalla de Android Emulator en este curso. Tomar capturas de pantalla es un proceso sencillo que puede resultar útil para compartir información, como pasos para reproducir errores con otros miembros del equipo. Para tomar una captura de pantalla en Android Emulator, presiona el ícono de la cámara ubicado en la barra de herramientas de la derecha.

455336f50c5c3c7f.png

También puedes usar la combinación de teclas Command + S para tomar una captura de pantalla. La captura de pantalla se guarda automáticamente en la carpeta Escritorio.

Cómo grabar una app en ejecución

Si bien las capturas de pantalla pueden contener mucha información, a veces resulta útil compartir las grabaciones de una app en ejecución para ayudar a otras personas a reproducir algo que causó un error. Android Emulator ofrece algunas herramientas integradas para ayudarte a capturar fácilmente un GIF (imagen animada) de la app en ejecución.

  1. En las herramientas del emulador ubicadas a la derecha, haz clic en el botón Más 558dbea4f70514a8.png (última opción) para que se muestren las opciones adicionales de depuración del emulador. Aparece una ventana emergente que proporciona herramientas adicionales para simular la funcionalidad de los dispositivos físicos con fines de prueba.

46b1743301a2d12.png

  1. En el menú lateral izquierdo, haz clic en Record and Playback, y verás una pantalla con un botón para comenzar a grabar.

dd8b5019702ead03.png

  1. Por el momento, tu proyecto no tiene nada más interesante que registrar, excepto una TextView estática. Modifiquemos el código para actualizar la etiqueta cada pocos segundos y mostrar el resultado de la división. En el método division() de MainActivity, agrega una llamada a Thread.sleep(3000) antes de la llamada a Log(). El método ahora debería verse de la siguiente manera (ten en cuenta que el bucle solo debe repetirse 4 veces para evitar una falla).
fun division() {
   val numerator = 60
   var denominator = 4
   repeat(4) {
       Thread.sleep(3000)
       Log.v(TAG, "${numerator / denominator}")
       denominator--
   }
}
  1. En activity_main.xml, configura el id de TextView como division_textview.

db3c1ef675872faf.png

  1. En MainActivity.kt, reemplaza la llamada a Log.v() por las siguientes llamadas a findViewById() y setText() para establecer el texto en el cociente.
findViewById<TextView>(R.id.division_textview).setText("${numerator / denominator}")
  1. Debido a que ahora procesas el resultado de la división en la IU de la app, debes ocuparte de algunos detalles sobre cómo se ejecutan las actualizaciones de la IU. Primero, debes crear un subproceso nuevo que pueda ejecutar el bucle repeat. De lo contrario, Thread.sleep(3000) bloquearía el subproceso principal y la vista de la app no se renderizaría hasta que finalice onCreate() (incluido division() con su bucle repeat).
fun division() {
   val numerator = 60
   var denominator = 4

   thread(start = true) {
      repeat(4) {
         Thread.sleep(3000)
         findViewById<TextView>(R.id.division_textview).setText("${numerator / denominator}")
         denominator--
      }
   }
}
  1. Si intentas ejecutar la app ahora, verás una FATAL EXCEPTION. El motivo de esta excepción es que solo los subprocesos que crearon una vista tienen permitido cambiarla. Afortunadamente, puedes hacer referencia al subproceso de IU con runOnUiThread(). Cambia division() para actualizar TextView dentro del subproceso de IU.
private fun division() {
   val numerator = 60
   var denominator = 4
   thread(start = true) {
      repeat(4) {
         Thread.sleep(3000)
         runOnUiThread {
            findViewById<TextView>(R.id.division_textview).setText("${numerator / denominator}")
            denominator--
         }
      }
   }
}
  1. Ejecuta tu app y, luego, cambia de inmediato al emulador. Cuando se inicie la app, haz clic en el botón Start Recording de la ventana Extended Controls. Deberías ver que el cociente se actualiza cada tres segundos. Después de que el cociente se haya actualizado varias veces, haz clic en Stop Recording.

55121bab5b5afaa6.png

  1. De forma predeterminada, el resultado se guarda en formato .webm. Usa el menú desplegable para exportar el resultado como un archivo GIF.

850713aa27145908.png

7. Felicitaciones

¡Felicitaciones! En esta ruta de aprendizaje, aprendiste lo siguiente:

  • La depuración es el proceso que se utiliza para solucionar los errores de código.
  • Los registros te permiten mostrar texto con diferentes niveles y etiquetas de registro.
  • El seguimiento de pila proporciona información sobre una excepción, como la función exacta que la causó y el número de línea en el que ocurrió.
  • Cuando realices una depuración, es probable que alguien haya tenido el mismo problema o uno similar. Puedes usar sitios como StackOverflow para investigar el error.
  • Puedes exportar fácilmente capturas de pantalla y GIF animados con Android Emulator.

Más información