Chapters

Hide chapters

Real-World Android by Tutorials

Second Edition · Android 12 · Kotlin 1.6+ · Android Studio Chipmunk

Section I: Developing Real World Apps

Section 1: 7 chapters
Show chapters Hide chapters

21. Advanced Debugging
Written by Subhrajyoti Sen

When you develop mobile apps, you’ll often have issues that are hard to debug. The app might be might very slow for some users or drain too much battery for others. Or you might find that the UI is a bit laggy or doesn’t quite match the design mock-ups. Debugging these issues can be tedious. Fortunately, there are tools that make the process easier.

In this chapter, you’ll learn about:

  • Finding and fixing memory leaks using LeakCanary.
  • Using the Memory Profiler to find Fragment and Activity leaks.
  • Examining network calls using the Network Inspector.
  • Finding Wake Locks using the Energy Profiler.
  • Using Layout Inspector to improve your layouts.

You’ll start by looking at memory leaks.

Memory Leaks

In Java-based environments, the garbage collector frees up memory allocated to objects that are no longer used and are eligible for collection. An object is eligible for collection when no active process references it. Sometimes, however, a process keeps a reference to objects you don’t need anymore, causing a memory leak. Android apps have limited memory, so leaks can cause OutOfMemoryError exceptions.

Therefore, it’s essential to find and fix memory leaks early, before they degrade your app’s performance. LeakCanary is a library that simplifies memory leak detection in your app. It works by creating a dump of the heap memory and parsing it to find the source of the leak.

Installing LeakCanary

To install LeakCanary, add the following dependency to your app build.gradle:

debugImplementation "com.squareup.leakcanary:leakcanary-android:2.8.1"

Click Sync now and wait for Gradle to download the dependency.

Adding Obfuscation Support

Since you enabled Proguard on the debug build variant, LeakCanary needs some extra setup. You can skip this setup if you disable Proguard for debugging.

Add the following classpath to the main project build.gradle:

classpath 'com.squareup.leakcanary:leakcanary-deobfuscation-gradle-plugin:2.9.1'

The Gradle plugin above finds the mapping file during the build process and pushes it into the APK. The mapping file enables LeakCanary to deobfuscate the heap dump when it finds a leak.

Now, open the app build.gradle and add the following line above the android block:

apply plugin: 'com.squareup.leakcanary.deobfuscation'

This enables the plugin.

Finally, you need to tell the plugin which build variants could be obfuscated. Do this by adding the following code before the dependencies block:

leakCanary {
  filterObfuscatedVariants { variant ->
    variant.name == "debug"
  }
}

The code above checks if the name of the build variant is debug. It uses the result to inform the plugin that you’ve enabled obfuscation on the debug variant.

With the setup complete, you’re ready to start hunting for leaks.

Detecting Memory Leaks

There’s no secret map that can help you find memory leaks. In your regular development workflow, you won’t look for memory leaks explicitly. Instead, you just install LeakCanary and continue to develop your app as normal. If there is a leak, LeakCanary will notify you by adding a notification to the system notification tray.

Run the app, go through the various user flows and check if LeakCanary notifies you. Remember to check the secret flows too.

You’ll notice that when you visit the Secret Doggo screen and come back to the Details screen, LeakCanary notifies you of a leak.

Note: Remember that you can reach the Secret Doggo screen from the Detail screen by dragging the phone icon to the top.

Repeat the flow to confirm the leak. The notification will be similar to the one below:

Figure 21.1 — LeakCanary Memory Leak Notification
Figure 21.1 — LeakCanary Memory Leak Notification

Finding the Leak Source

In this section, you’ll use the heap dump to find the source of the leak.

Since a heap dump is a long operation, LeakCanary prefers to batch them. LeakCanary, by default, waits for five leaks before dumping the heap. However, you can tap the leak notification to force a heap dump even with one leak.

Tap the leak notification and wait for the heap dump to complete. It will take a few seconds. Once complete, open your device’s app drawer and search for an app named Leaks. LeakCanary installs this app to help you view the leak logs. Open the app.

Figure 21.2 — The Leaks App
Figure 21.2 — The Leaks App

The app lists all the memory leaks LeakCanary detected in your app. At the moment, there’s only one leak, as shown below:

Figure 21.3 — LeakCanary Leak List
Figure 21.3 — LeakCanary Leak List

Tap the list item and open the leak details screen. You’ll get a screen that displays all the details of the leak:

Figure 21.4 — Leak Information Details
Figure 21.4 — Leak Information Details

The image above shows that:

  1. Fragment is leaking MotionLayout. This is your first clue.
  2. References underlined in red are the likely causes of the leak.

Scroll through the leak details until you find something underlined in red. You’ll come across the following section:

Figure 21.5 — Leak Sources
Figure 21.5 — Leak Sources

In the image above, you can see that callFlingXAnimation is underlined in red. It’s leaking its target. Right below that, you can also see that its target points to a FloatingActionButton with the ID call. You’ve found the source of your bug.

The FlingAnimation instance leaks an instance of the Call button, which ultimately leaks the MotionLayout instance.

Understanding the Leak Cause

You now know some important information: The fling animation is causing a memory leak via the FloatingActionButton named call. It’s time to figure out why.

In most cases involving a leaked view, the problem is that an object that holds a reference to the view outlives the lifecycle of that view.

It’s time to take a step back and revisit lifecycles. One significant difference between Activitys and Fragments is their lifecycle. An Activity has a single lifecycle, whereas a Fragment has two lifecycles: one for the Fragment as a whole and another for the Fragment’s view. Because of this, Fragment has different onDestroyView and onDestroy callbacks, whereas Activity has only the onDestory callback.

With this knowledge, you can figure out why the fling animation is leaking the view. callFlingXAnimation has a reference to the call view but you declared it as a global variable. When the user navigates back to the Pet Details screen from the Secret screen, only the view contained inside AnimalDetailsFragment is recreated, not the entire Fragment. Therefore, the app retains the memory allocated to callFlingXAnimation — and it contains a reference to the old view that was destroyed. This is your memory leak.

Plugging the Leak

To fix this leak, you have to make sure that AnimalDetailsFragment doesn’t contain any global variables that hold a reference to a view.

Open AnimalDetailsFragment.kt and look at the following initializations:

private val callScaleXSpringAnimation = SpringAnimation(binding.call, DynamicAnimation.SCALE_X).apply {
  spring = springForce
}

private val callScaleYSpringAnimation = SpringAnimation(binding.call, DynamicAnimation.SCALE_Y).apply {
  spring = springForce
}

private val callFlingXAnimation = FlingAnimation(binding.call, DynamicAnimation.X).apply {
  friction = FLING_FRICTION
  setMinValue(0f)
  setMaxValue(binding.root.width.toFloat() - binding.call.width.toFloat())
}

private val callFlingYAnimation = FlingAnimation(binding.call, DynamicAnimation.Y).apply {
  friction = FLING_FRICTION
  setMinValue(0f)
  setMaxValue(binding.root.height.toFloat() - binding.call.width.toFloat())
}

You don’t want to keep their references in the View, but you do want to limit their scope so the garbage collector can remove them when that scope completes. In this case, the new scope is displayPetDetails(), so just move the initializations to the beginning of displayPetDetails(). This removes the private visibility modifier, which you don’t need for local variables.

displayPetDetails() will now be as follows:

private fun displayPetDetails(animalDetails: UIAnimalDetailed, adopted: Boolean) {
  val callScaleXSpringAnimation = SpringAnimation(binding.call, DynamicAnimation.SCALE_X).apply {
    spring = springForce
  }

  val callScaleYSpringAnimation = SpringAnimation(binding.call, DynamicAnimation.SCALE_Y).apply {
    spring = springForce
  }

  val callFlingXAnimation = FlingAnimation(binding.call, DynamicAnimation.X).apply {
    friction = FLING_FRICTION
    setMinValue(0f)
    setMaxValue(binding.root.width.toFloat() - binding.call.width.toFloat())
  }

  val callFlingYAnimation = FlingAnimation(binding.call, DynamicAnimation.Y).apply {
    friction = FLING_FRICTION
    setMinValue(0f)
    setMaxValue(binding.root.height.toFloat() - binding.call.width.toFloat())
  }

  binding.call.scaleX = 0.6f
  binding.call.scaleY = 0.6f
  //... rest of the method
}

Build and run. Repeat the same workflow as before and verify that there’s no longer a memory leak. Congratulations, you’ve resolved your first memory leak!

Android Studio Profiler

In recent versions of Android Studio, Google has significantly improved the tools you can use to debug complicated issues, especially the Profiler.

The Profiler consists of four main components:

  1. CPU Profiler
  2. Memory Profiler
  3. Network Profiler (now moved to Network Inspector in recent versions of Android Studio)
  4. Energy Profiler

In this section, you’ll learn how to use the Memory, Network and Energy Profilers.

Start by opening the Profiler by selecting View ▸ Tools Windows ▸ Profiler.

Figure 21.6 — Android Studio Profiler
Figure 21.6 — Android Studio Profiler

Finding Memory Leaks With the Memory Profiler

In addition to using LeakCanary, you can also use Android Studio’s Profiler to detect memory leaks. Android Studio 3.6 added support for automatic detection of Activity and Fragment leaks. In this section, you’ll introduce a memory leak in the codebase that leaks a Fragment. You’ll then use the Memory Profiler to find and trace the leak.

Introducing a Fragment Leak

Open MainActivity.kt and add the following global variable before onCreate():

lateinit var currentFragment: Fragment

The code above adds a public variable that holds an instance of a Fragment.

Next, open AnimalDetailsFragment.kt and add the following code inside onViewCreated():

(requireActivity() as MainActivity).currentFragment = this

The code above does the following:

  1. It gets a reference to MainActivity since AnimalDetailsFragment is attached to MainActivity.
  2. It then initializes currentFragment with the current instance of AnimalDetailsFragment.

This is a common source of memory leaks. Exiting AnimalDetailsFragment invokes its onDestroy(), so you’d expect its memory to be garbage collected. But since MainActivity has a reference to the AnimalDetailsFragment instance, the garbage collector can’t collect that instance, which results in a leak.

Detecting and Tracing the Leak

Build and run. Once the app is running on a device, open the Profiler tab and start a session as shown below:

Figure 21.7 — Starting a Profiler Session
Figure 21.7 — Starting a Profiler Session

Select the MEMORY row. You will see a screen like the one below:

Figure 21.8 — Memory Profiler
Figure 21.8 — Memory Profiler

In the image above, you see the two tools that you’ll use to find the leak:

  1. Force garbage collection: This tool can force garbage collection at any point in time. You need this because you can’t determine when garbage collection will occur, so it would be difficult to figure out when to look for a memory leak.
  2. Capture heap dump: This tool will create a dump of the current Java heap, allowing you to analyze the heap’s memory allocation in greater detail.

On the app, open the details page for any pet, interact with the UI, then press Back to return to the previous page.

Back in the Android Studio Memory Profiler, click Force garbage collection, select Capture heap dump and click Record. Recording the heap dump will take a few seconds. Once it’s done, you’ll get a screen like the one below:

Figure 21.9 — Memory Profiler Heap Dump
Figure 21.9 — Memory Profiler Heap Dump

In the heap dump, you can view the different types of objects in the heap and the memory each of them takes up. You’ll also notice that the dump alerts you of a memory leak. Click the alert about the leak.

Android Studio will apply a filter that shows the Activity/Fragment leaks in the dump. In this case, it will tell you that AnimalDetailsFragment is leaking, as shown below:

Figure 21.10 — Example of Memory Leak in the Memory Profiler
Figure 21.10 — Example of Memory Leak in the Memory Profiler

Click the AnimalDetailsFragment row to open the Instance List. This will help you figure out which instances of AnimalDetailsFragment are leaking. Since only one instance is leaking, you’ll have one row in the instance list, as shown below:

Figure 21.11 — Memory Profile Leak Instance List
Figure 21.11 — Memory Profile Leak Instance List

Click the instance to open the Instance Details panel. The panel has two tabs: Fields and References. Choose the References tab. You’ll get a window like the one shown below:

Figure 21.12 — Memory Profile Leaks References
Figure 21.12 — Memory Profile Leaks References

In the window, you can see that currentFragment inside MainActivity has a reference to the leaked Fragment. You’ve successfully found the source of the leak using the Memory Profiler!

As an exercise, try using the lessons from the Memory Leak section above to resolve this Fragment leak. If you get stuck, you can always refer to the final project for the chapter.

Network Inspector

Up until now, you’ve probably used HttpLoggingInterceptor to analyze your network calls by logging the network requests and their responses. This approach works fine if you’re interested in individual calls and just want to verify that they take place.

Now, however, you have a new option. Android Studio introduced the Network Inspector to help you visualize all the network calls taking place in your app, as well as the details of each call.

Note: Network Inspector only supports HttpURLConnection and OkHttp networking libraries.

Why Network Profiling Matters

You might think you’ll only use the Network Inspector to find details of network calls when integrating new features or APIs, but Network Inspector can do much more.

Network Inspector lets you visualize the frequency of the network calls happening in your app. This is very important when it comes to radio battery consumption. If the user is on mobile data, a network call awakens the mobile chip to find a radio signal and make your request go through. After making the request, the chip stays awake for a few more seconds to wait for the response. Every time this happens, the network call wakes up the chip, making it consume more power. And users don’t like apps that consume too much battery, especially when they are on the go.

A good way to save battery is to use the Network Inspector to discover which calls happen frequently. You can then determine if you can defer any of them. For example, an API call to make a purchase has to be instant, whereas you can defer a call to sync profile images of different contacts on a messaging app. You can batch the deferrable calls and perform them in one go. This keeps the chip awake for a single duration instead of waking it up repeatedly.

Another good use of the Network Inspector is finding unexpected network calls that arise from bugs in the code or from third-party libraries. If a library you integrate is making network calls, you want to know about them.

Navigating the Network Inspector

Before inspecting your network activity, you need to disable proguard on the debug build. Remove the leakCanary block from the app build.gradle. Also, set minifyEnabled to false for the debug build type.

Build and run. Open the App Inspection window by going to View ▸ Tools Windows ▸ App Inspection. Next, switch select the Network Inspector tab.

In the app, navigate to the Search tab and search for a pet. In the Network Inspector, you’ll get a screen like this:

Figure 21.13 — Android Studio Network Inspector
Figure 21.13 — Android Studio Network Inspector

In the image above, you can see a few things:

  • The y-axis represents the network speed.
  • The x-axis represents time.
  • A yellow spike represents the network request. The width of the spike represents the time taken by the request, while its height indicates the amount of data transferred.
  • A blue spike represents the server response. The width and height of the spike represent statistics similar to the yellow spike’s.

To dig deeper into how the network works, select a section of the timeline and view the details. Drag your cursor across a part of the timeline to select it, as shown below:

Figure 21.14 — Network Inspector Details
Figure 21.14 — Network Inspector Details

In the image above, you can see that the app has made five network calls and all of them have a status code of 200. You can also see that four requests have the type jpeg, while one has the type json.

The calls with the jpeg types are mostly from Glide. You can confirm this by switching to the Thread View. You’ll notice that the Glide threads have made many calls, whereas the OkHttp thread has made one call.

Figure 21.15 — Network Inspector Thread View
Figure 21.15 — Network Inspector Thread View

Switch back to the Connection View tab and note how hovering over any request will display the URL for that request. Clicking any of the Glide requests will show the image that was downloaded, assuming the download is complete. Clicking the call with the json type will open the details windows for the request, as shown below:

Figure 21.16 — Network Inspector Request Detail
Figure 21.16 — Network Inspector Request Detail

Use this window to find the details of the network request as well as the response. As a bonus, it also auto-formats the response JSON.

Now, you’ll move on to learn more about another tool that can help you reduce your app’s battery drain.

Energy Profiler

The battery usage of an app is a vital metric to track. Users care a lot about their phone’s battery.

There are many reasons an app might be consuming a lot of battery, including:

  1. Frequent GPS location requests
  2. Unbatched network calls
  3. Wake locks
  4. Frequent alarms to schedule tasks

and many more. Android Studio added Energy Profiler to help monitor the energy consumption of components, like CPU, radio and GPS sensors, as well as events that cause battery drain, like alarms and wake locks.

Run the app. Go to the Profiler tab in Android Studio and click anywhere in the ENERGY timeline. This opens the Energy Profiler. Hover your cursor over the Energy Profiler timeline to see a screen like the one below:

Figure 21.17 — Android Studio Energy Profiler
Figure 21.17 — Android Studio Energy Profiler

From the tooltip in the image above, you can see that the CPU and Network energy usage for the app is Light. It also indicates there are no system events that affect the app’s energy consumption.

Finding a System Event

Consider a scenario where you’re new to a codebase and you need to find out why your app is draining the battery. The Energy Profiler is one of the best places to start.

Keeping the Battery Profiler open, explore and interact with the different screens of the app. You’ll notice that, when you enter the Animal Details screen, a red bar appears at the bottom of the Energy Profiler, as shown below:

Figure 21.18 — Energy Profiler Tooltip
Figure 21.18 — Energy Profiler Tooltip

From the tooltip in the image above, you can infer that the red line represents a wake lock in the app. Now, this wake lock should ideally go away once you exit the Details screen, but the Energy Profiler will tell you a different story. The red line continues to show, even after you’ve left the screen. This is a possible source of energy drain.

Clicking anywhere on the red line will display the following message on newer versions of Android Studio:

Figure 21.19 — System Events Has Moved Message
Figure 21.19 — System Events Has Moved Message

Open App Inspection and go the Background Task Inspector tab.

Figure 21.20 — Energy Profiler Wake Lock
Figure 21.20 — Energy Profiler Wake Lock

Repeat the same set of steps to trigger a wake lock so that App Inspection can record it. You’ll see that a wake lock has been recorded as shown below:

In the previous image, onCreate in AnimalDetailsFragment is calling a partial wake lock. To know more about the leak, click on the entry to open the Task Details window, as shown below:

Figure 21.21 — Energy Profiler Wake Lock Details
Figure 21.21 — Energy Profiler Wake Lock Details

In the image above, a callstack points to line 104 in AnimalDetailsFragment. Open AnimalDetailsFragment.kt and go to line 102. You’ll notice the following code, which acquires a wake lock:

wakeLock = (requireContext().getSystemService(Context.POWER_SERVICE) as PowerManager).run {
        newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp::MyWakelockTag").apply {
          acquire()
        }
      }

According to the Energy Profiler, this wake lock is never released. Checking for wakelock usages confirms that.

To release the wake lock when the user leaves the screen, add the following code at the end of AnimalDetailsFragment:

override fun onDestroy() {
  super.onDestroy()
  wakeLock.release()
}

The code above releases the wake lock when the Fragment is destroyed.

Build and run. Start the Energy Profiler and verify that the Fragment releases the wake lock after you exit the details screen.

Now, you’re ready to learn to use a tool that helps you solve problems in your user interface.

Layout Inspector

When you implement your app’s UI, you have to keep many things in mind, like:

  • Making sure there’s no unnecessary nesting in the layouts.
  • Ensuring the layout closely matches the design mocks.

The steps to verify your app’s layout are tedious and time-consuming. To ensure flat layouts, you have to go through all the XML, view by view, and figure out if you can flatten anything. Matching your UI with the design mocks involves comparing them visually and going through each detail of the UI. Even then, you might miss that a TextView is off by 8dp or your button has an extra margin of 4dp on one side.

To simplify the process of finding visual bugs, Android Studio provides a tool named Layout Inspector. It lets you inspect your view attributes after the layout has rendered them on the device and also lets you visualize each layout in 3D. In this section, you’ll use the Layout Inspector to flatten the view hierarchy and make sure your UI matches the design mock.

Starting the Layout Inspector

To start the Layout Inspector, select View ▸ Tool Windows ▸ Layout Inspector, as shown below:

Figure 21.22 — Android Studio Layout Inspector
Figure 21.22 — Android Studio Layout Inspector

This will open the Layout Inspector tab. First, you need to choose the process Layout Inspector will use to extract the layout information. Click Select Process and choose the process named com.realworld.android.petsave from your device:

Figure 21.23 — Layout Inspector Process Selection
Figure 21.23 — Layout Inspector Process Selection

The Layout Inspector will now show the layout that your device displays. Click any of the views in the layout and the Layout Inspector will display the components present in the layout on the Component Tree panel on the left. It will also display all the attributes of the selected view on a panel to the right, as shown below:

Figure 21.24 — Layout Inspector Components Tree
Figure 21.24 — Layout Inspector Components Tree

Finding Unnecessary Nesting

With the Layout Inspector open, visit the Near You tab in the app. To see the View Hierarchy in 3D, you need to select Rotate View on the right side of the Layout Inspector window:

Figure 21.25 — Layout Inspector’s Rotate View
Figure 21.25 — Layout Inspector’s Rotate View

Selecting Rotate View displays the different levels of views in the layout. You can drag your cursor around to view the hierarchy from different angles. Keep doing it till you see a view like the one shown below:

Figure 21.26 — Layout Inspector View Hierarchy
Figure 21.26 — Layout Inspector View Hierarchy

In the image above, look at the views marked 1 and 2. Do you see any differences between them? By the looks of it, view 2 doesn’t add anything new to view 1, which indicates unnecessary nesting.

Click on the view tagged as 2. In the Component Tree window, you’ll notice that you have a LinearLayout inside another LinearLayout, as shown below:

Figure 21.27 - Layout Inspector Nested LinearLayout Example
Figure 21.27 - Layout Inspector Nested LinearLayout Example

Open recycler_view_animal_item.xml and look for the LinearLayout with recycler_view_item. You’ll notice that it’s nested inside another LinearLayout with the same set of attributes.

<LinearLayout
  android:layout_width="match_parent"
  android:ayout_height="match_parent">

  <LinearLayout
    android:id="@+id/recycler_view_item"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView...>
  
    <TextView...>

  </LinearLayout>

</LinearLayout>

Remove the outer LinearLayout so the structure becomes:

<com.google.android.material.card.MaterialCardView>

  <LinearLayout>

    <ImageView/>

    <TextView/>

  </LinearLayout>
</com.google.android.material.card.MaterialCardView>

Build and run, then verify the RecyclerView items on the Near Me screen look the same as before. Congratulations, you’ve successfully used Layout Inspector to reduce an extra level of nesting!

Comparing the Layout With a Design Mock

Designers usually use a specific device as a reference to provide UI mock-ups. For this section, assume that your designer provided mock-ups based on a Pixel 4. You’ll create a new Android Virtual Device based on Pixel 4.

Build and run on the Pixel 4 emulator. Navigate to the Details screen of any pet. Next, open Layout Inspector and click the Load Overlay icon, as shown below:

Figure 21.28 — Layout Inspector Overlays
Figure 21.28 — Layout Inspector Overlays

Click Load Overlay to open a file chooser, then select design_mockup.png inside the starter project. Doing this will lay the mock-up over your layout. Use the slider labeled Overlay Alpha to change the transparency of the overlay.

Change the transparency a few times and try to find differences between the mock-up and your layout. You might notice a few differences in the text below the pet’s description. However, this is expected since the text length varies from pet to pet. Another difference you’ll find is in the position of the Call button, as you can see below. Change the Overlay Alpha to around 50% make it clear.

Figure 21.29 — Layout Inspector Overlay Example
Figure 21.29 — Layout Inspector Overlay Example

Open fragment_details.xml and check the margin you used for the FloatingActionButton. It’s set to @dimen/half_default_margin. Change the margin to @dimen/default_margin, instead.

Build and run the app. Compare the Details screen using Layout Inspector. You’ll find that the Call button is in the correct position, and both you and your designer are happy now.

Key Points

  • Use LeakCanary to find and rectify memory leaks in your app.
  • Avoid holding view references in global variables. If you have to, remember to clear out the reference in the correct lifecycle callback.
  • Find Activity and Fragment leaks using the Memory Profiler.
  • Use the Network Inspector to examine your network calls.
  • Use batching to avoid making frequent network calls.
  • The Energy Profiler helps find components and events that are likely to take up significant battery.
  • Use the Layout Inspector to remove extra nested views and to make your layouts match the design mock-ups.

In the next chapter, you’ll learn about analyzing databases and the different ways you can reverse engineer code.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.