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 3 of 4 of this article. Click here to view the first page.

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.

Introspection and Reflection

Sloths hang out in trees for hours and hours, not seeming to do very much. It’s probably because they’re busy introspecting and reflecting on life. In Kotlin, introspection and reflection are features of the language that inspect objects and call methods dynamically at runtime.

ProGuard looks at the static version of your app, but doesn’t actually run your app, so it cannot know which methods are reachable using introspection and reflection. Remember that ProGuard takes long class names and replaces them with smaller names. If something tries to reference a name at runtime with a constant string, the name will have changed. You can often tell this is happening with the NoSuchMethodException.

You need to tell ProGuard to keep the sections that use reflection, like the @param code you added. You’ll use the implements keyword to keep all classes that extend or implement Parameter. Add the following to the end of the ProGuard rules file:

-keep class * implements org.simpleframework.xml.core.Parameter { public *; }

While you’re at it, go ahead and add a bit more functionality to the app. In the MainActivity.kt file, add this to the onBubbleSelected() method of the BubblePickerListener:

val showDetailsIntent = Intent(picker.context, SlothDetailActivity::class.java)
val pet = map[item.title]
showDetailsIntent.putExtra(SLOTH_KEY, pet)
startActivity(showDetailsIntent)

item.isSelected = false

Build and run the app. You should see the bubbles populated with the different species of sloths.
Final project
Tap on a bubble to reveal more information about each species.
Details view

Congratulations! You’ve set up a cool looking app using the default ProGuard settings.
Happy face
You could stop here, but if you’re interested in how the app size can be further reduced, continue reading.

Enabling Advanced Optimizations

ProGuard provides an advanced optimization profile. It’s not used as the default because it can cause further build and runtime errors. You’ll enable the advanced optimizations by swapping the default proguard-android.txt file out with proguard-android-optimize.txt.

In the build.gradle app module file, replace the proguardFiles line in the debug section with the following:

proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
          'proguard-rules.pro'

Sync Gradle, then build and run the app. The build time will be much longer because ProGuard is performing more analysis and optimizations inside and across methods. It will seem very slow, much like a sloth climbing a tree.

Your app should crash with the following error:
Crash error message
Here, the @ symbol is in reference to an annotated class – in this case, classes annotated with org.simpleframework.xml.*. Add a rule to the end of the ProGuard rules file to keep methods annotated with org.simpleframework.xml.*:

-keepclassmembers class * { @org.simpleframework.xml.* *; }

This time, the error mentions a constructor instead of a missing class, which is why this code uses keepclassmembers to keep class members only.

Build and run the app. You should see your app back up and running again.

Note: This might be a good time to stop and check the APK Analyzer again to see how the size is doing.

Note: This might be a good time to stop and check the APK Analyzer again to see how the size is doing.

You’ve set up a fancy ProGuard optimized app for your Sloth Sanctuary. The Sleuth Zoo is very competitive – they’ve been taking all your ideas and making a profit off of them. How evil! You think that when you release your app, they’re going to analyze your APK and steal your “secret sauce”. Since ProGuard performs obfuscation, this is yet another place that it can help you out.