Introduction au débogage

1. Avant de commencer

Toute personne qui utilise des logiciels a au moins une fois dans sa vie rencontré un bug. Un bug est une erreur de logiciel qui entraîne un comportement inattendu, tel qu'un plantage de l'application ou une fonctionnalité dont le comportement est inadéquat. Quel que soit leur niveau d'expérience, tous les développeurs introduisent des bugs lorsqu'ils écrivent du code. L'une des compétences les plus importantes d'un développeur Android consiste à savoir identifier ces bugs et les corriger. Il n'est pas rare que la sortie d'une nouvelle version d'application soit dédiée à la correction de ses bugs. Par exemple, consultez les détails de la version de Google Maps ci-dessous :

9d5ec1958683e173.png

Le processus de correction des bugs est appelé débogage. Le célèbre informaticien Brian Kernighan a un jour déclaré que "l'outil de débogage le plus efficace reste une réflexion approfondie, associée à des instructions d'impression judicieusement placées". Même si vous connaissez peut-être l'instruction println() de Kotlin utilisée lors d'ateliers de programmation précédents, les développeurs Android professionnels utilisent la journalisation pour mieux organiser la sortie de leur programme. Dans cet atelier de programmation, vous apprendrez à utiliser la journalisation dans Android Studio et à l'utiliser comme outil de débogage. Vous découvrirez également comment lire les journaux des messages d'erreur, appelés traces de pile, pour identifier et corriger les bugs. Enfin, vous verrez comment rechercher des bugs par vous-même et comment capturer la sortie de l'émulateur Android sous forme de capture d'écran ou de GIF de votre application en cours d'exécution.

Conditions préalables

  • Vous savez comment naviguer dans un projet dans Android Studio.

Points abordés

À la fin de cette formation, vous serez en mesure d'effectuer les opérations suivantes :

  • Écrire des journaux avec android.util.Logger
  • Savoir quand utiliser différents niveaux de journalisation
  • Utiliser les journaux comme un outil de débogage simple et performant
  • Trouver des informations pertinentes dans une trace de la pile
  • Rechercher les messages d'erreur pour résoudre les plantages de l'application
  • Effectuer des captures d'écran et des GIF animés à partir d'Android Emulator

Ce dont vous avez besoin

  • Un ordinateur sur lequel est installé Android Studio

2. Créer un projet

Plutôt que d'utiliser une application volumineuse et complexe, nous allons commencer par un projet vierge pour présenter les instructions de journalisation et leur utilisation pour le débogage.

Commencez par créer un projet Android Studio, comme indiqué ci-dessous.

  1. Sur l'écran New project (Nouveau projet), sélectionnez Empty Activity (Activité vide).

72a0bbf2012bcb7d.png

  1. Nommez l'application Debugging (Débogage). Assurez-vous que le langage est défini sur Kotlin. Ne modifiez pas les autres champs.

60a1619c07fae8f5.png

Une fois le projet créé, un nouveau projet Android Studio s'affiche, avec le fichier MainActivity.kt.

e3ab4a557c50b9b0.png

3. Journalisation et débogage de la sortie

Dans les leçons précédentes, vous avez utilisé l'instruction println() de Kotlin pour générer une sortie textuelle. Dans une application Android, la bonne pratique consiste à utiliser la classe Log. Il existe plusieurs fonctions de journalisation, qui prennent la forme Log.v(), Log.d(), Log.i(), Log.w() ou Log.e(). Ces méthodes utilisent deux paramètres : le premier, appelé "tag", est une chaîne identifiant la source du message du journal (comme le nom de la classe qui a enregistré le texte). Le second est le message de journal réel.

Pour commencer à utiliser la journalisation dans le projet vide, procédez comme suit :

  1. Dans MainActivity.kt, avant la déclaration de classe, ajoutez une constante appelée TAG et définissez sa valeur sur le nom de la classe, MainActivity.
private const val TAG = "MainActivity"
  1. Ajoutez une fonction à la classe MainActivity appelée logging(), comme indiqué ci-dessous.
fun logging() {
    Log.v(TAG, "Hello, world!")
}
  1. Appelez logging() dans onCreate(). La nouvelle méthode onCreate() devrait se présenter comme suit.
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    logging()
}
  1. Exécutez l'application pour voir les journaux en action. Les journaux apparaissent dans la fenêtre "Logcat" en bas de l'écran. Étant donné que Logcat affiche la sortie d'autres processus sur l'appareil (ou dans l'émulateur), vous pouvez sélectionner votre application (com.example.debugging) dans le menu déroulant pour filtrer les journaux qui ne sont pas pertinents dans votre cas de figure.

199c65d11ee52b5c.png

Dans la fenêtre de sortie, le message "Hello World!" doit s'afficher. Si nécessaire, saisissez "hello" dans le champ de recherche situé en haut de la fenêtre Logcat pour lancer une recherche dans tous les journaux.

92f258013bc15d12.png

Niveaux de journalisation

Il existe différentes fonctions de journal, nommées avec des lettres distinctes, car elles correspondent à différents niveaux de journalisation. Selon le type d'informations que vous souhaitez afficher, vous pouvez utiliser un autre niveau de journalisation pour les filtrer dans la sortie Logcat. Vous utiliserez régulièrement cinq niveaux de journalisation principaux.

Niveau de journalisation

Cas d'utilisation

Exemple

ERROR

Les journaux d'erreurs signalent un problème sérieux, tel que la raison pour laquelle une application a planté.

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

WARN

Les journaux d'avertissement sont moins graves qu'une erreur, mais signalent quand même un problème qui doit être corrigé pour éviter une erreur plus grave. Par exemple, un avertissement peut être généré si vous appelez une fonction obsolète et que son utilisation est déconseillée en faveur d'une alternative plus récente.

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

INFO

Les journaux d'information fournissent des détails utiles, telles qu'une confirmation que l'opération a abouti.

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

DEBUG

Les journaux de débogage contiennent des informations qui peuvent être utiles pour examiner un problème. Ces journaux ne sont pas présents dans les builds comme ceux que vous publiez sur le 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

Comme son nom l'indique, ce niveau de journalisation est le plus détaillé. Ce qui fait la différence entre un journal de débogage et un journal détaillé est quelque peu subjectif. De manière générale, il est possible de supprimer un journal détaillé après qu'une fonctionnalité a été implémentée, tandis qu'il est préférable de conserver un journal de débogage pour corriger les bugs éventuels. Ces journaux ne sont pas non plus inclus dans les builds.

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

N'oubliez pas que vous pouvez utiliser chaque type de niveau de journalisation à votre gré, en particulier DEBUG et VERBOSE. Les équipes de développement de logiciels peuvent créer leurs propres consignes concernant l'utilisation de chaque niveau de journalisation ou décider de ne pas en utiliser certains, comme VERBOSE. N'oubliez pas que ces deux niveaux de journalisation ne sont pas présents dans les builds. Dès lors, l'utilisation de journaux de débogage n'aura pas d'incidence sur les performances des applications publiées, tandis que les instructions println() restent dans les builds et ont un impact négatif sur les performances.

Voyons à quoi ressemblent ces différents niveaux de journalisation dans Logcat.

  1. Dans MainActivity.kt, remplacez le contenu de la méthode logging() par le code suivant.
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. Exécutez votre application et observez la sortie dans Logcat. Si nécessaire, filtrez la sortie pour n'afficher que les journaux du processus com.example.debugging. Vous pouvez également filtrer la sortie pour n'afficher que les journaux associés à la balise "MainActivity". Pour ce faire, sélectionnez Edit Filter Configuration (Modifier la configuration de filtre) dans le menu déroulant en haut à droite de la fenêtre Logcat.

383ec6d746bb72b1.png

  1. Saisissez ensuite "MainActivity" dans le champ Log Tag (Balise de journal) et donnez un nom à votre filtre, comme indiqué.

e7ccfbb26795b3fc.png

  1. Vous ne devriez maintenant voir que les messages de journal contenant la balise "MainActivity".

4061ca006b1d278c.png

Notez la présence d'une lettre devant le nom de la classe (par exemple W/MainActivity), Celle-ci correspond au niveau de journalisation. De plus, le journal WARN est affiché en bleu, tandis que le journal ERROR est affiché en rouge, comme l'erreur fatale de l'exemple précédent.

  1. Tout comme vous pouvez filtrer les résultats de débogage par processus, vous pouvez également filtrer la sortie par niveau de journalisation. Par défaut, ce paramètre est défini sur Verbose, qui affiche les journaux VERBOSE et les niveaux de journalisation supérieurs. Sélectionnez Warn dans le menu déroulant. Notez que seuls les journaux de niveau WARN et ERROR sont affichés.

c4aa479a8dd9d4ca.png

  1. Encore une fois, sélectionnez Assert dans la liste déroulante et notez qu'aucun journal ne s'affiche. Cela permet d'exclure tout contenu de niveau ERROR ou inférieur.

ee3be7cfaa0d8bd1.png

Bien qu'il puisse sembler que nous prenons println() un peu trop au sérieux, sachez que plus vous créerez d'applications volumineuses, plus vous aurez de sorties dans Logcat. Utiliser différents niveaux de journalisation vous permettra de sélectionner les informations les plus utiles et les plus pertinentes. L'utilisation de la journalisation est considérée comme une bonne pratique et est préférable à println() dans le développement Android, car les journaux de débogage et les journaux détaillés n'ont aucune incidence sur les performances des builds. Vous pouvez également filtrer les journaux en fonction de différents niveaux. En choisissant le niveau de journalisation approprié, vous aiderez les membres de votre équipe de développement qui ne connaissent pas le code aussi bien que vous. Cela facilite notamment l'identification et la résolution des bugs.

4. Journaux avec des messages d'erreur

Introduire un bug

Comme notre projet est vide, nous n'avons pas grand-chose à déboguer. Bon nombre des bugs que vous rencontrerez en tant que développeur Android impliquent le plantage des applications, ce qui nuit à l'expérience utilisateur. Ajoutons maintenant du code qui entraînera le plantage de cette application.

Vous vous souvenez peut-être d'avoir appris en classe de mathématiques que vous ne pouvez pas diviser un nombre par zéro. Voyons ce qui se passe lorsque nous essayons d'effectuer cette opération dans le code.

  1. Ajoutez la fonction suivante à MainActivity.kt au-dessus de la fonction logging(). Ce code commence par deux nombres et utilise repeat pour consigner le résultat de la division du numérateur par le dénominateur cinq fois. Chaque fois que le code du bloc repeat est exécuté, la valeur du dénominateur est réduite de un. Lors de la cinquième et dernière itération, l'application tente de diviser cette valeur par zéro.
fun division() {
    val numerator = 60
    var denominator = 4
    repeat(5) {
        Log.v(TAG, "${numerator / denominator}")
        denominator--
    }
}
  1. Après l'appel de logging() dans onCreate(), ajoutez un appel à la fonction division().
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    logging()
    division()
}
  1. Exécutez à nouveau l'application et notez qu'elle plante. Si vous faites défiler la page jusqu'aux journaux de la classe MainActivity.kt, vous verrez ceux de la fonction logging() que vous avez définie précédemment, les journaux détaillés de la fonction division(), puis un journal d'erreurs en rouge expliquant pourquoi l'application a planté.

12d87f287661a66.png

Anatomie d'une trace de la pile

Le journal d'erreurs qui décrit le plantage (ou exception) s'appelle une trace de la pile. La trace de la pile affiche toutes les fonctions appelées jusqu'à l'exception, en commençant par la fonction la plus récente. La sortie complète est illustrée ci-dessous.

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)

Cela fait beaucoup de texte ! Heureusement, vous n'avez généralement besoin de vous concentrer que sur quelques éléments pour identifier l'erreur exacte. Commençons par le haut.

  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 première ligne indique que l'activité n'a pas pu être lancée par l'application, ce qui explique le plantage de l'application. La ligne suivante fournit des informations supplémentaires. Plus précisément, elle indique que l'activité n'a pas pu être lancée à cause d'une exception arithmétique (ArithmeticException). Encore plus précisément, elle indique aussi que le type d'exception (ArithmeticException) était "divide by zero" (diviser par zéro).

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

Si vous faites défiler la page vers le bas jusqu'à la ligne "Caused by" (Causé par), une erreur s'affiche de nouveau. Cette fois, elle indique également la fonction exacte dans laquelle l'erreur s'est produite (division()), ainsi que le numéro de ligne exact (21). Le nom de fichier et le numéro de ligne présents dans la fenêtre Logcat sont accessibles par un lien hypertexte. La sortie affiche également le nom de la fonction dans laquelle l'erreur s'est produite, division(), ainsi que la fonction qui l'a appelée, onCreate().

Nous ne sommes pas surpris dans ce cas, car nous avons délibérément introduit ce bug. Toutefois, si vous devez déterminer la cause d'une erreur inconnue, il est particulièrement utile de connaître le type d'exception exact, le nom de la fonction et le numéro de ligne.

Pourquoi parle-t-on de "trace de la pile" ?

L'utilisation du terme "trace de la pile" pour désigner la sortie textuelle d'une erreur peut sembler étrange. Pour mieux en comprendre le fonctionnement, il est important de se familiariser d'abord avec la pile de fonctions.

Lorsqu'une fonction appelle une autre fonction, l'appareil n'exécute pas de code de la première fonction jusqu'à la fin de la deuxième. Une fois l'exécution de la deuxième fonction terminée, la première reprend là où elle s'était arrêtée. Il en va de même pour toutes les fonctions appelées par la deuxième fonction. La deuxième ne reprend qu'à la fin de la troisième (et de toutes les autres fonctions qu'elle appelle) et la première ne reprend qu'à la fin de l'exécution de la deuxième. Ce fonctionnement est semblable à celui d'une pile ordinaire dans le monde physique, comme une pile d'assiettes ou une pile de cartes. Si vous voulez prendre une assiette, vous allez prendre celle du haut. Il est impossible de prendre une assiette plus bas dans la pile sans retirer les assiettes qui se trouvent au-dessus.

La pile de fonctions peut être illustrée avec le code suivant.

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 vous appelez first(), les numéros sont consignés dans l'ordre suivant.

3
2
4
1

Pourquoi ? Lorsque la première fonction est appelée, elle appelle immédiatement second(). Le numéro 1 ne peut donc pas être consigné immédiatement. Voici ce à quoi ressemble la pile de fonctions.

second()
first()

La deuxième fonction appelle ensuite third(), qui l'ajoute à la pile de fonctions.

third()
second()
first()

La troisième fonction affiche ensuite le numéro 3. Une fois son exécution terminée, elle est supprimée de la pile de fonctions.

second()
first()

La fonction second() enregistre ensuite le numéro 2, puis appelle fourth(). Jusqu'à présent, les numéros 3, puis 2, ont été consignés, et la pile de fonctions est désormais comme suit.

fourth()
second()
first()

La fonction fourth() imprime le numéro 4 et est supprimée de la pile de fonctions. L'exécution de la fonction second() se termine ensuite, ce qui entraîne sa suppression de la pile de fonctions. Maintenant que second() et toutes les fonctions appelées sont terminées, l'appareil exécute le code restant dans first(), qui imprime le numéro 1.

Par conséquent, les numéros sont consignés dans l'ordre : 4, 2, 3, 1.

Si vous prenez le temps de parcourir le code en gardant à l'esprit le principe de fonctionnement de la pile de fonctions, vous pouvez voir exactement quel code est exécuté et dans quel ordre. C'est une technique efficace pour résoudre des bugs tels que la division par zéro. En parcourant le code, vous pouvez également déterminer où placer les instructions de journalisation pour résoudre des problèmes plus complexes.

5. Identifier et corriger le bug à l'aide des journaux

Dans la section précédente, vous avez examiné la trace de la pile, et plus particulièrement cette ligne.

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

Ici, vous pouvez constater que le plantage s'est produit à la ligne 21 et qu'il était lié à une opération de division par zéro. Nous pouvons en déduire que, quelque part avant l'exécution de ce code, le dénominateur était 0. Bien que vous puissiez essayer de parcourir le code vous-même, ce qui est une méthode légitime pour un petit exemple comme celui-ci, vous pouvez également recourir à des instructions de journalisation pour gagner du temps en imprimant la valeur du dénominateur avant que la division par zéro n'ait lieu.

  1. Avant l'instruction Log.v(), ajoutez un appel Log.d() qui enregistre le dénominateur. Log.d() est utilisé, car ce journal est spécifiquement conçu pour le débogage et vous permet de filtrer les journaux détaillés.
Log.d(TAG, "$denominator")
  1. Exécutez à nouveau votre application. Même si elle plante toujours, le dénominateur devrait être consigné plusieurs fois. Vous pouvez utiliser une configuration de filtre pour n'afficher que les journaux avec la balise "MainActivity".

d6ae5224469d3fd4.png

  1. Vous pouvez voir que plusieurs valeurs sont affichées. Il semble que la boucle s'exécute plusieurs fois avant de planter à la cinquième itération lorsque le dénominateur correspond à 0. Ce comportement est logique, car le dénominateur correspond à 4, et la boucle réduit cette valeur de 1 pendant 5 itérations. Pour corriger le bug, vous pouvez remplacer le nombre d'itérations dans la boucle, qui est de 5, par 4. Si vous relancez l'application, elle ne devrait plus planter.
fun division() {
    val numerator = 60
    var denominator = 4
    repeat(4) {
        Log.v(TAG, "${numerator / denominator}")
        denominator--
    }
}

6. Exemple de débogage : accès à une valeur qui n'existe pas

Par défaut, le modèle Blank Activity (Activité vide) que vous avez utilisé pour créer le projet ajoute une seule activité. TextView est centré sur l'écran. Comme vous l'avez appris précédemment, pour référencer des vues à partir du code, vous pouvez définir un ID dans l'éditeur de mise en page et accéder à la vue avec findViewById(). Lorsque l'élément onCreate() est appelé dans une classe d'activité, vous devez d'abord appeler setContentView() pour charger un fichier de mise en page (comme activity_main.xml). Si vous essayez d'appeler findViewById() avant d'appeler setContentView(), l'application plante, car la vue n'existe pas. Essayons d'accéder à cette vue pour illustrer un autre bug.

  1. Ouvrez activity_main.xml, sélectionnez Hello World! (TextView) et définissez id sur hello_world.

c94be640d0e03e1d.png

  1. Revenez dans ActivityMain.kt dans onCreate(), ajoutez du code pour obtenir TextView et remplacez son texte par "Hello, debugging!" avant l'appel de 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. Exécutez à nouveau l'application et notez qu'elle plante à nouveau immédiatement après son lancement. Vous devrez peut-être supprimer le filtre de l'exemple précédent pour afficher les journaux sans la balise "MainActivity". 840ddd002e92ee46.png

L'exception devrait être l'un des derniers éléments à apparaître dans Logcat (sinon, vous pouvez rechercher RuntimeException). La sortie devrait se présenter comme suit.

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)

Comme précédemment, le message "Unable to start activity" (Impossible de démarrer l'activité) s'affiche en haut de l'écran. Ce comportement est logique, car l'application a planté avant le lancement de MainActivity. La ligne suivante fournit un peu plus de détails sur l'erreur.

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

Plus bas dans la trace de la pile, figure également cette ligne, avec l'appel de fonction et le numéro de ligne exacts.

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

Que signifie exactement cette erreur et qu'est-ce qu'une valeur "null" ? Bien qu'il s'agisse d'un exemple fictif et que vous ayez peut-être déjà compris la raison pour laquelle l'application a planté, vous recevrez inévitablement des messages d'erreur inconnus. Lorsque cela se produit, il est probable que vous ne soyez pas le premier à rencontrer cette erreur. Même les développeurs les plus expérimentés font des recherches sur Google pour déterminer comment d'autres ont résolu un problème. Lorsque vous effectuez des recherches sur cette erreur, plusieurs résultats s'affichent sur StackOverflow, un site sur lequel les développeurs peuvent poser des questions et répondre à d'autres questions sur des bugs spécifiques ou des sujets de programmation plus généraux.

Toutefois, vous pouvez être dérouté par le nombre de questions posées avec des réponses similaires, mais pas exactement identiques. C'est pourquoi nous vous conseillons de garder à l'esprit les conseils suivants lorsque vous cherchez des réponses par vous-même.

  1. Quand la réponse a-t-elle été soumise ? Il est possible que les réponses datant de plusieurs années ne soient plus pertinentes ou qu'elles utilisent une version obsolète d'un langage ou d'un framework.
  2. La réponse porte-t-elle sur le langage Java ou Kotlin ? Votre problème est-il propre à un langage ou à un framework spécifique ?
  3. Les réponses marquées comme "acceptées" ou ayant reçu le plus de votes sont généralement de meilleure qualité, mais gardez à l'esprit que d'autres réponses peuvent aussi fournir des informations précieuses.

1636a21ff125a74c.png

Le chiffre indique le nombre de votes pour (ou de "votes contre"), et la coche verte indique qu'une réponse a été acceptée.

Si la question que vous recherchez n'a pas encore été posée, vous pouvez toujours en poser une nouvelle. Lorsque vous posez une question sur StackOverflow (ou un autre site), n'oubliez pas ces consignes.

Commencez par rechercher l'erreur.

a60ba40e5247455e.png

Si vous consultez certaines réponses, vous constaterez que l'erreur peut avoir plusieurs causes. Toutefois, étant donné que vous avez délibérément appelé findViewById() avant setContentView(), certaines réponses à cette question semblent prometteuses. La deuxième réponse ayant reçu le plus de votes, par exemple, dit :

"Vous appelez probablement FindViewById avant d'appeler setContentView. Dans ce cas, essayez d'appeler FindViewById APRÈS setContentView."

Grâce à cette réponse, vous pouvez constater dans le code que l'appel à findViewById() a été effectué avant setContentView() alors qu'il doit être appelé après setContentView().

Mettez à jour le code pour corriger l'erreur.

  1. Déplacez l'appel vers findViewById() et la ligne qui définit le texte de helloTextView ci-dessous sous l'appel de setContentView(). La nouvelle méthode onCreate() devrait se présenter comme suit. Vous pouvez également ajouter des journaux, comme illustré ici, pour vérifier que le bug a été corrigé.
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. Exécutez à nouveau l'application. Notez que l'application ne plante plus et que le texte est mis à jour comme prévu.

9ff26c7deaa4a7cc.png

Réaliser des captures d'écran

À ce stade, vous avez probablement vu de nombreuses captures d'écran de l'émulateur Android dans ce cours. Les captures d'écran sont relativement simples, mais elles peuvent être utiles pour partager des informations telles que les étapes permettant de reproduire des bugs avec d'autres membres de l'équipe. Pour effectuer une capture d'écran dans l'émulateur Android, appuyez sur l'icône représentant un appareil photo dans la barre d'outils située à droite.

455336f50c5c3c7f.png

Vous pouvez également utiliser le raccourci clavier Commande+S pour effectuer une capture d'écran. Celle-ci sera automatiquement enregistrée dans le dossier "Bureau".

Enregistrer une application en cours d'exécution

Les captures d'écran peuvent fournir beaucoup d'informations, mais il est parfois utile de partager les enregistrements d'une application en cours d'exécution pour aider d'autres personnes à reproduire le comportement qui a causé un bug. L'émulateur Android propose des outils intégrés pour vous aider à capturer facilement un GIF (image animée) de l'application en cours d'exécution.

  1. Dans les outils de l'émulateur, à droite, cliquez sur le bouton Plus 558dbea4f70514a8.png (dernière option) pour afficher les autres options de débogage de l'émulateur. S'affiche alors une fenêtre pop-up qui contient des outils supplémentaires permettant de simuler la fonctionnalité de différents appareils physiques à des fins de test.

46b1743301a2d12.png

  1. Dans le menu de gauche, cliquez sur Record and Playback (Enregistrement et lecture). Un écran avec un bouton vous permet de démarrer l'enregistrement.

dd8b5019702ead03.png

  1. Pour le moment, votre projet n'a rien d'intéressant à enregistrer, si ce n'est un élément TextView statique. Modifions le code pour mettre à jour le libellé à quelques secondes d'intervalle et afficher le résultat de la division. Dans la méthode division() de MainActivity, ajoutez un appel à Thread.sleep(3000) avant l'appel à Log(). La méthode devrait maintenant se présenter comme suit (notez que la boucle ne doit se répéter que quatre fois afin d'éviter un plantage).
fun division() {
   val numerator = 60
   var denominator = 4
   repeat(4) {
       Thread.sleep(3000)
       Log.v(TAG, "${numerator / denominator}")
       denominator--
   }
}
  1. Dans activity_main.xml, définissez l'id de TextView sur division_textview.

db3c1ef675872faf.png

  1. Dans MainActivity.kt, remplacez l'appel à Log.v() par les appels suivants à findViewById() et setText() pour définir le texte sur le quotient.
findViewById<TextView>(R.id.division_textview).setText("${numerator / denominator}")
  1. Étant donné que vous affichez maintenant le résultat de la division dans l'interface utilisateur de l'application, vous devez vérifier certains détails sur la façon dont s'exécutent les mises à jour de l'interface utilisateur. Tout d'abord, vous devez créer un thread pouvant exécuter la boucle repeat. Sinon, Thread.sleep(3000) bloquerait le thread principal, et la vue de l'application ne s'afficherait pas tant que onCreate() n'est pas terminé (y compris division() avec sa boucle 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 vous essayez maintenant d'exécuter l'application, vous remarquerez une FATAL EXCEPTION. Cette exception s'explique par le fait que seuls les threads qui ont créé une vue sont autorisés à la modifier. Heureusement, vous pouvez référencer le thread UI à l'aide de runOnUiThread(). Modifiez division() pour mettre à jour TextView dans le thread UI.
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. Exécutez l'application, puis passez immédiatement à l'émulateur. Au lancement de l'application, cliquez sur le bouton Start Recording (Démarrer l'enregistrement) dans la fenêtre "Extended Controls" (Commandes étendues). Le quotient devrait être mis à jour toutes les trois secondes. Une fois qu'il aura été mis à jour plusieurs fois, cliquez sur Stop Recording (Arrêter l'enregistrement).

55121bab5b5afaa6.png

  1. Par défaut, le résultat est enregistré au format .webm. Utilisez la liste déroulante pour exporter la sortie sous forme de fichier GIF.

850713aa27145908.png

7. Félicitations

Félicitations ! Dans ce parcours, vous avez appris ce qui suit :

  • Le débogage consiste à corriger les bugs dans votre code.
  • Le journal vous permet d'imprimer du texte avec différents niveaux de journalisation et balises de journal.
  • La trace de la pile fournit des informations sur une exception, telles que la fonction exacte qui en est à l'origine et le numéro de ligne concerné.
  • Lors du débogage, il est possible qu'un utilisateur ait déjà rencontré le même problème ou un problème similaire. Vous pouvez utiliser des sites tels que StackOverflow pour effectuer des recherches sur ce bug.
  • Vous pouvez facilement exporter des captures d'écran et des GIF animés à l'aide de l'émulateur Android.

En savoir plus