Getting Started with ProGuard

In this Android tutorial, you’ll learn how to strip down your app size by making use of ProGuard – an app shrinking and obfuscation tool. By Kolin Stürt.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Adding BubblePicker Code

In the res/layout/activity_main.xml file, replace the second TextView (descriptionTextView) with the following:

<com.igalata.bubblepicker.rendering.BubblePicker
  android:id="@+id/picker"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  app:backgroundColor="@android:color/white" />

This adds the BubblePicker to the main layout. In the MainActivity.kt file, add the following to the setupBubblePicker() method:

picker.bubbleSize = 50
picker.centerImmediately = true
picker.adapter = object : BubblePickerAdapter {

  val colors = resources.obtainTypedArray(R.array.colors)
  val titles = listOf("1", "2", "3", "4", "5", "6")

  val multiplier = 2
  val modulus = 8
  val addition = 1

  override val totalCount = titles.size // 1

  override fun getItem(position: Int): PickerItem { // 2
    return PickerItem().apply {
      title = titles[position]

      val start = colors.getColor((position * multiplier) % modulus,0)
      val end = colors.getColor((position * multiplier) % modulus + addition,0)
      gradient = BubbleGradient(start, end, BubbleGradient.VERTICAL)

      textColor = ContextCompat.getColor(this@MainActivity, android.R.color.white)
    }
  }

}

picker.listener = object : BubblePickerListener { // 3

  override fun onBubbleSelected(item: PickerItem) {

  }

  override fun onBubbleDeselected(item: PickerItem) {

  }

}

Note: This tutorial assumes you’re familiar with handling imports. If you don’t have on-the-fly imports set up, import by pressing option + return on a Mac or Alt + Enter on a PC while your cursor is over the item that needs to be imported.

Here’s what’s going on in the updated method:

  1. You are overriding totalCount to tell the BubblePicker how many bubbles there will be.
  2. You’re overriding getItem() to return a customized PickerItem.
  3. You set up the BubblePickerListener to handle selecting a bubble.

Finish off the implementation by adding this to the end of onResume:

picker.onResume()

Add this to the end of onPause:

picker.onPause()

Build and run the app. Uh oh — the app crashes with a NullPointerException! Note that several methods in your stack trace are obfuscated – the names are changed and minified. This is one of the key features of ProGuard.
Crash error message
Sad face
Check the output log to narrow down what the problem is. You can see in the Run tab that it has something to do with “GL” and “onDrawFrame” in the BubblePicker library.

Note: It’s always good practice to add sufficient logging in your catch statements, nullability checks and error states. With ProGuard, this is crucial – in the event of a problem, it will help lead you or other developers to the root of the issue, especially when the method names in stack traces are obfuscated.

Debugging with ProGuard Output Files

When ProGuard finishes running, it produces 4 output files. They are:

  • usage.txt – Lists code that ProGuard removed.
  • dump.txt – Describes the structure of the class files in your APK.
  • seeds.txt – Lists the classes and members that were not obfuscated. Good to verify that you have obfuscated your important files.
  • mapping.txt – The file that maps the obfuscated names back to the original.

You can use the mapping file to find the culprit of the crash.

Run the APK Analyzer again, then select the classes.dex file. Your crash points to obfuscated code looking something like org.a.d.l.a. The single characters you see may vary from this example but you can follow the directory path of the characters:
Proguarded directories
At first, it’s not clear what the directories are. Click the Load Proguard mappings… button to map the obfuscated names back to the original.
Load Proguard mappings button
Select the mapping.txt file in the debug folder and click OK.

Toggle the Deobfuscate names button to the left of the Change ProGuard mappings… button to switch between obfuscated and deobfuscated code. You can trace the problem down to the jbox2d library.
Unobfuscated code
You need to preserve jbox2d so that ProGuard doesn’t muck with it.

There are a few more things you should know about the mappings file:

  • Every time you make a release build, the mapping.txt file is rewritten. That means you must save each copy with each release of your app. That way, when you receive an obfuscated stack trace for a particular app release, it will be useful.
  • You can upload your mapping.txt file to Google Play to deobfuscate your crash stack traces.
  • If you’re using Fabric, a deployment and crash reporting tool from Google, instructions for uploading your mapping.txt file are here.

Adding Keep Rules

Keep rules tell ProGuard not to obfuscate certain parts of your code. Some options are:

  • keep – Preserve entire class and class members.
  • keepclassmembers – Preserve the class members.
  • keepclasseswithmembers – Preserve all classes that have a specified member.

ProGuard rules are written in a specific template format. It’s best practice to use explicit keep rules, rather than keeping the entire class. Instead of preserving the entire BubblePicker library, you only need to keep org.jbox2d and its sub-packages. The format is the same as the dontwarn rules.

Add the following to the proguard-rules.pro file right after the dontwarn rules you added:

-keep class org.jbox2d.** { *; }

Inside the curly braces, you told ProGuard to match any method name.

Build and run the app. When your app loads up, you should see the bubbles floating around on the screen.

App with bubbles

Note: If you’re sharing your code, write keep rules as you write your code and be sure to publish them on your site or GitHub README page so other developers can easily use your code without any problems.

Now you’ll set up some real sloth data.

Adding Data to the BubblePicker

There’s a file included in the res folder of the project called sloths.xml. Since it’s in XML format, you’ll need a way to get that data into Kotlin objects. You’ll use another third party library included with the Retrofit package called SimpleXML.

Add this code to the list of dependencies in the build.gradle app module file. You can exclude groups and modules that you’re sure you won’t use:

implementation ('com.squareup.retrofit2:converter-simplexml:2.0.0-beta3'){
    exclude group: 'xpp3', module: 'xpp3'
    exclude group: 'stax', module: 'stax-api'
    exclude group: 'stax', module: 'stax'
}

Sync Gradle, then build and run the app.

You’ll have some new warnings: a reference to org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement, and many variations of warnings about javax.xml.stream.**.

Compiler warnings

You won’t be using Java’s XML stream feature. For this tutorial’s purpose, it’s safe to ignore the animal_sniffer warnings. Add the following to the proguard-rules.pro file:

-dontwarn javax.xml.stream.**
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement

Select Build ▸ Make Project. This time the build should succeed.

Open the SlothViewModel.kt file and replace the loadSloths() function with the following:

private fun loadSloths(resources: Resources) { // 1
  val serializer = Persister()
  val inputStream = resources.openRawResource(R.raw.sloths) // 2
  val sloths = serializer.read(Sloths::class.java, inputStream) // 3
  sloths.list?.let { theList ->
      val map = theList.associateBy( { it.name }, {it})
      this.sloths = map.toSortedMap()
  }
}

Here:

  1. You removed the explicit return type for the function.
  2. You opened the sloths XML file an an InputStream.
  3. You retrieved a list of Sloths by invoking read().

The XML parser knows how to map the XML fields to your Kotlin objects by using annotations. In the Sloths.kt file, add this annotation above the Sloths constructor:

@Root(name = "sloths", strict = false)

This tells the parser to look for the root node called “sloths”.

Then add annotations above the val list property in the constructor:

@field:ElementList(entry = "sloth", inline = true)
@param:ElementList(entry = "sloth", inline = true)

@field and @param let the parser know to look for “sloth” items.

The same will need to be done for the Sloth class. In the Sloth.kt file, replace everything after the import statement with this:

@Root(name = "sloth", strict = false)
data class Sloth constructor(
  @field:Element(name = "name")
  @param:Element(name = "name")
  var name: String = "",

  @field:Element(name = "realName")
  @param:Element(name = "realName")
  var realName: String = "",

  @field:Element(name = "imageResource")
  @param:Element(name = "imageResource")
  var imageResourceName: String = "",

  @field:Element(name = "description")
  @param:Element(name = "description")
  var description: String = "") : Serializable

In the MainActivity.kt file, add a ViewModel variable right above onCreate():

private val viewModel: SlothViewModel by lazy {
  ViewModelProviders.of(this).get(SlothViewModel::class.java)
}

Add a call to get the sloth data as the first line of setupBubblePicker():

val map = viewModel.getSloths(resources)

Replace the line that reads val titles = listOf("1", "2", "3", "4", "5", "6") with the following:

val titles = map.toList()

And finally, replace the title = line with this:

title = titles[position].first

Build and run the app. Notice the NoSuchMethodException crash.

Crash error message

You know it must be the code you just added. Doing similar debugging to what you did earlier, you can narrow the problem down to the Parameter object inside SimpleXML. This time you’ll take a deeper look at the problem. SimpleXML works by loading XML entities presented at runtime, then instantiates Kotlin counterparts. Kotlin can only do this by using introspection and reflection.