Android library development best practices guide

le 13/09/2017 par Rémi Pradal
Tags: Software Engineering

As Android developers, we are used to having to integrate many libraries in our applications. It can be from quite small .jar files to huge .aar archives which embed multiple screens. Sometimes the integration of these libraries goes smoothly but in some cases, it can be quite painful and lead to the addition of some “hacks” into the app in order to integrate it properly. In some other cases, it can also lead to time-consuming exchanges between the person who integrates the library and the one who developed it.

In this article, I will try to give Android library developers a few tips to avoid common pitfalls you might fall into. It can be of great benefit to you,

  • If you are developing an open source library : people will tend to not use your work if they find it hard to integrate in their own project.
  • If you are developing a private library in a business context, you and the team which will have to integrate the library will lose a lot of time (and money) if the integration is too complicated.

I will not develop in this article some good practice aspects which are “obvious” and are generally followed by any serious Android library developer : clean versioning, good documentation, regular maintenance ...

Provide a convenient way to retrieve the library file

Probably one of the most annoying things when it comes to integrate a new library in a project : discovering it is only possible to integrate the library thanks to a manually downloadable .jar/.aar that the developer has to put in the /libs folder of his project.

Here are a few reasons (list not exhaustive!) why, as a library developer, you should avoid delivering libraries by sending directly the artifacts:

  • Your library versioning will be lost : the user will have no other way to know the library version he is using than renaming it. It is obviously error prone.
  • Large files are not meant to be committed in the versioning control system (VCS) of an app. It will increase the overall code base size.
  • Version bumps are painful for the integrator. Indeed to perform the version upgrade the developer will have to check on a pre-established channel if there is a new version, download it and put the new file in the project. It induces friction so the integrating developer will upgrade your library version less often..

That being said, you now understand why it is necessary that you expose your binary files in a way that avoid all the disadvantages listed above : a binary repository manager (often improperly named “maven repository”). Having to remind such a thing might sound commonplace but in my experience, for private libraries, I had to face many situations where the binaries were sent directly by emails!

If you are exposing your library to everyone, the simplest way would be to host your library on a repository like maven central, jCenter or even jitpack which let you upload artifacts easily, for free and are repositories which are usually already used in any Android application. If you are developing a library which will be diffused internally then you will have to upload your artifacts on a private repository and provide repository address and credentials to the developers who will have to integrate your library on their project.

In the case where you have the infrastructure to do so, you can install a binary repository manager on a server you own and administer it by yourself. If you do not want to bother with maintaining it, SaaS private binary repository manager exists which will provide excellent service for a reasonable price. You can find different repository systems on this quite complete page. You can compare their different characteristics and whether a SaaS version exists or not.

Think about how your library will integrate with Proguard

Proguard is a tool widely used but often misunderstood. Ignoring how your library will work when Proguard is activated in the integrating application might lead to some compilation error or even crashes at runtime.

To understand properly this point, let’s make a quick recap of how Proguard works when a library is integrated into a project using Proguard features:

  • The library binary (.aar/.jar) is generated and then exposed to integrators.
  • During the app compilation, the output byte code is generated, containing the app bytecode and all the libraries bytecode indistinguishably.
  • If Proguard is enabled, it will apply the Proguard rules of the app over the app bytecode AND the libraries bytecode indistinctly.

A first solution to make sure that your library will work properly when integrated in an app is to provide a set of Proguard rules that the integrator will have to put in its own set of proguard rules. You can easily verify that these rules are appropriate by creating a sample application integrating your lib and applying the “standard” Proguard rule.

An easier solution (and more convenient for the developer integrating your library) is to take advantage of the consumerProguardFiles property. This property, as stated in the documentation, allows you to specify a set of Proguard rules which will be merged to the integrating app set of Proguard rules. This is similar to the first solution, except that the integrating developer do not have to manually add the rules (therefore there is no risk that he forgets to do so). Keep in mind that the rules you set in this file will then be applied to the entire app + libraries bytecode, so put very specific rules to prevent impacting bytecode not related to your library.

In some cases, for instance when you do not want anyone to be able to easily read your library bytecode, you want to apply a first Proguard pass before exposing your library binaries. In that case, you can use the “regular”  proguardFiles property. Of course you will have to take great care of the rules you add in this file (for instance you do not want your public APIs to be obfuscated). If you do so then your library bytecode is subject to two Proguard passes : a first time by you when you generate the library binary and a second time after being integrated in the app. This example of proguard files shows what can be the appropriate rules when you want to process your library before exposing it.

The gradle snippet below is an example of a proguard configuration of an .aar library with a Proguard pass before exposing the binary :

Voir le lien github

Be sparing about the libraries you include in your own new library

This point is self-explanatory. If you include too much libraries in your own library the developer who will integrate it in his project might face some issues :

  • If you add many libraries (particularly libraries whose size is big such as guava) the size of the generated app will increase. This is particularly true if the integrating app has not properly set up with Proguard. In that case even if your library uses a single method of another library the whole bytecode of this transitive library will be embedded in the final apk.
  • Embedding multiple libraries increases the risk of having dependency conflict. If the library you embed in your own is also used by the project integrating yours (or if it is used by another library integrated as well) with a different version, then it might lead to dependency conflict. I will not explain what are the solutions to face these kind of problems as it could be an entire article alone. If this kind of problem occurs, the developer integrating your library will lose some time fixing it.

Prevent resource names conflicts

It is common when you develop an Android library to have the need of defining your own resource (string, integer, theme…). If this is done the wrong way, it can lead to some issues when the library is integrated. Indeed, what happen if the resource name that you use is redefined elsewhere in application?

Before giving some insights on how is it possible to tackle this issue, let’s make a quick recap of Android’s resource conflict merging policy :

  • If there is a conflict between a library resource and the app resource, the project will simply do not compile at all. In that case, the developer will have to rename its resource to make sure there is no more conflict. This is obviously problematic because a library integration should not have this kind of impact on the application code base.
  • If there is a conflict name between two libraries integrated in the app, then the resource which will be eventually included in the apk is the one belonging to the library defined first in the gradle file. This is even more problematic that the previous case because the developer might miss the fact that a resource is being overloaded and then it can lead to some unexpected behaviors.

The solution to this problem is to make sure that all the resources you define have a unique name. The recommended way is to use a scheme which will ensure that the resource name will probably be unique. The android gradle plugin provides a convenient way of having this kind of scheme : the resourcePrefix parameter. If you set a string to this parameter you will be “forced” to have all your library’s resource names be prefixed with the parameter you gave. This is a good way to keep in mind to use a unique pattern to avoid name conflicts. Meanwhile, do note that you will still be able to generate your .aar if the resource names do not comply with the prefix you set : this safeguard only warns you in Android Studio if you do not follow the rule you set.

The following gradle file extract shows how to define resourcePrefix :

Voir le lien github

This screenshot is an example of the kind warning you have if your resource name does not comply with the rule you set :

resourcePrefix utilization

Do not make your APIs too invasive

When you design the way your library will be used it is important to keep in mind that your library will be integrated with many others into a project. So solutions that might seem elegant or are more convenient for you can, in a concrete complex project, be at best constraining and in the worst case make the integration simply impossible.

A typical example of invasive library is a library, in order to be used, which requires that the Android Application object is inherited from a particular BaseApplication class of the library. This is quite a bad choice because in the case where we have to integrate another library who has made the same design choice, then we are stuck. Maybe this design is the one that leads to the fewer code change in the integrating project but it is too invasive.

In more general terms, favor a design which allows integrating developers to isolate the different calls of your library.

Conclusion

In this article we have seen some points that you should keep in mind when developing your library. This will prevent you from having to modify your binaries after their integration in applications.

Of course, this list is not exhaustive at all. You can find other tips and tricks for developing an Android library on the related Android Developer page and great advices for designing your API here.

Last but not least, do not forget that the most important thing is probably to maintain an easy way to communicate with the people using your library : doing so you will contribute to increase the “Developer experience” of your library.