- Things I wish I knew when I started building Android SDK/Libraries
- This article was written by Nishant Srivastava
- The Nuggets translation Project
- Translator: jifaxu
- Proofreader: BoilerYao, Gaozp
Things I wish I knew when releasing android open Source libraries
It all starts with Android developers building their own “cool apps,” and most of them will encounter a series of problems along the way, and some of them will suggest possible solutions.
The thing is, if you think, as I do, that the problem is important enough that there is no known solution, then I will abstract the entire solution in a modular way, and this is an Android library. This way I can easily reuse the solution when I run into the problem again
So far so good. Now you have a library, maybe just for your own use, or maybe you think someone else is going to have this problem, and you release it (open source). I believe (or rather it looks like it) that many people think this is the end of the story.
Wrong! This is where most people usually get it wrong. Your Android library will be used by developers who are not around you and just want to use your library to solve the same problem. The better your library’s API is designed, the more likely it will be used because it won’t confuse users. It should be clear from the outset what you need to do in order for others to start using the library smoothly.
Why is this happening?
Developers usually don’t pay attention to API design when they first release android libraries, at least most of them don’t. Not out of indifference, but because they were new to the game and didn’t have an API design specification to refer to. I was in the same impasse before, so I can understand the frustration of not being able to find relevant information.
I just made an open source library (you can view it at this address) so I have some experience with it. I’ve given you a brief list of things to keep in mind for every Android API library developer (some of them apply to general API design as well).
It should be noted that my list is not complete. It only covers some of the issues I’ve encountered and wish I’d clarified at the beginning, and I’ll update this blog as I have new experiences.
Before we get started, there are some basic questions that everyone faces when building an Android library:
Why did you create an Android library?
The forehead…
Well, you don’t always have to create a library. Think about what value it will give you before you start. Ask yourself the following questions:
Is there a ready-made solution?
If you say yes, consider using an existing solution.
If the existing solution doesn’t solve your problem perfectly, even in that case, it’s best to start with the code fork and modify it to solve your problem.
Pulling requests into existing libraries for your fixes will be a great plus for you and benefit the community as a whole.
If your answer is no, it’s time to start writing android libraries. Then share your work with the world so that others can use it.
What are the packaging options for your artifact
Before you start, you need to decide how to release your artifact to developers.
Let me explain some of the concepts in this blog post. First explain the artifact.
In general software terms, an artifact is something that is produced during software development, either as a related document or as an executable file. In Maven terminology, an artifact is a compiled output, JAR, WAR, ARR, or other executable file.
Let’s look at the options
- Library Project: You must take the code and link it to your Project. This is the most flexible way in which you can modify its code, but it also introduces the problem of synchronizing with upstream changes.
- JAR: Java Archive is a package file dedicated to putting together many Java classes and metadata.
- AAR: Android Archive is similar to JAR, but with some additional features. Unlike a JAR, an AAR can store Android resources and a manifest file, which allows you to share resource files such as layout and drawable.
We have artifact, and then what? Where do these artifacts go?
A joke…
You have several options, each with advantages and disadvantages. Let’s look at them one by one.
Local ARR
If you don’t want to commit your library to any repository, you can create an ARR file and use it directly. Read an answer on StackOverflow to learn how.
Simply put the ARR file in the libs folder (create none) and add the following code to build.gradle:
dependencies {
compile(name:'nameOfYourAARFileWithoutExtension'.ext:'aar')
}
repositories{
flatDir{
dirs 'libs'}}Copy the code
As a result, you can’t get around your ARR files whenever you want to share your Android library (which is not a good way to share your Android library).
Avoid this as much as possible, as it raises many issues, especially the manageability and maintainability of the code base. Another problem is that there is no way to keep your users’ code up to date. Not to mention that the process is long and prone to human error, and we’re just adding a library to the project.
Local/remote Maven repository
What should you do if you only want to use the Android library for yourself? The solution is to deploy your own Artifact repository (learn how to do this here) or use GitHub or Bitbucket as your own Maven repository (here).
Again, this is just a way to publish your own package. If you want to share with others, this is not the way you need to do it.
The first problem with this approach is that your artifact is stored in private repositories, and in order to give someone access to your library you have to give them access to the entire repository, which can lead to security issues.
The second problem is that someone who wants to use your library has to add extra statements to their build.gradle file.
allprojects {
repositories {
...
maven { url '
http://url.to_your_hosted_artifactory_instance.maven_repository' }
}
}Copy the code
To be honest, it’s a hassle, and we all wish things were easier. This is a quick way to release android libraries but adds an extra step for others to use.
Maven Central, Jcenter, or JitPack
Now the easiest way to publish is through JitPack, which you might want to try. The JitPack pulls code from your public Git repository, checks out the latest release code, compiles and generates the artifact, and finally publishes it to its own Maven repository.
However, it has the same problems as the local/remote repository and must be added to the root build.gradle to use it.
allprojects {
repositories {
...
maven { url 'https://www.jitpack.io'}}}Copy the code
You can learn how to publish your Android library to JitPack here.
Another option is Maven Central or Jcenter.
I personally recommend using Jcenter because it is well documented and well managed, and it is the default repository for Android projects (unless someone changes the default options).
If you publish to Jcenter, Bintray offers the option to synchronize your library to Maven Central. Once successfully published to Jcenter, add the following code to build.gradle to make it easy to use.
dependencies {
compile 'com. Making. Nisrulz: awesomelib: 1.0'
}Copy the code
You can learn how to publish your Android library to Jcenter here.
With the basics out of the way, let’s discuss some of the issues you need to pay attention to when writing android libraries.
Avoid multiple parameters
Each Android library usually needs to be initialized with some parameters. To do this, you may need to accept these parameters in the constructor or create a new init method. Consider the following questions as you do so
Passing more than 2-3 parameters to the init() method can make the user feel overwhelmed. Because it’s hard to remember the use and order of each argument, it also opens up the potential for errors like passing int data to String arguments.
// Don't do that
void init(String apikey, int refresh, long interval, String type);
/ / to do so
void init(ApiSecret apisecret);Copy the code
ApiSecret is an entity class, defined as follows
public class ApiSecret {
String apikey;
int refresh;
long interval;
String type;
// constructor
/* you can define proper checks(such as type safety) and * conditions to validate data before it gets set */
// setter and getters
}Copy the code
Or you can use builder mode.
You can read this article to learn more about the Builder pattern. In this article JOSE LUIS ORDIALES discusses in depth how to implement the Builder pattern in your code.
Ease of use
When building your Android library, pay attention to the library’s ease of use and exposed methods, which should have the following characteristics:
- In line with the intuitive
Whatever code does in the Android library should be reported back to the user in some form, either as log output or as view changes, depending on the type of library. If it does something unintelligible, then the library doesn’t work for the developer. Your code should work the way the user wants it to, even if the user doesn’t look at the documentation.
- consistency
The code should be easy to understand while avoiding drastic changes during release iterations. Follow Sematic Versioning.
- Easy to use, hard to misuse
It should be easy to understand in terms of implementation and first use. Methods exposed to users should be adequately checked to ensure that users only use them for what they are supposed to do, and to prevent methods from being misused by users. Provide reasonable defaults and solutions when something you need doesn’t exist. Exposed methods should be adequately checked to ensure that users do not.
In a nutshell
Simple.
Minimize permissions
When every developer is asking users for a lot of permissions, you have to stop and think about whether you really need those extra permissions. This is especially important.
- Request as few permissions as possible.
- Use intents to make specialized programs work for you and return results.
- Enable your functionality based on the permissions you have obtained. Avoid crashes due to insufficient permissions. If you can, let the user know why you need permissions before asking for them. Try to roll back functions without obtaining permissions.
Check whether you have a permission in the following way.
public boolean hasPermission(Context context, String permission) {
int result = context.checkCallingOrSelfPermission(permission);
return result == PackageManager.PERMISSION_GRANTED;
}Copy the code
Some developers may say that they really need a particular permission, but what about that? The library code should be common to all applications that require this functionality. If you need a dangerous permission to access data that the user of the library can provide, then you should provide a method to receive that data. At this point you should not force the developer to apply for permissions that he doesn’t want to apply for. When permissions are not available, the implementation of functionality rollback (which cannot be achieved but is as close to the desired effect as possible) is provided.
/* Requiring GET_ACCOUNTS permission (as a requisite to use the * library) is avoided here by providing a function which lets the * devs to get it on their own and feed it to a function in the * library. */
MyAwesomeLibrary.getEmail("[email protected]");Copy the code
Minimization condition
Now, we have a feature that requires the device to have a certain feature. Typically we define this in our manifest file
<uses-feature android:name="android.hardware.bluetooth" />Copy the code
The problem comes when you write this in android library code, it merges with the app’s manifest file during build and prevents devices without Bluetooth from downloading it from the Play Store. This will cause an app that was previously visible to most users to be visible only to some users, simply by referencing your library.
That’s not what we want. So we have to deal with it. Do not write uses-feature in the manifest file, check for this feature at runtime
String feature = PackageManager.FEATURE_BLUETOOTH;
public boolean isFeatureAvailable(Context context, String feature) {
return context.getPackageManager().hasSystemFeature(feature);
}Copy the code
This approach does not cause Play store filtering.
Provided as an additional feature is not to call related methods or use alternative callback methods in library code when this feature is not available. This is a win-win situation for both library developers and users.
Multi-version support
How many versions are there now?
If you have code in your library that only runs in a particular version, you should disable it on devices with earlier versions.
The general practice is to specify the supported version by defining minSdkVersion and targetSdkVersion. You should check the version in your code to determine whether to enable a feature or provide a fallback.
// Method to check if the Android Version on device is greater than or equal to Marshmallow.
public boolean isMarshmallow(){
return Build.VERSION.SDK_INT>= Build.VERSION_CODES.M;
}Copy the code
Do not log in the official release
Just don’t do it.
Almost every time I’ve been asked to test an app or Android Library project I’ve seen them log everything, and this is a release. (Printing logs in the official version is not necessary, may affect performance, and may be a security issue)
As a rule of thumb, never export logs in the official release. You should use build-variants and timber in combination to implement different log outputs in both the release and debug versions. A simpler solution is to provide a Debuggable flag that the developer can set to turn on or off logging output in the Android library.
// In code
boolean debuggable = false;
MyAwesomeLibrary.init(apisecret,debuggable);
// In build.gradle
debuggable = trueCopy the code
Let users know when errors occur
It’s not uncommon for developers not to post errors and exceptions in the log, and I’ve seen this happen many times. This makes android library users feel very headache in the process of debugging. Although it is said not to export logs in releases, you should understand that errors and exceptions need to be exported in both releases and debug releases. If you really don’t want to print it in a release, at least provide a way for users to enable logging at initialization.
void init(ApiSecret apisecret,boolean debuggable){
...
try{
...
}catch(Exception ex){
if(debuggable){
// This is printed only when debuggable is trueex.printStackTrace(); }}... }Copy the code
When your Android library crashes, show the user an exception immediately, rather than hang up and do something about it. Avoid writing code that blocks the main process.
Exit and disable functionality when errors occur
What I mean by this is that when your code dies, try to check and fix it so that the problem code will only cause some functionality in your library to be disabled rather than the entire APP to crash.
Catch a specific exception
Following up, you can see that I used a try-catch statement in the code above. The Catch statement simply catches all exceptions. There is not much difference between one exception and another. Therefore, specific types of exceptions must be caught based on the requirements at hand. NULLPointerException, SocketTimeoutException, IOException, etc.
Handle poor network conditions
This is important. Be serious!
If your Android library needs to make network requests, it’s easy to overlook a slow connection or a lack of response.
In my observation, developers always assume that the network is smooth. For example, your Android library needs to get a configuration file from the server to initialize it. If you ignore the fact that you can’t download the configuration file when the network is in bad condition, your code may crash because you can’t get the configuration file. If you do network status checks and handle them, you can save your library users a lot of trouble.
Batch your web requests as much as possible and avoid multiple requests. That saves a lot of power. Look at this.
Save data transfers by converting JSON and XML to Flatbuffers.
Read more about network management.
Avoid relying on large libraries
This one doesn’t need much explanation. As android developers know, an Android app has a maximum of 65K methods. If you rely on a large library, there are two unintended effects on applications that use your library.
- You will increase the number of methods in your application greatly, even if your library has only a few methods, but the methods in the library you depend on are counted.
- If the method count reaches 65K due to the introduction of your library, then the application developer will have to use the Multi-dex. Trust me, no one wants to use the multi-dex. In this case, to solve one problem you introduce a bigger problem, and users of your library will switch to another library.
Avoid references to libraries that are not required
I think this is a known rule, isn’t it? Don’t let your Android library swell by introducing libraries you don’t need. But be careful that even if you need dependencies, let your users download those dependencies transitively (having to download another library because they use your library). For example, dependencies that are not bound to your library. So now the question is how do we use it if it’s not bound to our library?
The answer is simple: ask the user to provide the dependencies you need at compile time. Not every user may need the methods provided by this dependency, and for those users, if you can’t find the dependencies, you just need to disable some of the methods. For those users who need them, they provide dependencies in build.gradle.
How to implement it?Check the classpath
private boolean hasOKHttpOnClasspath() {
try {
Class.forName("com.squareup.okhttp3.OkHttpClient");
return true;
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
return false;
}Copy the code
Next, you can use provided(Gradle v2.12 or lower) or compileOnly(Gradle v2.12+) to get the classes defined in the dependency library at compile time.
dependencies {
For gradle version 2.12 and below
provided 'com. Squareup. Okhttp3: okhttp: 3.6.0'
// For gradle version 2.12+
compileOnly 'com. Squareup. Okhttp3: okhttp: 3.6.0'
}Copy the code
Also note that you can only use this method of controlling dependencies if they are pure Java dependencies. For example, if you introduce the Android library at compile time, you can’t reference its dependency libraries or resource files, which must be added before compilation. Only if the dependency is a pure Java dependency (consisting only of Java classes) can it be used by adding the ClassPath during compilation.
Do not block the startup process
Not kidding
I’m talking about not initializing your Android library as soon as the app starts. Doing so will slow down the startup of your application, even if it does nothing but initialize your library.
The solution is not initialized in the main thread, can create a new thread, a better approach is to use Executors. NewSingleThreadExecutor () keep the number of threads only.
Another solution is to initialize your Android libraries as needed, such as loading/initializing them only when they are used.
Gracefully remove methods and functions
Do not remove the public method during version iterations, as this will cause applications using your library to become unusable without the developer knowing what is causing the problem.
Solution: Annotate the method with @deprecated and give deprecation plans for future releases.
Make your code testable
Make sure you have test instances in your code. This is not a rule, but common sense, and you should do this in every application and library you create.
Use mocks to test your code, avoid final classes, have no static methods, and so on.
Writing your public API based on the interface enables your Android library to swap implementations, which in turn makes your code testable. For example, when testing, you can easily provide mock implementations.
Document everything
As the creator of the Android library, you know your code well, but users won’t unless you let them read it (which you should never do).
Document every detail of use, every feature you implement.
- To create a
Readme.md
File and put it in the root directory of the library. - All in the code
public
写javadoc
The comments. They should include
public
Purpose of methodParameter passed in
Returned data
- Provide a sample application to demonstrate the library’s capabilities and how to use it.
- Make sure you have a detailed change log. On the
release
The special version tag in the record is appropriate.
GitHub screenshot of the Release section of Sensey library
This is the Release link for Sensey
Provides a minimalist example application
It goes without saying. Always provide a minimal sample program, which is the first thing developers encounter when learning to use your library. The simpler it is, the easier it is to understand. Making the program look fancy or making the sample code complex defeats its original purpose, which is simply an example of how to use the library.
Consider adding a License
A lot of times developers forget the License part. This is a factor in others deciding whether to adopt your library or not.
If you decide to use a restrictive protocol, such as GRL, it means that whoever changes your code must commit the changes to your code base. This limitation discourages the use of android libraries, and developers tend to avoid using such libraries.
The solution is to use a more open protocol such as MIT or Apache 2.
Read about the protocols and copyright requirements for your code at this simple site.
Finally, get feedback
Yes, you heard me!
At first, your Android library was for your own needs. Once you put it out there for others to use, you’ll find plenty of problems. Solicit feedback from users of your library. Consider adding new features and fixing some problems while keeping the original purpose unchanged based on these suggestions.
conclusion
In short, you need to pay attention to the following points during the coding process
- Avoid multiple parameters
- Easy to use
- Minimize permissions
- Minimum preconditions
- Multi-version support
- Do not print logs in releases
- Give user feedback during crashes
- Exit and disable functionality when errors occur
- Catching specific exceptions
- Deal with the problem of bad network
- Avoid relying on large libraries
- Do not introduce dependencies unless you absolutely need them
- Avoid blocking the startup process
- Gracefully remove functions and features
- Make your code testable
- Well-documented
- Provides a minimalist example application
- Consider adding an agreement
- Get feedback
As a rule of thumb, your library should follow the scouring principle
Simple — Simple and clear expression
It has a Purposeful purpose — it solves a problem
OpenSource – Freely accessible, free protocol
Idiamatic — Conforming to normal usage habits
Logical — clear and rational
I saw this at one point in a presentation by an author, but I can’t remember who he was. I took notes at the time because it made sense and provided the pictures in a very concise way. If you know who he is, leave a comment below and I’ll add his link.
Final thoughts
I hope this blog post helps developers who are working on a better Android library. The Android community benefits greatly from the libraries that developers release every day. If everyone started paying attention to their API design and learned to think for their users (other Android developers), we’d have a much better ecosystem.
This tutorial is based on my experience developing android libraries. I’d love to know your opinion on these points. Feel free to leave a comment.
If you have any suggestions or would like me to add something, please let me know.
Till then keep crushing code 🤓
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. Android, iOS, React, front end, back end, product, design, etc. Keep an eye on the Nuggets Translation project for more quality translations.