Now that we have already introduced some of the elements involved in the process of building mobile applications for Android devices, we will dive deep into some of them.
To build an application we do not only need source code, we also need some additional files such as images, text, layout description files, or others. Those additional files are what we call resources. Our project will contain a res directory together with our src
directory. Inside this directory, we can find resources needed by our application.
To make our application as easy as possible to maintain and add new features to we should externalize resources such as application images and texts from the source code. It will keep our application code simple and we can easily add support for new countries or new languages for example. As explained earlier, we can have multiple resources and, thanks to the resource qualifiers, the Android device will pick the proper resource based on its properties in runtime.
For static files that need to be included without any kind of filter, you can use the assets folder. Everything there will be included into the final application. To access these assets, we will have to use the AssetManager
class, but we will cover this later. Visit http://developer.android.com/reference/android/content/res/AssetManager.html for more information on the AssetManager
class.
In order to keep the code of the application tidy and uncoupled, we can identify parts of the application, which might be even reused later in some other applications, that can be completely decoupled and exposed as a module. Gradle, the build system, allows us to have several modules and establish dependencies between the main application and those modules. To make it more interesting, these modules can be extracted as independent projects and have an independent release cycle as though we are using a third-party library. Modules can be considered as Android libraries or Android library projects. Instead of having third-party dependencies pulled from remote repositories, we have them inside our project. Visit https://developer.android.com/studio/projects/add-app-module.html for more information on modules.
The Android Manifest file is our application descriptor. Here, we can find all the activities, services, content providers, and broadcast receivers defined in our application, the list of permissions required, which icon to use on the Application
menu, and a lot of other configurations. For an exhaustive list of configurations, check the official documentation at http://developer.android.com/guide/topics/manifest/manifest-intro.html.
When the application is compiled, the manifest is transformed into a binary format. In order to see the manifest of a compiled APK, we can use the following tool, included with the Android SDK:
<Android SDK path>/build-tools/23.0.2/aapt dump badging <apk file>
On Mac OS, Android SDK will be installed inside your local Library
directory, as ~/Library/Android/sdk/
.
Gradle (http://gradle.org) is the new build system recommended by Google. On previous versions, and before the introduction of Android Studio, Ant was the default build system used. Gradle is a DSL, or domain-specific language, that allows scripting for more complex build processes or configurations. You can do lots of things with it, but some of the most used parts of the Gradle build system are dependency management and the option to build different flavors (or configurations) of your application.
Dependency management is not only useful for managing internal modules, but also for managing external third-party libraries that we will use in our application.
If, for instance, we want to include Retrofit (HTTP client,
http://square.github.io/retrofit/) and Picasso (the image downloading library, http://square.github.io/picasso/), we will have to add the two dependencies to our build.gradle
file under the dependencies keyword:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.squareup.retrofit2:retrofit:2.0.0-beta3'
compile 'com.squareup.picasso:picasso:2.5.2'
}
We have just added the last two lines to the dependencies that Android Studio puts in by default.
Let's now discuss flavors, a very powerful way to build multiple configurations out of the same source code. If we take a look at Google Play, we will notice that there are many apps and games with a free version, usually limited or with ads, and a full or pro version.
Instead of duplicating all the code and having to build two different applications, adding two different flavors to your application allows you to have two or more different builds out of almost the same source code. Each flavor can have a specific source code and resources that will differentiate it from the other flavors, but at the same time each flavor will share the common source code and resources with all the others.
Let's modify our test application, the one created by the Android Studio Wizard, to add two flavors.
First, we need to add the two flavors to our build.gradle
file. Here is the resulting file with the two flavors and the dependencies we introduced in the previous topic.
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"
defaultConfig {
applicationId "com.packt.rrafols.example"
minSdkVersion 14
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
productFlavors {
free {
applicationId "com.packt.rrafols.example.free"
}
pro {
applicationId "com.packt.rrafols.example.pro"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-
android.txt'),'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.squareup.retrofit2:retrofit:2.0.0-beta3'
compile 'com.squareup.picasso:picasso:2.5.2'
}
As you can see, there are two different flavors, free and pro, with different applicationId
objects, so we can have both installed on any device at the same time.
Now, we go to Android Studio and create these directories:
app/src/free/java/com/packt/rrafols
app/src/pro/java/com/packt/rrafols
The shared part of the code will remain in app/src/java
and the specific code for each flavor will go into its own directory. Let's create a dummy class named ApplicationName
inside the directory we created for the free flavor with the following content:
package com.packt.rrafols;
public class ApplicationName {
public static final String APPLICATION_FLAVOR = "free";
}
We will do the same for the pro flavor:
package com.packt.rrafols;
public class ApplicationName {
public static final String APPLICATION_FLAVOR = "pro";
}
Now, we will have two classes with the same name but, no need to worry, only one of them will be included in our build, depending on which flavor we are building.
To show that this is working, let's modify the MainActivity
class to change the title to the APPLICATION_FLAVOR
value:
package com.packt.rrafols.example;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import com.packt.rrafols.ApplicationName;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getSupportActionBar()
.setTitle(ApplicationName.APPLICATION_FLAVOR);
}
}
We can choose which flavor to build from the Build Variants tab in Android Studio.
We can also achieve the same effect by having different resources in each flavor. Let's change our MainActivity
layout, as follows:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.packt.rrafols.example.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_flavor" />
</RelativeLayout>
Next, let's create the property in the res/values/strings.xml
directory:
<string name="app_flavor">No flavor specified</string>
To customize this message, we will have to create two resource folders, one for each flavor like we did before for the source code:
app/src/free/res/values
app/src/pro/res/values
In the free flavor resource folder, we will create a new strings.xml
file with the following content:
<resources>
<string name="app_flavor">Application flavor: free</string>
</resources>
Also, we will do the same for the pro flavor:
<resources>
<string name="app_flavor">Application flavor: pro</string>
</resources>
Properties from our flavor will be merged with the default properties. Those that are equal will be overwritten by the flavor-specific value and the application will show the selected flavor message.
As an alternative to Gradle, if you have a very big application and the build time is one of your bottlenecks, you might even try Buck. Buck (https://buckbuild.com) is the build system developed by Facebook. It highly focused on build performance although the latest versions of Gradle are improving on performance, and Gradle is the tool selected by Google.
ProGuard is a code obfuscation tool. The Java compiler does not do a good job of optimizing the resulting class files when compiled from Java sources. By default, it preserves all the variable names, method names and code is quite easy, not to say straightforward, to decompile to high-level code once again. There are many tools out there that allow us to do that, for example, smali (https://github.com/JesusFreke/smali) or dedexer (http://dedexer.sourceforge.net/). To make it difficult for anyone else to peek into our code, it is always recommended that we run ProGuard to obfuscate (or minify) the compiled version of our application. Not only will it replace all our class names, methods, and variables with single-letter strings (a, b, ..)
, but it will also slightly optimize the compiled bytecode and make it more complex (although not impossible) for hackers to hack our application. We should not rely only on ProGuard for the security of our application, but we can say that ProGuard is an additional barrier that we add to our application.
To enable ProGuard, we have to make a small change to our build.gradle
file in our app folder:
release {
minifyEnabled false
proguardFiles getDefaultProGuardFile('proguard-android.txt'),
'proGuard-rules.pro'}
By just changing minifyEnabled to true
, we are telling Gradle that it has to run ProGuard on the release build.
ProGuard needs to be configured properly to do a good job; we cannot just obfuscate the whole enchilada. The ProGuard configuration file tells ProGuard, among other things, which classes or methods need to be preserved. There are some methods that need to be retained as Android expects them to be there and, when using third-party libraries, always double-check the ProGuard requirements of those libraries, as they might come with their own set of rules. As an example, if we use retrofit, which will be introduced in Chapter 5, Remote Data, we will have to add the following set of rules:
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Signature
-keepattributes Exceptions
We can add these rules to the proguard-rules.pro
file, as it is specified in the build.gradle
section that we modified before enabling ProGuard.
Even though Android does not use the bytecode produced by the Java compiler directly, prior to Android 5.0, we had the DALVIK VM, which converted the java bytecode to DALVIK (DEX) bytecode, and now, with the introduction of ART, bytecode is compiled into native code for the sake of performance. So, to sum this up, all the resulting code, either DEX or native, is produced from the original Java bytecode, so optimizing it will definitely make a small improvement to the final code that will be run by the Android device. For more information, check out what I discussed at the talk in Droidcon, Amsterdam in late 2014 (http://blog.rafols.org/wp-content/uploads/droidcon_nl_android.pdf).
Another issue where ProGuard might help is with the 64k method limit. There is a design flaw on the DEX file specification that only allows 65536 methods to be referenced on each DEX file (http://developer.android.com/tools/building/multidex.html).
This is much of a problem for a simple application, but if we start adding lots of third-party libraries or our application is relatively complex, it can be a problem. For example, if we have to include the whole Google Play Services, it will already add 38k methods to our application. Now, Google Play Services is split into several smaller packages, and we can include only the parts that we require. Nevertheless, if we enable minification or, basically, ProGuard, it will remove all unused methods from both our application and the libraries we include, drastically reducing the total number of methods we will end up having in our application.
If you are concerned about security and would like to go the extra mile, I suggest that you go for DexGuard (https://www.guardsquare.com/dexguard); it is not free, but has more features than ProGuard and it is developed and maintained by the same company as ProGuard.