Pengantar proses debug

1. Sebelum memulai

Siapa pun yang pernah menggunakan software kemungkinan besar juga pernah menemui bug. Bug adalah error dalam software yang menyebabkan perilaku yang tidak diinginkan, seperti aplikasi error, atau fitur tidak berfungsi seperti yang diharapkan. Semua developer, baik yang sudah berpengalaman maupun belum, menghasilkan bug saat menulis kode, dan salah satu keterampilan paling penting bagi developer Android adalah mengidentifikasi dan memperbaikinya. Tidak jarang aplikasi dirilis ulang seluruhnya hanya untuk memperbaiki bug. Sebagai contoh, lihat detail versi Google Maps di bawah:

9d5ec1958683e173.png

Proses perbaikan bug disebut proses debug. Ilmuwan komputer terkenal, Brian Kernighan, pernah berkata bahwa "alat debug yang paling efektif adalah pemikiran yang cermat, ditambah dengan pernyataan cetak yang ditempatkan dengan bijak". Meskipun Anda mungkin sudah terbiasa dengan pernyataan println() Kotlin dari codelab sebelumnya, developer Android profesional menggunakan logging untuk mengatur output programnya dengan lebih baik. Dalam codelab ini, Anda akan mempelajari cara menggunakan logging di Android Studio dan cara menggunakannya sebagai alat debug. Anda juga akan belajar membaca log pesan error, yang disebut pelacakan tumpukan, untuk mengidentifikasi dan memperbaiki bug. Terakhir, Anda akan mempelajari cara meneliti bug sendiri, serta mempelajari cara merekam output dari emulator Android, baik berupa screenshot atau pun GIF dari aplikasi yang berjalan.

Prasyarat

  • Anda sudah mengetahui cara membuka project di Android Studio.

Yang akan Anda pelajari

Di akhir codelab ini, Anda akan dapat

  • Menulis log menggunakan android.util.Logger.
  • Mengetahui kapan harus menggunakan berbagai level log.
  • Menggunakan log sebagai alat debug yang sederhana tetapi efektif.
  • Mengetahui cara menemukan informasi penting dalam pelacakan tumpukan.
  • Menelusuri pesan error untuk mengatasi error terkait aplikasi.
  • Mengambil screenshot dan GIF animasi dari Android Emulator.

Yang akan Anda butuhkan

  • Komputer yang dilengkapi Android Studio.

2. Membuat project baru

Daripada menggunakan aplikasi yang besar dan kompleks, kita akan memulai dengan project kosong untuk menunjukkan laporan log dan penggunaannya untuk proses debug.

Mulailah dengan membuat project Android Studio baru, seperti yang ditunjukkan.

  1. Di layar New Project, pilih Empty Activity.

72a0bbf2012bcb7d.png

  1. Beri nama Debugging untuk aplikasi yang akan dibuat. Pastikan bahasa telah disetel ke Kotlin, dan setelan lainnya tidak diubah.

60a1619c07fae8f5.png

Setelah membuat project, Anda akan disambut dengan project Android Studio baru, yang menampilkan file bernama MainActivity.kt.

e3ab4a557c50b9b0.png

3. Output logging dan debug

Pada tutorial sebelumnya, Anda menggunakan pernyataan println() Kotlin untuk menghasilkan output teks. Di aplikasi Android, praktik terbaik untuk logging output adalah menggunakan class Log. Ada beberapa fungsi untuk logging output dengan format Log.v(), Log.d(), Log.i(), Log.w(), atau Log.e(). Metode ini menggunakan dua parameter: yang pertama, disebut "tag", adalah string yang mengidentifikasi sumber pesan log (seperti nama class yang mencatat log teks). Yang kedua adalah pesan log-nya.

Lakukan langkah-langkah berikut untuk mulai menggunakan logging di project kosong Anda.

  1. Di MainActivity.kt, sebelum deklarasi class, tambahkan konstanta yang disebut TAG, dan setel nilainya ke nama class, MainActivity.
private const val TAG = "MainActivity"
  1. Tambahkan fungsi baru ke class MainActivity, yang disebut logging(), seperti yang ditunjukkan.
fun logging() {
    Log.v(TAG, "Hello, world!")
}
  1. Panggil logging() di onCreate(). Metode onCreate() yang baru akan terlihat seperti berikut.
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    logging()
}
  1. Jalankan aplikasi untuk melihat cara kerja log. Log akan muncul di jendela Logcat di bagian bawah layar. Karena Logcat akan menampilkan output dari proses lain di perangkat (atau emulator), Anda dapat memilih aplikasi (com.example.debugging) dari menu drop-down untuk memfilter log yang tidak relevan dengan aplikasi Anda.

199c65d11ee52b5c.png

Di jendela output, Anda akan dapat melihat halaman "Halo Dunia!". Jika perlu, ketik "halo" dalam kotak penelusuran di bagian atas jendela Logcat, untuk menelusuri semua log.

92f258013bc15d12.png

Level log

Alasan adanya berbagai fungsi log, yang diberi nama dengan huruf yang berbeda-beda, adalah karena fungsi tersebut terkait dengan level log yang berbeda-beda. Bergantung pada jenis informasi yang ingin dihasilkan, Anda akan menggunakan level log tertentu untuk membantu memfilternya dalam output Logcat. Ada lima level log utama yang akan Anda gunakan secara rutin.

Level log

Kasus penggunaan

Contoh

ERROR

Log ERROR melaporkan bahwa ada masalah yang sangat serius, seperti alasan aplikasi mengalami error.

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

WARN

Log WARN lebih ringan daripada error, tetapi tetap melaporkan sesuatu yang harus diperbaiki untuk menghindari error yang lebih serius. Contohnya mungkin jika Anda memanggil fungsi yang tidak digunakan lagi, yang berarti penggunaannya tidak disarankan dan diganti dengan alternatif yang lebih baru.

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

INFO

Log INFO memberikan informasi yang berguna, seperti operasi yang berhasil diselesaikan.

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

DEBUG

Log DEBUG berisi informasi yang mungkin berguna saat menyelidiki masalah. Log ini tidak ada dalam build rilis seperti yang Anda publikasikan di 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

Sesuai dengan namanya, verbose (panjang) adalah level log yang paling tidak spesifik. Penentuan apakah suatu log adalah log debug atau log verbose memang sedikit subjektif. Namun, umumnya log verbose adalah log yang bisa dihapus setelah sebuah fitur diimplementasikan, sedangkan log debug mungkin masih berguna untuk proses debug. Log ini juga tidak disertakan dalam build rilis.

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

Perlu diingat bahwa tidak ada aturan yang ditetapkan untuk waktu penggunaan setiap jenis level log, terutama untuk waktu penggunaan DEBUG dan VERBOSE. Tim pengembangan software dapat membuat panduannya sendiri tentang kapan harus menggunakan setiap level log, atau mungkin memutuskan untuk sama sekali tidak menggunakan level log tertentu, seperti VERBOSE. Hal penting yang harus diingat tentang kedua level log ini adalah bahwa level log tidak ada di build rilis, sehingga penggunaan log untuk debug tidak akan memengaruhi performa aplikasi yang dipublikasikan, sedangkan pernyataan println() tetap berada di build rilis dan akan berdampak negatif pada performa aplikasi.

Mari kita lihat seperti apa wujud berbagai level log ini di Logcat.

  1. Di MainActivity.kt, ganti konten metode logging() dengan yang berikut ini.
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. Jalankan aplikasi dan amati output di Logcat. Jika perlu, filter output untuk hanya menampilkan log dari proses com.example.debugging. Anda juga dapat memfilter output untuk hanya menampilkan log dengan tag "MainActivity". Untuk melakukannya, pilih Edit Filter Configuration dari menu dropdown di kanan atas jendela Logcat.

383ec6d746bb72b1.png

  1. Lalu ketik "MainActivity" untuk Tag Log, lalu buat nama untuk filter Anda seperti yang ditampilkan.

e7ccfbb26795b3fc.png

  1. Sekarang Anda hanya akan melihat pesan log dengan tag "MainActivity".

4061ca006b1d278c.png

Perhatikan bahwa ada huruf sebelum nama class, misalnya W/MainActivity, yang sesuai dengan level log. Selain itu, log WARN ditampilkan dalam warna biru, sementara log ERROR ditampilkan dalam warna merah, seperti error fatal dalam contoh sebelumnya.

  1. Seperti halnya memfilter output debug menurut proses, Anda juga dapat memfilter output menurut level log. Secara default, filter ini disetel ke Verbose yang akan menampilkan log VERBOSE dan level log yang lebih tinggi. Pilih Warn dari menu dropdown dan perhatikan, sekarang hanya log level WARN dan ERROR yang ditampilkan.

c4aa479a8dd9d4ca.png

  1. Ubah lagi menu dropdown ke Assert, dan amati bahwa tidak ada log yang ditampilkan. Ini akan memfilter semua yang berada di level ERROR dan di bawahnya.

ee3be7cfaa0d8bd1.png

Meskipun mungkin terkesan menganggap pernyataan println() terlalu serius, saat Anda membangun aplikasi yang lebih besar akan ada lebih banyak output Logcat, dan penggunaan berbagai level log akan memungkinkan Anda memilih informasi yang paling berguna dan relevan. Menggunakan Log dianggap sebagai praktik terbaik dan lebih disarankan daripada println() dalam pengembangan Android, karena log debug dan log verbose tidak akan memengaruhi performa di build rilis. Anda juga dapat memfilter log menurut berbagai level log. Memilih level log yang tepat akan membantu orang lain di tim pengembangan Anda yang mungkin belum terlalu memahami kode tersebut, dan membuat Anda jauh lebih mudah dalam mengidentifikasi serta memperbaiki bug.

4. Log dengan pesan error

Menghadirkan bug

Tidak banyak proses debug yang dapat dilakukan dalam project kosong. Banyak bug yang akan Anda temui sebagai developer Android berkaitan dengan error yang membuat aplikasi berhenti berjalan - jelas bukan pengalaman pengguna yang baik. Mari tambahkan beberapa kode yang menyebabkan aplikasi ini error.

Anda mungkin mengingat pembelajaran di kelas matematika bahwa kita tidak dapat membagi sebuah angka dengan nol. Mari kita lihat apa yang terjadi saat kita mencoba membagi dengan nol dalam kode.

  1. Tambahkan fungsi berikut ke MainActivity.kt di atas fungsi logging(). Kode ini dimulai dengan dua angka dan menggunakan repeat untuk mencatat log hasil pembagian pembilang dengan penyebut sebanyak lima kali. Setiap kali kode dalam blok repeat berjalan, nilai penyebutnya dikurangi satu. Pada iterasi kelima dan terakhir, aplikasi akan mencoba membagi dengan nol.
fun division() {
    val numerator = 60
    var denominator = 4
    repeat(5) {
        Log.v(TAG, "${numerator / denominator}")
        denominator--
    }
}
  1. Setelah panggilan ke logging() di onCreate(), tambahkan panggilan ke fungsi division().
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    logging()
    division()
}
  1. Jalankan aplikasi Anda lagi dan perhatikan bahwa aplikasi menjadi error. Jika men-scroll ke bawah log dari class MainActivity.kt, Anda akan melihat log dari fungsi logging() yang ditentukan sebelumnya, log verbose dari fungsi division(), lalu log error berwarna merah yang menjelaskan penyebab aplikasi error.

12d87f287661a66.png

Anatomi pelacakan tumpukan

Log error yang menjelaskan error (juga disebut pengecualian) disebut pelacakan tumpukan. Pelacakan tumpukan menampilkan semua fungsi yang dipanggil, yang mengarah ke pengecualian dan dimulai dari fungsi yang terakhir dipanggil. Output lengkapnya ditampilkan di bawah.

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)

Teksnya banyak sekali! Untungnya, Anda biasanya hanya memerlukan beberapa langkah untuk menemukan error yang benar. Mari mulai dari atas.

  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

Baris pertama menyatakan bahwa aplikasi tidak dapat memulai aktivitas yang menjadi penyebab aplikasi error. Baris berikutnya memberikan lebih banyak informasi. Secara khusus, alasan aktivitas tidak dapat dimulai adalah karena ArithmeticException. Lebih spesifik lagi, jenis ArithmeticException adalah "divide by zero".

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

Jika Anda men-scroll ke bawah, ke baris "Caused by", lagi-lagi ditampilkan error "divide by zero". Kali ini, laporan ini juga menunjukkan fungsi yang sama saat terjadi error (division()), dan nomor baris yang sama (21). Nama file dan nomor baris di jendela Logcat memiliki hyperlink. Output juga menunjukkan nama fungsi tempat error terjadi, division(), dan fungsi yang memanggilnya, onCreate().

Semua ini tidak mengherankan karena bug memang sengaja dihadirkan. Namun, jika Anda perlu mengetahui penyebab error yang tidak dikenal, informasi tentang jenis pengecualian, nama fungsi, dan nomor baris yang tepat akan sangat berguna.

Mengapa "pelacakan tumpukan"?

Istilah "pelacakan tumpukan" mungkin terdengar seperti istilah yang aneh untuk output teks dari error. Untuk lebih memahami cara kerjanya, Anda perlu mengetahui lebih banyak tentang stack fungsi.

Saat satu fungsi memanggil fungsi lain, perangkat tidak akan menjalankan kode apa pun dari fungsi pertama hingga fungsi kedua selesai. Setelah fungsi kedua selesai dijalankan, fungsi pertama akan dilanjutkan mulai dari saat eksekusi fungsi dihentikan. Hal yang sama berlaku untuk setiap fungsi yang dipanggil oleh fungsi kedua. Fungsi kedua tidak akan melanjutkan eksekusi hingga fungsi ketiga (dan fungsi lain yang dipanggilnya) selesai, dan fungsi pertama tidak akan dilanjutkan hingga fungsi kedua selesai dijalankan. Hal ini serupa dengan tumpukan di dunia fisik, seperti tumpukan piring atau tumpukan kartu. Jika ingin mengambil piring, Anda akan memilih yang paling atas. Tidak mungkin mengambil piring di bagian bawah tumpukan tanpa mengambil semua piring di atasnya terlebih dahulu.

Tumpukan fungsi dapat diilustrasikan dengan kode berikut.

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

Jika Anda memanggil first(), nomor tersebut akan dicatat dalam log dengan urutan berikut.

3
2
4
1

Mengapa seperti itu? Saat dipanggil, fungsi pertama akan langsung memanggil second(), sehingga angka 1 tidak dapat langsung dicatat ke log. Tumpukan fungsi terlihat seperti ini.

second()
first()

Fungsi kedua kemudian memanggil third(), yang menambahkannya ke stack fungsi.

third()
second()
first()

Fungsi ketiga kemudian mencetak angka 3. Setelah selesai dijalankan, angka ini akan dihapus dari stack fungsi.

second()
first()

Fungsi second() kemudian mencatat log nomor 2, lalu memanggil fourth(). Sejauh ini, angka 3, lalu 2, telah dicatat dalam log dan stack fungsi kini menjadi seperti berikut.

fourth()
second()
first()

Fungsi fourth() mencetak angka 4, dan dihapus (dikeluarkan) dari stack fungsi. Fungsi second() kemudian selesai dieksekusi dan dikeluarkan dari stack fungsi. Setelah second() dan semua fungsi yang dipanggil selesai, perangkat akan menjalankan kode yang tersisa di first() yang mencetak angka 1.

Jadi, angka-angka tersebut dicatat dalam log dengan urutan yang sama: 4, 2, 3, 1.

Dengan mempelajari kode tersebut secara perlahan, serta mengingat dan memahami stack fungsi, Anda dapat melihat dengan tepat kode yang sedang dijalankan beserta urutannya. Teknik ini saja dapat menjadi teknik proses debug yang canggih untuk bug seperti contoh pembagian dengan nol di atas. Melewati kode juga dapat memberi Anda ide yang bagus terkait tempat untuk meletakkan laporan log guna membantu men-debug masalah yang lebih kompleks.

5. Menggunakan log untuk mengidentifikasi dan memperbaiki bug

Di bagian sebelumnya, Anda telah memeriksa pelacakan tumpukan, khususnya baris ini.

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

Di sini, Anda dapat mengetahui bahwa error terjadi di baris 21 dan terkait pembagian dengan nol. Jadi, pada kode sebelum kode ini dieksekusi, penyebutnya adalah 0. Meskipun Anda dapat mencoba memperbaiki kode sendiri, yang dapat berfungsi dengan sangat baik untuk contoh kecil seperti ini, Anda juga dapat menggunakan laporan log untuk menghemat waktu dengan mencetak nilai penyebut sebelum pembagian dengan nol terjadi.

  1. Sebelum pernyataan Log.v(), tambahkan panggilan Log.d() yang mencatat log penyebut. Log.d() digunakan karena log ini khusus untuk proses debug, sehingga Anda dapat memfilter log verbose.
Log.d(TAG, "$denominator")
  1. Jalankan aplikasi Anda lagi. Meskipun masih error, penyebutnya harus dicatat dalam log beberapa kali. Anda dapat menggunakan Filter Configuration untuk hanya menampilkan log dengan tag "MainActivity".

d6ae5224469d3fd4.png

  1. Anda dapat melihat bahwa beberapa nilai telah dicetak. Terlihat bahwa loop ini dieksekusi beberapa kali sebelum error terjadi pada iterasi kelima saat penyebutnya adalah 0. Hal ini masuk akal, karena penyebutnya adalah 4 dan penyebut akan berkurang sebesar 1 pada setiap loop untuk 5 iterasi. Untuk memperbaiki bug, Anda dapat mengubah jumlah iterasi dalam loop dari 5 menjadi 4. Jika Anda menjalankan ulang aplikasi, seharusnya tidak terjadi error lagi.
fun division() {
    val numerator = 60
    var denominator = 4
    repeat(4) {
        Log.v(TAG, "${numerator / denominator}")
        denominator--
    }
}

6. Contoh proses debug: mengakses nilai yang tidak ada

Secara default, template Blank Activity yang Anda gunakan untuk membuat project akan menambahkan satu aktivitas, dengan TextView yang berpusat di layar. Seperti yang telah dipelajari sebelumnya, Anda dapat mereferensikan tampilan dari kode dengan menetapkan ID di editor tata letak dan mengakses tampilan dengan findViewById(). Saat onCreate() dipanggil di class aktivitas, setContentView() perlu dipanggil terlebih dahulu untuk memuat file tata letak (seperti activity_main.xml). Jika Anda mencoba memanggil findViewById() sebelum memanggil setContentView(), aplikasi akan error karena tampilan tidak ada. Mari kita coba mengakses tampilan untuk membantu menggambarkan bug lain.

  1. Buka activity_main.xml, pilih ikon Halo Dunia! TextView, lalu setel id ke hello_world.

c94be640d0e03e1d.png

  1. Kembali ke ActivityMain.kt di onCreate(), tambahkan kode untuk mendapatkan TextView dan ubah teksnya menjadi "Halo, proses debug!" sebelum panggilan ke 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. Jalankan lagi aplikasi, dan amati bahwa aplikasi langsung error lagi saat diluncurkan. Anda mungkin perlu menghapus filter dari contoh sebelumnya untuk melihat log tanpa tag "MainActivity". 840ddd002e92ee46.png

Pengecualian seharusnya menjadi salah satu hal terakhir yang muncul di Logcat (jika tidak, Anda dapat menelusuri RuntimeException). Outputnya akan terlihat seperti berikut.

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)

Seperti sebelumnya, di bagian atas akan tertera "Unable to start activity". Hal ini masuk akal karena aplikasi mengalami error sebelum MainActivity diluncurkan. Baris berikutnya memberitahukan error lebih lanjut.

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

Di bagian bawah pelacakan tumpukan, Anda juga akan melihat baris ini, yang menunjukkan panggilan fungsi dan nomor baris yang tepat.

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

Apa sebenarnya arti error ini, dan apa sebenarnya maksud nilai "null"? Meskipun ini adalah contoh rumit dan Anda mungkin sudah tahu mengapa aplikasi error, Anda pasti akan menemukan pesan error yang belum pernah dilihat sebelumnya. Saat ini terjadi, Anda bukanlah orang pertama yang melihat error tersebut, dan developer yang paling berpengalaman pun sering kali akan melihat pesan error tersebut untuk mengetahui cara orang lain menyelesaikan masalah tersebut. Pencarian error ini menampilkan beberapa hasil dari StackOverflow, situs tempat developer dapat mengajukan pertanyaan dan memberikan jawaban tentang kode yang berisi bug atau topik pemrograman yang lebih umum.

Karena mungkin ada banyak pertanyaan dengan jawaban yang mirip namun tidak sama persis, perhatikan tips berikut saat menelusuri jawaban.

  1. Berapa lama usia balasan tersebut? Balasan dari beberapa tahun lalu mungkin tidak lagi relevan, atau mungkin menggunakan versi bahasa atau framework yang sudah tidak berlaku.
  2. Apakah jawabannya menggunakan Java atau Kotlin? Apakah masalah Anda khusus untuk satu bahasa atau bahasa lainnya, atau terkait dengan framework tertentu?
  3. Jawaban yang ditandai sebagai "diterima" atau memiliki lebih banyak suara positif mungkin berkualitas lebih tinggi, tetapi perhatikan bahwa jawaban lain masih dapat memberikan informasi yang berharga.

1636a21ff125a74c.png

Angka menunjukkan jumlah suara positif (atau suara negatif) dan tanda centang hijau menunjukkan jawaban yang diterima.

Jika tidak dapat menemukan masalah Anda di pertanyaan yang sudah ada, Anda dapat mengajukan pertanyaan baru kapan saja. Saat mengajukan pertanyaan di StackOverflow (atau situs apa pun), sebaiknya perhatikan pedoman ini.

Lanjutkan dan telusuri error.

a60ba40e5247455e.png

Setelah membaca beberapa jawaban, Anda akan menemukan bahwa error dapat memiliki beberapa penyebab yang berbeda. Namun, karena Anda sengaja memanggil findViewById() sebelum setContentView(), beberapa jawaban tentang pertanyaan ini sepertinya menjanjikan. Misalnya, jawaban yang paling banyak diberi suara menyatakan:

"Mungkin, Anda memanggil findViewById sebelum memanggil setContentView? Jika demikian, coba panggil findViewById SETELAH memanggil setContentView"

Setelah melihat jawaban ini, Anda selanjutnya dapat memverifikasi dalam kode bahwa panggilan ke findViewById() terlalu awal, terjadi sebelum setContentView(), dan bahwa panggilan harus dipanggil setelah setContentView().

Perbaiki error dengan memperbarui kode.

  1. Pindahkan panggilan ke findViewById() dan baris yang menyetel teks helloTextView di bawah panggilan ke setContentView(). Metode onCreate() baru akan terlihat seperti di bawah ini. Anda juga dapat menambahkan log, seperti yang ditunjukkan, untuk memverifikasi bahwa bug telah diperbaiki.
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. Jalankan kembali aplikasi. Perhatikan bahwa aplikasi tidak lagi error, dan teks telah diperbarui seperti yang diharapkan.

9ff26c7deaa4a7cc.png

Mengambil screenshot

Hingga saat ini, Anda mungkin telah melihat banyak screenshot dari emulator Android dalam kursus ini. Mengambil screenshot relatif mudah, tetapi dapat berguna untuk berbagi informasi, seperti langkah untuk mereproduksi bug dengan anggota tim lainnya. Anda dapat mengambil screenshot di emulator Android dengan menekan ikon kamera di toolbar di sebelah kanan.

455336f50c5c3c7f.png

Anda juga dapat menggunakan pintasan keyboard Command+S untuk mengambil screenshot. Screenshot akan otomatis disimpan ke folder Desktop Anda.

Merekam aplikasi yang sedang berjalan

Meskipun screenshot dapat menyampaikan banyak informasi, terkadang lebih bermanfaat untuk membagikan rekaman dari aplikasi yang sedang berjalan guna membantu orang lain mereproduksi sesuatu yang menyebabkan bug. Emulator Android menawarkan beberapa alat bawaan untuk membantu Anda merekam GIF (gambar animasi) dengan mudah dari aplikasi yang sedang berjalan.

  1. Pada alat emulator di sebelah kanan, klik tombol More 558dbea4f70514a8.png (opsi terakhir) untuk menampilkan opsi proses debug emulator tambahan. Jendela pop-up akan muncul, yang menyediakan alat tambahan guna menyimulasikan fungsi perangkat fisik untuk tujuan pengujian.

46b1743301a2d12.png

  1. Di menu sebelah kiri, klik Record and Playback, lalu Anda akan melihat layar dengan tombol untuk mulai merekam.

dd8b5019702ead03.png

  1. Saat ini, project Anda tidak memiliki sesuatu yang menarik untuk direkam, selain TextView statis. Mari kita ubah kode untuk memperbarui label setiap beberapa detik guna menampilkan hasil pembagian. Pada metode division() di MainActivity, tambahkan panggilan ke Thread.sleep(3000) sebelum panggilan ke Log(). Sekarang metode akan terlihat seperti berikut (perhatikan bahwa loop hanya boleh diulang 4 kali untuk menghindari error).
fun division() {
   val numerator = 60
   var denominator = 4
   repeat(4) {
       Thread.sleep(3000)
       Log.v(TAG, "${numerator / denominator}")
       denominator--
   }
}
  1. Di activity_main.xml, setel id dari TextView ke division_textview.

db3c1ef675872faf.png

  1. Kembali ke MainActivity.kt, ganti panggilan ke Log.v() dengan panggilan ke findViewById() dan setText() berikut untuk menyetel teks menjadi hasil bagi.
findViewById<TextView>(R.id.division_textview).setText("${numerator / denominator}")
  1. Anda sekarang tengah merender hasil pembagian dalam UI aplikasi sehingga Anda perlu memperhatikan beberapa detail tentang bagaimana update UI Anda berjalan. Pertama, Anda harus membuat thread baru yang dapat menjalankan loop repeat. Jika tidak, Thread.sleep(3000) akan memblokir thread utama, dan tampilan aplikasi tidak akan dirender hingga onCreate() selesai (termasuk division() dengan loop repeat-nya).
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. Jika Anda mencoba menjalankan aplikasi sekarang, Anda akan melihat FATAL EXCEPTION. Alasan pengecualian ini adalah hanya thread yang membuat tampilan yang diizinkan untuk mengubahnya. Untungnya, Anda dapat mereferensikan UI thread menggunakan runOnUiThread(). Ubah division() untuk memperbarui TextView dalam UI thread.
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. Jalankan aplikasi Anda, lalu segera beralih ke emulator. Saat aplikasi diluncurkan, klik tombol Start Recording di jendela Extended Controls. Hasil bagi akan diperbarui setiap tiga detik. Setelah hasil bagi diperbarui beberapa kali, klik Stop Recording.

55121bab5b5afaa6.png

  1. Secara default, hasil rekaman akan disimpan dalam format .webm. Gunakan menu dropdown untuk mengekspor hasil rekaman sebagai file GIF.

850713aa27145908.png

7. Selamat

Selamat! Di jalur ini, Anda mempelajari bahwa:

  • Proses debug adalah proses memecahkan masalah bug dalam kode Anda.
  • Log memungkinkan Anda mencetak teks dengan berbagai level dan tag log.
  • Stack trace menyediakan informasi tentang pengecualian, seperti fungsi yang tepat yang menyebabkan pengecualian dan nomor baris tempat pengecualian terjadi.
  • Saat men-debug, seringkali seseorang mengalami masalah yang sama atau serupa dengan Anda, dan Anda dapat menggunakan situs seperti StackOverflow untuk meneliti bug tersebut.
  • Anda dapat mengekspor screenshot dan GIF animasi dengan mudah menggunakan emulator Android.

Pelajari lebih lanjut