At some point as a developer you’ll consider writing a library, whether to perform a specific task, modularize your code-base, or perhaps just to reuse your code in an elegant way. But writing a library is tough work. Hosted by the Bay Android Dev Group, in this talk our own Emanuele Zattin shares some of his best practices for writing libraries in both Java and C/C++. A discussion of API design, CI techniques, and performance considerations, you’ll finish with the right tools for the job.
Why write an Android Library? (0:00)
The first reason to write a library is modularity. You want to be able to separate your code into several logical units, which allows you to have cleaner code and better management of your code. This brings up the second reason to write a library, which is reusability. Once your code is modular, you can actually reuse it in several places, and you can change one piece or replace it with another piece of code much more easily than if you had one monolithic piece of code. Another reason would be “vanity”. If you had a brilliant idea or came up with a nice way to perform a certain task, a library would be the way to share it with the world and make it available for everybody to use.
Why Android in particular, rather than just a Java library? If you’re dealing with anything that is purely Android, then you have no choice. If you’re dealing with things like their UI, the messaging system, sensors of their device, or native code, then you have to develop an Android library.
Step One: Getting Started (3:02)
To get started, just fire up Android Studio and start a new project! But Android Studio doesn’t directly support the creation of a new library, so there are a couple of ways to do this. The first option is to create a new application project, add library module, and then delete the application module. That’s more of a hack, but it’s straightforward and it works.
Another way is to use the command line. The Android Command comes with many, many utilities, and creating a library project is one of them. Options allow you to specify the target with an ID number, the package name, the fact that you want to make a Gradle project, as well as what version of Gradle you want to be able to run. Gradle is an automation system that is very powerful and extremely flxible. You can run plugins for it very easily, more so than on Maven or ANT, which will make your development much easier.
Step Two: Code, code, code! (5:47)
When you create a library, API design is very important. Joshua Bloch is extremely well-known for that; his book Effective Java relates to Java 1.5 but still has valuable tips. He gave an excellent presentation called How To Design A Good API and Why it Matters, and so I wanted to share his points on that.
So, what makes an API a good one?
- It must be easy to learn. You don’t want developers constantly checking the documentation, so the names you use for methods, classes, arguments, and so on need to be self-explanatory when possible.
- The API also has to be hard to misuse, so make it as rock solid as you can.
- It has to be easy to read and maintain because you will have to deal with that code for a long time, especially if it gets popular among other users who will have many feature requests and bug reports.
- The API has to be powerful enough to satisfy your requirements. Sometimes you will receive requests that are against the design principle or the requirements you had, so you must have the courage sometimes to say when you won’t implement something.
- It’s also important that your API is easy to extend. Think of Jenkins: it’s not an API, but it’s a very successful open source project, partly because it’s so easy to extend and write plugins for it.
- Lastly, you have to choose your audience and make your API appropriate to the audience, whether it’s yourself, your team, the company you work for, or the whole world.
Step Three: Test (8:55)
Testing is universally important, but even more so for libraries because they’re going to be used in ways that you had no idea they could be used. Luckily, testing on Android is more or less like testing an Android app. You can use Android TestCase and use all the facilities that the Android framework provides.
One thing that is not so nice about Android testing is that you’re basically stuck to JUnit 3, especially if you can’t use tools like Robolectric, which doesn’t have support for native code. What’s missing from JUnit 3 is the lack of parametric tests, which is very important for libraries because you want to be able to test your methods with a series of parameters that are as big as possible. There is a nice library called Burst by Square, which does exactly that.
Automate your tests! Jenkins is a wonderful tool for this. It offers more than one thousand plugins, some of which are specialised for Android development. I highly suggest the following:
- the Job Config History plugin, which allows you to play with the configuration files and go back in case something breaks.
- the Git plugin, and its many cousins GitHub, GitHub pull request, GitLab, and so on.
- the Gradle plugin, which makes it easy to perform tasks and run automation. Gradle is actually so good at automation that you can run a lot of your logic just inside Gradnel instead of Jenkins.
- the Android Emulator plugin, which does more then just emulation. It’s very important if you need to test things like different screen resolutions and memory availability.
Another way to test is to actually write test apps. It’s usable in many ways: it will validate your use case, test indirectly, and ensure that your apps don’t crash or go unresponsive. To do this, I found this plugin for Gradle, and it basically allows you to send commands to one or more Android devices.
Step Four: Publish (13:58)
When it comes time to publish your library, what do you choose? You have two possibilities: you have Jar, the safe bar, and you have Aar, the Android archive. For those who don’t know, Aar is a format that Google came up with, and it’s made to contain not only your Java classes but also assets and resources so that it’s also possible to make libraries for UI elements and so on. It works great with Android and Android Studio, it’s supported out of the box, but it’s not supported by Ant and Eclipse.
Another bad thing about Aar is that using local Aar files is possible, but there are two different ways to do it and neither are trivial. This is especially important when you have your test apps because you want to use the Aar that you just created during the build process. It all boils down to whether or not you want to support Eclipse. If you have to, then you have no choice but to use Jar.
The problem is that the Android Gradle plugin will generate Aar files out of the box, but not Jar files. Well, actually the Jar file is contained inside the Aar file, so you can just copy that Jar file and rename it. The following code is a Gradle test to do just that: it copies the file, which is always named ‘classes.jar’ inside the Aar, and then renames it to whatever you want.
task generateJar(type: Copy) {
group 'Build'
description 'blah blah...'
dependsOn assemble
from 'build/intermediates/bundles/release/classes.jar'
into 'build/libs'
rename('classes.jar', 'awesome-library.jar')
}
Another thing about publishing is where to publish, and if you plan to go open source this is basically a no brainer. Bintray is amazing - it’s very easy to use, even though it requires a couple more things. It requires a sources Jar, which can be done simply enough, as well as a Javadoc Jar. With that, you basically have to go through all the variants as well as specify the source.
// sources Jar
task androidSourcesJar(type: Jar) {
from android.sourceSets.main.java.srcDirs
}
// Javadoc Jar
android.libraryVariants.all { variant ->
task("javadoc${variant.name.capitalize()}", type: Javadoc) {
description "Generates Javadoc for $variant.name."
group 'Docs'
source = variant.javaCompile.source
ext.androidJar = files(plugins
.findPlugin("com.android.library")
.getBootClasspath())
classpath = files(variant.javaCompile.classpath.files) +
ext.androidJar
exclude '**/BuildConfig.java'
exclude '**/R.java'
}
}
Bintray also provides a Gradle plugin for the actual publishing, which is nice but not so easy to use, especially if you’re new to Gradle. On the other hand, the web interface for publishing is pretty straightforward. You just upload those three files and a bit more information, and then you’re done. So, in the beginning you can use that and then as you get more confident you can actually use this plugin and then automate even more, up to the point where you can let Jenkins do your releases as well.
Advanced Topics
Annotation Processor (19:28)
Annotation processing technologies have become quite popular lately. During compile time, javac will actually find the annotations that you define and operate on them, and so what you can do is basically generate new classes or new Java files. You can write your own annotation processor, which isn’t simple but doable.
There are two things that annotation processing is very good at doing. The first is removing boilerplate, and the second is removing introspection from run time. It’s a very slow operation to perform at run time, so the more you can do at compile time the better to speed up your application. Some popular libraries that user annotation processing include Dagger, Butter Knife, AutoValue/Autoparcel, and Realm.
The bad news is that the Android API doesn’t provide the right package. To solve this, create two Java sub-projects, one for the annotations and one for the actual annotation processor. The annotations need both the processor and your code base, so it needs to be reachable by both. With your library, which is an Android Library project, you’ll have sub-projects. Now you need to modify the Jar file to include everything, not only your classes but also your annotations and annotation processor. You’ll also need to modify the javadoc tasks to add the annotations in order to read the documentation for that.
// Jar
task androidJar(type: Jar) {
dependsOn assemble
group 'Build'
description 'blah blah'
from zipTree(
'build/intermediates/bundles/release/classes.jar')
from zipTree(
'../annotations-processor/build/libs/processor.jar')
from zipTree(
'../annotations/build/libs/annotations.jar')
}
// javadoc tasks
android.libraryVariants.all { variant ->
task("javadoc${variant.name.capitalize()}", type: Javadoc) {
description "Generates Javadoc for $variant.name."
group 'Docs'
source = variant.javaCompile.source
source "../annotations/src/main/java"
ext.androidJar = files(plugins
.findPlugin("com.android.library")
.getBootClasspath())
classpath = files(variant.javaCompile.classpath.files)
+ ext.androidJar
exclude '**/BuildConfig.java'
exclude '**/R.java'
}
}
Native Code (25:10)
Native code is all about dealing with the NDK. This can be scary at times so really a whole workshop is required to learn it, but if you know C/C++ then you can take advantage of it. Gradle and Android Studio are really not ready for NDK, and so when you try to use the NDK module it’ll warn you that the current NDK support is deprecated. They’re currently trying to make it available as soon as possible and use the Native plugin for Gradle. Until that happens, I have to rely on manually setting up tool chains and manually compiling and putting the files in the right places.
So, what to do? One solution is to just ignore the warning, since it still works. One notable problem is that there is no definition of ldFlags, so you cannot specify the flags for the linker. If you need those flags, the other solution is to use the Native plugin. That might become obsolete very soon, but it requires you to handle the creation of standalone tool chains, and the moving around of objects and files from one project to another.
If you’re using Jar files, how do you include your native libraries? That’s just a matter of building it, and can be done through another “jar” task like so.
task androidJar(type: Jar, dependsOn: ['assemble']) {
group 'Build'
description 'blah blah'
from zipTree('build/intermediates/bundles/release/classes.jar')
from(file('src/main/jniLibs')) {
into 'lib'
}
}
Takeaways + Q&A (30:08)
- Embrace Gradle: It can take a while to learn, but it’s worth it.
- Explore Gradle plugins! There are tons of plugins, and most likely there is one that does something for you.
- Automate your tests. Use Jenkins and automate as much as possible.
- Bintray is the go-to solution for publishing if you are open source because it’s really easy to use.
Q: What are some advantages of using the Gradle plugin for Jenkins?
Emanuele: There are several options for running Gradle, including the version you install on your computer or the wrapper, but the plugin makes it easier to navigate when you go through the logs when something breaks.
Q: You mentioned that annotations were special because they were done at compile time, but is there anything special about the run time annotations?
Emanuele: You can use annotations also at run time and perform some introspection there if you want, but it’s much slower than doing it at compile time. In Android it’s extremely slow, so you don’t want to deal too much with annotations.
Q: There are a number of very useful libraries, so are they all large performance hits in Android?
Emanuele: No, I’m just saying that if you can do introspection at compile time then you should do it then. Sometimes, only at run time will you have the information that you need to actually perform some of the tasks you need to do, so you have to just do it then.
Q: Javapoet is a great library that helps with annotation processing, but it’s still annoying to actually read annotations and navigate hierachy of classes. Do you know of anything good for that?
Emanuele: Unforunately not. Actually, javapoet only helps you in the writing of the new class that you are generating. If you don’t have to write a lot of code, you can also use templates and not even have to use an extra plugin. The problem is actually introspecting that object, for which there are a lot of limitations. You can get the class, the annotations of the class, and so on, but you cannot introspect inside the method itself. For that kind of purple you can play with bytecode manipulation, which is scary but possible, and a plugin called Morpheus that can help you do that.
Q: When I was trying to create a library, I was having problems in Gradle with getting the path right so that it was clear that I was making a library and not an app. Have you seen that before?
Emanuele: Yes, you have to deal with the Google Android Gradle plugin and dig into it to see where they store the information you need and then play with that.
Q: Which is your preferring Jenkins provider, or do you always run your own locally?
Emanuele: I run it myself. The way I do it is that I have a master that doesn’t have any executors, and then I have a bunch of local slaves with all the executors. The master can then be a very small machine but doesn’t need to be very powerful, and doesn’t have to do any compilation.
Receive news and updates from Realm straight to your inbox