Introdução à depuração

1. Antes de começar

Qualquer pessoa que usou um software provavelmente já encontrou um bug. Um bug é um erro em um software que causa um comportamento não intencional, como uma falha no app ou um recurso que não funciona como esperado. Mesmo os desenvolvedores mais experientes geram bugs ao escrever códigos. Uma das habilidades mais importantes para um desenvolvedor Android é identificar e corrigir esses problemas. Não é incomum ver versões inteiras de apps dedicadas a corrigir bugs. Por exemplo, veja os detalhes da versão do Google Maps abaixo:

9d5ec1958683e173.png

O processo de correção de bugs é chamado de depuração. O famoso cientista da computação Brian Kernighan disse certa vez que "a ferramenta de depuração mais eficaz ainda é uma reflexão cuidadosa unida a instruções de exibição bem colocadas". Talvez você já tenha visto a instrução println() do Kotlin em codelabs anteriores, mas os desenvolvedores Android profissionais usam a geração de registros para organizar melhor as saídas dos programas deles. Neste codelab, você vai aprender a usar a geração de registros no Android Studio e como ela pode ser uma ferramenta de depuração. Você também vai aprender a ler registros de mensagens de erro, chamados de stack traces, para identificar e resolver bugs. Por fim, você vai aprender a pesquisar bugs por conta própria e capturar a saída do Android Emulator, como uma captura de tela ou um GIF do seu app em execução.

Pré-requisitos

  • Saber navegar em um projeto no Android Studio.

O que você vai aprender

Ao final deste codelab, você vai poder:

  • gravar registros usando android.util.Logger;
  • saber quando usar diferentes níveis de registro;
  • usar os registros como uma ferramenta de depuração simples e avançada;
  • encontrar informações significativas em um stack trace;
  • procurar mensagens de erro para resolver falhas no aplicativo;
  • criar capturas de tela e GIFs animados do Android Emulator.

Pré-requisitos

  • Um computador com o Android Studio instalado.

2. Criar um novo projeto

Em vez de usar um app grande e complexo, vamos começar com um projeto em branco para demonstrar os log statements e o uso deles para depuração.

Comece criando um novo projeto do Android Studio, como mostrado.

  1. Na tela New Project, selecione Empty Activity.

72a0bbf2012bcb7d.png

  1. Nomeie o app como Debugging. Confira se a linguagem está definida como Kotlin e não mude mais nada.

60a1619c07fae8f5.png

Após a criação, você verá um novo projeto do Android Studio, mostrando um arquivo chamado MainActivity.kt.

e3ab4a557c50b9b0.png

3. Geração de registros e saída de depuração

Nas lições anteriores, você usou a instrução println() do Kotlin para produzir saída de texto. Em um app Android, a prática recomendada para gerar registros de saída é usar a classe Log. Há várias funções para gerar registros de saída, no formato Log.v(), Log.d(), Log.i(), Log.w() ou Log.e(). Esses métodos usam dois parâmetros: o primeiro, chamado de "tag", é uma string que identifica a origem da mensagem de registro (como o nome da classe que registrou o texto). A segunda é a mensagem de registro em si.

Siga estas etapas para começar a usar a geração de registros no seu projeto em branco.

  1. No arquivo MainActivity.kt, antes da declaração de classe, adicione uma constante chamada TAG e defina o valor dela como o nome da classe, MainActivity.
private const val TAG = "MainActivity"
  1. Adicione uma nova função à classe MainActivity, chamada logging(), conforme mostrado.
fun logging() {
    Log.v(TAG, "Hello, world!")
}
  1. Chame a função logging() no onCreate(). O novo método onCreate() vai ficar como mostrado a seguir.
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    logging()
}
  1. Execute o app para ver os registros em ação. Eles aparecem na janela do Logcat, na parte de baixo da tela. Como o Logcat vai mostrar a saída de outros processos no dispositivo ou emulador, você pode selecionar seu app (com.example.debugging) no menu suspenso para filtrar os registros irrelevantes para o app.

199c65d11ee52b5c.png

Na janela de saída, a mensagem "Hello, world!" vai ser mostrada. Se necessário, digite "hello" na caixa de pesquisa, na parte de cima da janela do Logcat, para pesquisar em todos os registros.

92f258013bc15d12.png

Níveis de registro

Há diferentes funções de registro, nomeadas com letras diferentes, porque elas correspondem a níveis de registro distintos. Dependendo do tipo de informação que você quer gerar, use um nível de registro diferente para filtrá-la na saída de Logcat. Existem cinco níveis principais de registro que você usará com frequência.

Nível de registro

Caso de uso

Exemplo

ERROR

Registros de ERROR (erro) relatam que ocorreu um erro grave, como o motivo de um app ter falhado.

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

WARN

Os registros de WARN (aviso) são menos graves que um erro, mas ainda relatam algo que precisa ser corrigido para evitar um erro mais grave. Um exemplo pode ser se você chamar uma função descontinuada, o que significa que é recomendável usar uma alternativa mais recente.

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

INFO

Os registros de INFO (informações) fornecem informações úteis, como uma operação ter sido concluída.

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

DEBUG

Os registros de DEBUG (depuração) contêm informações que podem ser úteis ao investigar um problema. Não há registros desse tipo em builds de lançamento, como um que seria publicado na 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 indica o nome, VERBOSE (detalhado) é o nível de registro menos específico. O que é considerado um registro de depuração, em vez de um registro detalhado, é subjetivo. Geralmente, um registro detalhado é algo que pode ser removido depois que um recurso é implementado, enquanto um registro de depuração ainda pode ser útil para depuração. Esses registros também não são incluídos em builds de lançamento.

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

Lembre-se de que não há regras definidas para quando usar cada tipo de nível de registro, especialmente para quando usar DEBUG e VERBOSE. As equipes de desenvolvimento de software podem criar as próprias diretrizes sobre quando usar cada nível de registro ou decidir não usar determinados níveis, como VERBOSE. O mais importante a se lembrar sobre esses dois níveis é que eles não são incluídos nos builds de lançamento. Portanto, o uso de registros para depuração não vai afetar o desempenho dos apps publicados, enquanto as instruções println() permanecem nos builds de lançamento e têm impacto negativo no desempenho.

Veja como são esses diferentes níveis de registro no Logcat.

  1. Na MainActivity.kt, substitua o conteúdo do método logging() pelo seguinte.
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. Execute seu app e observe a saída no Logcat. Se necessário, filtre a saída para mostrar apenas os registros do processo com.example.debugging. Também é possível filtrar a saída para mostrar apenas registros com a tag "MainActivity". Para fazer isso, selecione Edit Filter Configuration no menu suspenso no canto superior direito da janela do Logcat.

383ec6d746bb72b1.png

  1. Em seguida, digite "MainActivity" em Log Tag e crie um nome para o filtro, conforme mostrado.

e7ccfbb26795b3fc.png

  1. Agora você vai receber apenas mensagens de registro com a tag "MainActivity".

4061ca006b1d278c.png

Observe como há uma letra, que corresponde ao nível de registro, antes do nome da classe (por exemplo, W/MainActivity). Além disso, o registro WARN é mostrado em azul, enquanto o registro ERROR aparece em vermelho, assim como o erro fatal no exemplo anterior.

  1. Assim como você pode filtrar a saída de depuração por processo, também é possível filtrar por nível de registro. Por padrão, essa opção está definida como Verbose, que vai mostrar os registros VERBOSE e de níveis mais altos. Selecione Warn no menu suspenso e observe que agora apenas os registros de nível WARN e ERROR são mostrados.

c4aa479a8dd9d4ca.png

  1. Mude o menu suspenso para Assert e observe que nenhum registro é exibido. Isso filtra tudo que está no nível ERROR e abaixo.

ee3be7cfaa0d8bd1.png

Embora isso possa parecer uma preocupação um pouco excessiva com as instruções println(), conforme você criar apps maiores, a saída do Logcat vai ficar muito maior. O uso de diferentes níveis de registro vai permitir a seleção das informações mais úteis e relevantes. O uso de registros é uma prática recomendada e tem preferência em relação à println() no desenvolvimento para Android, porque os registros detalhados e de depuração não afetam a performance nos builds de lançamento. Também é possível filtrar registros com base nos diferentes níveis. Escolher o nível de registro correto vai beneficiar outros membros da equipe de desenvolvimento que não estão familiarizados com o código e vai facilitar muito a identificação e a solução de bugs.

4. Registros com mensagens de erro

Introduzir um bug

Não há muita depuração para fazer em um projeto em branco. Muitos dos bugs que você vai encontrar como desenvolvedor Android envolvem falhas nos apps, o que, obviamente, não é uma boa experiência para o usuário. Vamos adicionar um código que cause falhas no app.

Você pode se lembrar de ter aprendido na aula de matemática que não é possível dividir um número por zero. Vejamos o que acontece quando tentamos dividir por zero no código.

  1. Adicione a seguinte função ao MainActivity.kt acima da função logging(). Esse código começa com dois números e usa repeat para registrar o resultado da divisão do numerador pelo denominador cinco vezes. Toda vez que o código no bloco repeat é executado, o valor do denominador é reduzido em um. Na quinta e última iteração, o app tenta dividir por zero.
fun division() {
    val numerator = 60
    var denominator = 4
    repeat(5) {
        Log.v(TAG, "${numerator / denominator}")
        denominator--
    }
}
  1. Após a chamada para logging() no onCreate(), adicione uma chamada para a função division().
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    logging()
    division()
}
  1. Execute o app novamente e observe ele falhar. Ao rolar para baixo até os registros da classe MainActivity.kt, você verá os da função logging() definida anteriormente, os registros detalhados da função division() e, em seguida, um registro de erro em vermelho explicando por que o app falhou.

12d87f287661a66.png

Anatomia de um stack trace

O registro de erro que descreve a falha, também conhecida como exceção, é chamado de stack trace. O stack trace mostra todas as funções que foram chamadas que levam à exceção, começando pela chamada mais recente. A saída completa é mostrada abaixo.

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)

É uma grande quantidade de texto. Por sorte, você normalmente só precisa de algumas partes para restringir ao erro exato. Vamos começar do início.

  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

A primeira linha indica que o app não conseguiu iniciar a atividade, e esse é o motivo da falha. A próxima linha traz um pouco mais de informações. Especificamente, não foi possível iniciar a atividade devido a uma ArithmeticException. Mais especificamente, o tipo de ArithmeticException era "divide by zero" (dividir por zero).

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

Ao rolar para baixo até a linha "Caused by" (Causado por), novamente você verá um erro "divide by zero". Desta vez, ele também mostra a função exata onde o erro ocorreu (division()) e o número da linha (21). O nome do arquivo e o número da linha na janela do Logcat estão com hiperlinks. A saída também mostra o nome da função em que ocorreu o erro, division(), e a função que a chamou, onCreate().

Nenhuma dessas situações é uma surpresa, já que o bug foi introduzido intencionalmente. No entanto, se você precisar determinar a causa de um erro desconhecido, saber o tipo exato de exceção, o nome da função e o número da linha vai fornecer informações muito úteis.

Por que um "stack trace"?

"Stack trace" pode parecer um termo estranho para a saída de texto de um erro. Para entender melhor como isso funciona, é preciso saber um pouco mais sobre a pilha de funções.

Quando uma função chama outra, o dispositivo não executa nenhum código da primeira função até a segunda ser concluída. Quando a segunda função terminar de ser executada, a primeira será retomada de onde parou. O mesmo vale para qualquer função chamada pela segunda. A segunda função não será retomada até que a terceira (e qualquer outra que ela chame) termine, e a primeira função não será retomada até a segunda terminar de ser executada. Isso é semelhante a uma pilha no mundo físico, como uma pilha de pratos ou cartas. Se você quiser pegar um prato, vai pegar o primeiro de todos. É impossível colocar um prato na parte de baixo da pilha sem remover todos os outros acima dele.

A pilha de funções pode ser ilustrada com o código a seguir.

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

Se você chamar first(), os números serão registrados na seguinte ordem.

3
2
4
1

Qual é o motivo disso? Quando a primeira função é chamada, ela chama a second() no mesmo instante e, portanto, o número 1 não pode ser registrado imediatamente. A pilha de funções tem a seguinte aparência.

second()
first()

Em seguida, a segunda função chama a third(), que a adiciona à pilha de funções.

third()
second()
first()

A terceira função exibe o número 3. Quando a execução é concluída, ela é removida da pilha de funções.

second()
first()

A função second() registra o número 2 e, em seguida, chama a fourth(). Até agora, os números 3 e depois 2 foram registrados, e a pilha de funções ficou da seguinte maneira.

fourth()
second()
first()

A função fourth() exibe o número 4 e é removida (retirada) da pilha de funções. A função second() termina a execução e é retirada da pilha de funções. Agora que second() e todas as funções que ela chamou foram concluídas, o dispositivo executa o código restante na first(), que exibe o número 1.

Assim, os números são registrados na ordem: 4, 2, 3, 1.

Ao analisar o código e manter uma imagem mental da pilha de funções, você pode ver exatamente qual código é executado e em que ordem. Essa pode ser uma técnica de depuração eficiente para bugs, como o exemplo acima de divisão por zero. Percorrer o código também pode dar uma boa ideia de onde colocar log statements para ajudar a depurar problemas mais complexos.

5. Como usar registros para identificar e corrigir o bug

Na seção anterior, você analisou o stack trace, especificamente esta linha.

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

Aqui, é possível ver que a falha ocorreu na linha 21 e estava relacionada à divisão por zero. Assim, em algum lugar antes da execução desse código, o denominador era 0. Você pode tentar percorrer o código por conta própria, o que funcionaria perfeitamente para um exemplo pequeno como este. No entanto, você também pode usar log statements para economizar tempo, exibindo o valor do denominador antes que a divisão por zero ocorra.

  1. Antes da instrução Log.v(), adicione uma chamada Log.d() que registre o denominador. A Log.d() é usada porque esse registro é especificamente para depuração. Assim, você pode filtrar os registros detalhados.
Log.d(TAG, "$denominator")
  1. Execute o app novamente. Embora ele ainda falhe, o denominador será registrado várias vezes. Você pode usar uma configuração de filtro para mostrar apenas registros com a tag "MainActivity".

d6ae5224469d3fd4.png

  1. Observe que diversos valores são mostrados. Aparentemente, a repetição é executada algumas vezes antes de falhar na quinta iteração, quando o denominador é 0. Isso faz sentido, porque o denominador é 4 e a repetição o diminui em 1 por 5 iterações. Para corrigir o bug, você pode mudar o número de iterações na repetição de 5 para 4. Se você executar o app novamente, ele não falhará mais.
fun division() {
    val numerator = 60
    var denominator = 4
    repeat(4) {
        Log.v(TAG, "${numerator / denominator}")
        denominator--
    }
}

6. Exemplo de depuração: acessar um valor que não existe

Por padrão, o modelo Blank Activity usado para criar o projeto adiciona uma única atividade, com uma TextView centralizada na tela. Como você aprendeu anteriormente, é possível referenciar visualizações pelo código, definindo um ID no editor de layout e acessando a visualização com findViewById(). Quando onCreate() é chamado em uma classe de atividade, primeiro é necessário chamar setContentView() para carregar um arquivo de layout, como activity_main.xml. Se você tentar chamar findViewById() antes de setContentView(), o app vai falhar, porque a visualização não existe. Vamos tentar acessar a visualização para ajudar a ilustrar outro bug.

  1. Abra o activity_main.xml, selecione a TextView Hello, world! e defina o id como hello_world.

c94be640d0e03e1d.png

  1. Volte para o ActivityMain.kt no onCreate(), adicione o código para receber a TextView e mude o texto dela para Hello, debugging! antes da chamada para 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. Execute o app de novo e observe que ele falha imediatamente após a inicialização. Talvez seja necessário remover o filtro do exemplo anterior para ver os registros sem a tag "MainActivity". 840ddd002e92ee46.png

A exceção será uma das últimas coisas que aparecem no Logcat. Se não for, você pode pesquisar por RuntimeException. A saída será semelhante à seguinte.

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)

Como antes, a mensagem "Unable to start activity" (Não foi possível iniciar a atividade) aparece na parte de cima da tela. Isso faz sentido porque o app falhou antes do lançamento da MainActivity. A linha a seguir mostra um pouco mais sobre o erro.

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

Mais abaixo no stack trace, você também vai ver essa linha mostrando a chamada de função e o número da linha exatos.

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

O que exatamente significa esse erro e o que é exatamente um valor "nulo"? Embora esse seja um exemplo fictício e você já tenha uma ideia do motivo da falha do app, inevitavelmente encontrará mensagens de erro que não viu antes. Quando isso acontece, é provável que você não seja o primeiro a encontrar o erro. Até mesmo os desenvolvedores mais experientes costumam pesquisar mensagens de erro no Google para saber como os outros resolveram o problema. Ao buscar esse erro, você vai encontrar vários resultados no StackOverflow, um site em que os desenvolvedores podem fazer perguntas e dar respostas sobre códigos com bugs ou temas de programação mais gerais.

Como pode haver muitas perguntas com respostas semelhantes, mas não exatamente iguais, lembre-se das dicas a seguir ao pesquisar respostas.

  1. Quantos anos tem a resposta? Respostas de vários anos atrás podem não ser mais relevantes ou podem usar uma versão desatualizada de uma linguagem ou de um framework.
  2. A resposta usa Java ou Kotlin? Seu problema é específico de uma linguagem ou de outra, ou está relacionado a um framework específico?
  3. Respostas marcadas como "accepted" (aceitas) ou com mais votos de apoio podem ser de maior qualidade, mas outras respostas também podem fornecer informações valiosas.

1636a21ff125a74c.png

Um número indica a quantidade de apoios (ou reprovações), e uma marca de verificação verde indica uma resposta aceita.

Se não encontrar uma pergunta, faça uma nova. Ao fazer uma pergunta no StackOverflow (ou em qualquer site), é importante considerar estas orientações (link em inglês).

Depois, pesquise o erro.

a60ba40e5247455e.png

Se você ler algumas das respostas, vai descobrir que o erro pode ter várias causas diferentes. No entanto, considerando que você chamou deliberadamente findViewById() antes de setContentView(), algumas das respostas a esta pergunta (link em inglês) parecem promissoras. A segunda resposta mais votada, por exemplo, diz o seguinte:

"É possível que você esteja chamando findViewById antes de chamar setContentView? Se for o caso, tente chamar findViewById DEPOIS de chamar setContentView"

Depois de ver essa resposta, você pode verificar no código que a chamada para findViewById() realmente aparece muito cedo, vindo antes de setContentView(). Em vez disso, a chamada precisa ser feita depois de setContentView().

Para corrigir o erro, atualize o código.

  1. Mova a chamada para findViewById() e a linha que define o texto de helloTextView abaixo da chamada para setContentView(). O novo método onCreate() será semelhante ao mostrado abaixo. Você também pode adicionar registros, conforme mostrado, para verificar se o bug foi corrigido.
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. Execute o app novamente. Observe que o app não falha mais e o texto é atualizado como esperado.

9ff26c7deaa4a7cc.png

Fazer capturas de tela

Você provavelmente já viu várias capturas de tela do Android Emulator neste curso. Fazer capturas de tela é relativamente simples, e elas podem ser úteis para compartilhar informações com outros membros da equipe, como etapas de reprodução de bugs. Você pode fazer uma captura de tela no Android Emulator pressionando o ícone de câmera na barra de ferramentas à direita.

455336f50c5c3c7f.png

Também é possível usar o atalho de teclado Command + S para fazer uma captura de tela. A imagem é salva automaticamente na pasta "Área de trabalho".

Gravar um app em execução

Embora as capturas de tela possam transmitir muitas informações, às vezes é útil compartilhar gravações de um app em execução para ajudar as pessoas a reproduzir algo que causou um bug. O Android Emulator oferece algumas ferramentas integradas para ajudar você a capturar facilmente um GIF (imagem animada) do app em execução.

  1. À direita, nas ferramentas do emulador, clique no botão Mais 558dbea4f70514a8.png (último item) para mostrar as opções extras de depuração do emulador. Será aberta uma janela com mais ferramentas para simular a funcionalidade de dispositivos físicos para fins de teste.

46b1743301a2d12.png

  1. No menu lateral à esquerda, clique em Record and Playback. Você verá uma tela com um botão para iniciar a gravação.

dd8b5019702ead03.png

  1. No momento, o projeto não tem nada interessante para registrar, além de uma TextView estática. Vamos modificar o código para atualizar a etiqueta a cada poucos segundos para mostrar o resultado da divisão. No método division() da MainActivity, adicione uma chamada de Thread.sleep(3000) antes de Log(). O método vai ficar parecido com o mostrado a seguir. Observe que a repetição acontece apenas quatro vezes para evitar uma falha.
fun division() {
   val numerator = 60
   var denominator = 4
   repeat(4) {
       Thread.sleep(3000)
       Log.v(TAG, "${numerator / denominator}")
       denominator--
   }
}
  1. No activity_main.xml, defina o id da TextView como division_textview.

db3c1ef675872faf.png

  1. Em MainActivity.kt, substitua a chamada para Log.v() pelas seguintes chamadas para findViewById() e setText() para definir o texto do quociente.
findViewById<TextView>(R.id.division_textview).setText("${numerator / denominator}")
  1. Como o resultado da divisão é renderizado na interface do app, você precisa considerar alguns detalhes sobre como as atualizações da interface são executadas. Primeiro, você precisa criar uma nova linha de execução que possa gerar a repetição repeat. Caso contrário, o Thread.sleep(3000) bloquearia a linha de execução principal e a visualização do app não seria renderizada até que onCreate() fosse concluído (incluindo division() com a repetição 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. Caso tente executar o app agora, você vai encontrar uma FATAL EXCEPTION. Essa exceção é gerada porque apenas as linhas de execução que criaram uma visualização podem modificá-la. Felizmente, é possível referenciar a linha de execução de interface usando runOnUiThread(). Mude division() para atualizar a TextView na linha de execução de interface.
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. Execute seu app e depois alterne imediatamente para o emulador. Quando o app for iniciado, clique no botão Start Recording na janela "Extended Controls". Você verá o quociente ser atualizado a cada três segundos. Depois que o quociente for atualizado algumas vezes, clique em Stop Recording.

55121bab5b5afaa6.png

  1. Por padrão, a saída é salva no formato .webm. Use o menu suspenso para exportar a saída como um arquivo GIF.

850713aa27145908.png

7. Parabéns

Parabéns! Neste Programa de treinamentos, você aprendeu que:

  • a depuração é o processo de solucionar os bugs no seu código;
  • o registro permite exibir o texto com diferentes níveis de registro e tags;
  • o stack trace fornece informações sobre uma exceção, como a função exata que a causou e o número da linha em que a exceção ocorreu;
  • ao depurar, é provável que alguém tenha encontrado o mesmo problema que você ou um parecido, e você pode usar sites como o StackOverflow para pesquisar o bug;
  • é fácil exportar capturas de tela e GIFs animados usando o Android Emulator.

Saiba mais