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:
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.
- En la pantalla New Project, selecciona Empty Activity.
- Asígnale a la app el nombre Debugging. Asegúrate de que el lenguaje esté configurado en Kotlin y no modifiques nada más.
Una vez que hayas creado el proyecto, aparecerá un nuevo proyecto de Android Studio que mostrará un archivo llamado MainActivity.kt
.
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.
- En
MainActivity.kt
, antes de la declaración de clase, agrega una constante llamadaTAG
y establece su valor como el nombre de la clase,MainActivity
.
private const val TAG = "MainActivity"
- Agrega una función nueva a la clase
MainActivity
llamadalogging()
, como se muestra.
fun logging() {
Log.v(TAG, "Hello, world!")
}
- Llama a
logging()
enonCreate()
. El nuevo métodoonCreate()
debería verse de la siguiente manera.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
logging()
}
- 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.
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.
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. |
|
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. |
|
INFO | Los registros de INFO proporcionan información útil, como que una operación se completó con éxito. |
|
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. |
|
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. |
|
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.
- En
MainActivity.kt
, reemplaza el contenido del métodologging()
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")
}
- 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.
- Luego, escribe "MainActivity" en Log Tag y crea un nombre para el filtro, como se muestra.
- Ahora, solo deberías ver mensajes de registro con la etiqueta "MainActivity".
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.
- 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 nivelWARN
yERROR
.
- 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.
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.
- Agrega la siguiente función a tu
MainActivity.kt
sobre la funciónlogging()
. Este código comienza con dos números y usarepeat
para registrar el resultado de dividir el numerador por el denominador cinco veces. Cada vez que se ejecuta el código en el bloquerepeat
, 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--
}
}
- Después de la llamada a
logging()
enonCreate()
, agrega una llamada a la funcióndivision()
.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
logging()
division()
}
- 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ónlogging()
que definiste antes, los registros detallados de la funcióndivision()
y, luego, un registro de error rojo que explica por qué falló la app.
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.
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".
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.
- Antes de la sentencia
Log.v()
, agrega una llamadaLog.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")
- 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"
.
- 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 es4
, y el bucle lo disminuye por1
durante5
iteraciones. Para corregir el error, puedes cambiar la cantidad de iteraciones en el bucle de5
a4
. 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.
- Abre
activity_main.xml
, selecciona Hello, World!TextView
y establece elid
enhello_world
.
- Luego, de nuevo en
ActivityMain.kt
, enonCreate()
, agrega código para obtenerTextView
y cambia tu texto a "Hello, debugging!" antes de llamar asetContentView()
.
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()
}
- 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"
.
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.
- ¿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.
- ¿En la respuesta se usa Java o Kotlin? ¿El problema está relacionado con un lenguaje o un framework específicos?
- 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.
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.
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.
- Mueve la llamada a
findViewById()
y la línea que establece el texto dehelloTextView
debajo de la llamada asetContentView()
. El nuevo métodoonCreate()
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()
}
- Vuelve a ejecutar la app. Observa que la app ya no falla y el texto se actualiza según lo esperado.
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.
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.
- En las herramientas del emulador ubicadas a la derecha, haz clic en el botón Más (ú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.
- En el menú lateral izquierdo, haz clic en Record and Playback, y verás una pantalla con un botón para comenzar a grabar.
- 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étododivision()
deMainActivity
, agrega una llamada aThread.sleep(3000)
antes de la llamada aLog()
. 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--
}
}
- En
activity_main.xml
, configura elid
deTextView
comodivision_textview
.
- En
MainActivity.kt
, reemplaza la llamada aLog.v()
por las siguientes llamadas afindViewById()
ysetText()
para establecer el texto en el cociente.
findViewById<TextView>(R.id.division_textview).setText("${numerator / denominator}")
- 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 finaliceonCreate()
(incluidodivision()
con su buclerepeat
).
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--
}
}
}
- 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 conrunOnUiThread()
. Cambiadivision()
para actualizarTextView
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--
}
}
}
}
- 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.
- De forma predeterminada, el resultado se guarda en formato
.webm
. Usa el menú desplegable para exportar el resultado como un archivo GIF.
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.