Developing Android libraries that are easy to use – Part 3: How to do it technically
This is Part 3 of the series Developing Android libraries that are easy to use. You might want to start by reading Part 1 and Part 2.
With this post, I am going to get to the point and state dos and don’ts on how to actually write the source code.
1. Changes in public APIs
The APIs available in your library are going to be public classes and public methods. You have to make the assumption that public APIs are in use and therefore:
- Make backwards compatible changes to public APIs
- Try not to add breaking changes to public APIs
- Clearly communicate breaking changes through changelog, announcements, emails and Slack channel announcements
2. Public API design
Public APIs will have mandatory parameters, variables, and configurations.
The following should be in the design:
- Make mandatory params an IMMEDIATE PAIN, e.g. have them in the constructors or overloaded methods
- Don’t enable adding mandatory params using a builder or setter — This makes it easy for developers to forget to pass the mandatory params
- Don’t enable passing nulls to mandatory params
- Make optional params “hidden” — Provide optional params through setters or overloaded methods where the params are optional
- Don’t have optional params as
@NonNull
without providing overloaded method(s) and constructor(s) where these params do not have to be provided — The developer should be able to use the API without passing the optional param - Fail fast for critical operations and required params. Use the
RuntimeException
if the library cannot work without a prior step (initialization) or a method cannot work without a param. The idea is that the app will fail during QA or developer testing and can be fixed early instead of wreaking havoc for the end-user in production - Don’t handle non-recoverable exceptions in the library. An example is allowing the developer to continue using the library without initializing the library. Another example would be silently failing when the API_KEY is invalid or not provided
- Use checked exceptions for recoverable operations, e.g. use checked exceptions for HTTP error responses
- Create custom exceptions that help the developer understand the problem, location of the problem, and possible solution, e.g. throw an
InvalidJsonException
with line number, column, and phrase where the JSON could not be parsed or where a widget type does not exist - Don’t throw inappropriate exceptions with general messages, e.g. don’t throw the wrong type of exception or provide a non-detailed message
- When exceptions are handled, log the exception and stack trace while providing an explanation for the cause, possible solutions, and any troubleshooting information. An example is Mockito, which provides a lot of troubleshooting information when a method verification fails or an operation on a non-mocked object is performed.
- Log using
error
(or awtf
),info
, ordebug
depending on severity of the error. Include the exception stack trace in the log so that the error is easily traceable to its location in source code. - Hide APIs that are not designed to be consumed by other developers. Use private, protected, and package-private scopes to achieve this. Hiding an API indicates to other developers that:
- Support for the APIs is not provided
- The APIs do not follow conventions
- The APIs require a lot of contextual and technical knowledge to use
- Use of the APIs can easily cause crashes or data-loss
- The APIs might be part of a strict flow
- Do not expose APIs that you do not provide support for e.g.
- Do not expose raw variables without getters and setters that support developers
- Do not expose constructors for classes that have builders — where the class constructor or definition does not support easy usage
- Use the
abstract
keyword for classes that are templates, incomplete, and require certain user-implemented mandatory methods while providing some of the implementation - Do not provide a class with null defaults for required non-null values
- Do not provide empty methods where such methods are required to have features — Use abstract methods to force the developer to implement the methods
This is the end of the series and I hope it helps to get you started. Most of the suggestions in the article are basic conventions that are known to a good number of developers but are not used with intention. There should be clear understanding and intention when deciding to expose, not expose, fail fast, and provide the best experience.