I’ll write the 1 out front
For a long time, most of the articles in the tech world dealing with Android Library are about how to publish to Maven/Jcenter, but there are very few articles on how to write a standard and useful Android Library.
Android over the years a variety of open source libraries emerge in endlessly, the domestic many developers have made open source libraries will generously of their some results released out, but when we are interested to want to try out these libraries, but often encounter “reference” “rely on” will be “conflict” “API calls” all sorts of problems, such as there are many problems, It’s actually the author of the library himself.
Meizu’s intermodal SDK started to be approved in August last year, and partners began to access it gradually in October. After more than half a year, more than 50 CP applications have been connected, during which the version was only upgraded once, and the rest of the time, it has been running steadily and seeking new partners. We also received a lot of feedback from cp developers, but most of them said that the library was very easy to install and use, which I was very pleased with.
In fact, I had been a solo developer for over two years before I started working, and that experience gave me a real insight into what developers like and don’t like. If every Android Library author can think about the design and implementation of the Library from the perspective of the user, the Android Library will not be bad.
So I’m going to share with you some of our practices in the following content, some of which are also after stepping on the hole to fill in, I will write them out, I hope to help you in the future development work.
##2 Specification engineering structure
A standard Android Library project should consist of a Library module and a demo module.
The demo module has two advantages:
-
Convenient development of their own debugging, their own library, their own writing process to keep tasting salty to ensure that “really fragrant”
-
After the library is released, apK can be compiled for people to experience first
Note that the Build. gradle of the Demo module should make a distinction between referring to the Library project directly if it is built in debug mode and referring to your released version if it is built in release mode.
Android developers have experienced the experience of “developing and debugging will be fine, but the official version will have problems”. Using this reference mode, if you release a library with problems, you can immediately discover them when compiling the Demo APk. Build. gradle makes it easy to distinguish between build.gradle references:
debugImplementation project(':library'// The debug version directly references the local project releaseImplementation'Remote library address'// The release version references the remote version for final testing to find problemsCopy the code
##3 Instructs access users to quickly rely on all AArs
If your library cannot be published to Mavan Central, there may be multiple AArs that need to be added to the project when providing an SDK to others. We often see a practice on the web that requires the user to copy the AAR file to the project and then modify build.gradle to declare participation. The user must read the aar name carefully because build.gradle is required to declare it clearly.
In fact, your accusers are not obligated to figure out your AAR name. It’s tiring enough picking up your library, why would you want someone to read your name carefully? Here’s a recommended way to do it:
1. Ask your accusers to create a libs/ XXX directory under the APP module of their project, and copy all aar provided by you into it. This XXX can be the name of your channel, and the AAR below it will be yours from now on, separated from other ones.
2. Open build.gradle for your app and declare it on the root node:
repositories {
flatDir {
dirs 'libs/xxx'}}Copy the code
Add the following statement to the dependencies{} closure:
/ / recursionDef xxxLibs = project.file(' libs/ XXX ')libs/xxx') xxxLibs.traverse(nameFilter: ~/.*\.aar/) { file -> def name = file.getName().replace('.aar', '') implementation(name: name, ext: 'aar')}Copy the code
Alternatively, we can refer to the first line of the dependency and just do it in one step with the following code (thanks for being young in the comment section @then) :
implementation fileTree(include: ['*.aar'], dir: 'libs/xxx')
Copy the code
In this way, gradle automatically goes into the XXX directory, traverses and references all aar files before compiling. If any AAR updates later, ask your accuser to dump the new aar directly into the XXX directory and delete the old one. They don’t care what your AAR prefix is.
# # 4 kotlin? Bold use!
Google officially announced the relationship between Android and Kotlin back in 2017. One of the boldest decisions I made while writing the SDK was to use Kotlin entirely, and I was right. The introduction of Kotlin saved me a lot of glue code, and the syntactic candy tasted good. So if you decide to build a wheel from now on, go ahead and use Kotlin in full, but be warned. Since most of your references are Java programmers and may not even be familiar with Kotlin, some compatibility points are worth noting.
The referrer’s project must add Kotlin support
If your library is written by Kotlin, regardless of whether the person using your library is using Java or Kotlin, ask them to add Kotlin support to your project. Otherwise, it will be fine at compile time, but NoClassDefError may be encountered at run time, such as this one:
java.lang.NoClassDefFoundError:Failed resolution of: Lkotlin/jvm/internal/Intrinsics
Copy the code
Adding dependencies is simple: Android Studio -> Tools -> Kotlin -> Configure Kotlin in project. Android Studio will automatically add dependencies for your project. We’re done.
Type @jVMStatic to expose apis in associated objects
As anyone already writing Kotlin knows, Kotlin’s “static methods” and “static constants” are implemented by “associated objects”. For example, a simple class:
class DemoPlatform private constructor() {
companion object {
fun sayHello() {
//do something
}
}
}
Copy the code
This class if I want to call the sayHello() method, in Kotlin it’s very simple, just demoplatform.sayHello (). But if in Java, it must use the compiler automatically help us generate Companion, into DemoPlatform.Com panion. SayHello ().
This is unfriendly to Java programmers who are not familiar with Kotlin, and while the IDE hints may lead them to eventually figure it out on their own, they will still be left stunned by the unfamiliar Companion classes. So the best thing to do is to annotate this method with @jVMStatic:
@JvmStatic
fun sayHello() {
//do something
}
Copy the code
So the compiler will generate a separate, statically accessible Java method for your Kotlin function, and then go back to the Java class, You can directly demoplatform.sayHello ().
In fact, Google uses this method itself. If your project uses Kotlin, you can try right-clicking on the code tree -> New -> Fragment -> Frgment(Blank), Let Android Studio automatically create a Fragment for us.
We all know that a normal Fragment must contain a static newInstance() method to limit the number of incoming arguments. Android Studio automatically generates this method with a @jVMStatic annotation.
@JvmStatic
fun newInstance(param1: String, param2: String) =
BlankFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
Copy the code
A lot of projects in the migration phase will be mixed with Java and Kotlin, and we are an Android Library for others to use, not to mention a little annotation can save some learning costs for users, why not?
# # 5 proguard confusion
Confused myself
If your library is intended for human use only and is not intended to be fully open source, be sure to turn on obfuscate. Before you open it. The need to fully exposed to the caller of the method or property on @. Android support. The annotation. Just Keep notes, such as the sayHello () method, I hope to expose it to go out, then becomes:
@Keep
@JvmStatic
fun sayHello() {
//do something
}
Copy the code
Of course, not just methods, but anything that the @keep annotation supports. If you don’t know what the @keep comment is, man, you’re gonna lose your job if you don’t catch up.
Obfuscation can be easily enabled when compiling a release, like this:
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
Copy the code
This way, after callers rely on your library, some of the internal implementations are not easy to find, except for the methods or classes you expose yourself.
Package your ProGuard configuration file into an AAR
It is common to see a Proguard section under the home page of some open source libraries for callers to add to their app module’s Proguard profile. Proguard-rules.pro: ProGuard-rules.pro: ProGuard-rules.pro: ProGuard-rules.pro: ProGuard-rules.pro: ProGuard-rules.pro
Then open the library build.gradle and call the consumerProguardFiles() method in the defaultConfig closure:
defaultConfig {
minSdkVersion build_versions.min_sdk
targetSdkVersion build_versions.target_sdk
consumerProguardFiles 'proguard-rules.pro'. }Copy the code
Once your library is dependent, Gradle will merge this rule with the app module’s ProGuard configuration file and run it together. This way the person who references your library doesn’t have to worry about confusing the configuration anymore because you’ve done it all for him.
# # 6 so file
CMake compiles the so file directly
Because the SDK of intermodal transport involves payment business, some security-related work is bound to be carried out in the C layer. At the very beginning, I also considered compiling the SO file directly and letting the access party directly copy it to the JNI directory. In fact, many third-party libraries in China now do this when they let others connect to them. However, this is really not cool, and the access party often encounters the following problems in the operation process:
-
So what’s the name?
-
Copy to which directory?
-
How to build. Gradle?
-
How do ABI’s differentiate?
The good news is that since Android Studio 2.3, CMake has been so well integrated that we can add C/C++ code directly to our projects and dynamically generate so files during compilation.
There are already many tutorials on how to integrate C/C++ compilation into your project. Google Android Studio Cmake and you’ll find many. Of course, THE most recommended course is the official website. Or if you’re as hands-on as I am, you can create a clean Android Project and Include C++ Support in the wizard. The resulting Project will Include a simple example that is very easy to learn.
Developer.android.com/studio/proj…
extern "C" JNIEXPORT jstring JNICALL
Java_your_app_package_name_YourClass_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
Copy the code
class YourClass(private val context: Context) {
init {
System.loadLibrary(your-name-lib") } /** * A native method that is implemented by the 'native-lib' native library, * which is packaged with this application. */ external fun stringFromJNI(): String //Kotlin's external keyword is similar to Java native keyword}Copy the code
Include as many ASIs as possible and leave the option to the access party
One month after the launch of the intermodal SDK, we received feedback from CP that there was a crash after the connection. Later, we found that it was caused by the lack of SO file under Armeabi.
There’s nothing wrong with that. However, there is no way to ensure that the armeabi file of the access app is empty. Once there is so, Android will look for it. Another possibility is that many apps now set abiFilter to filter out some OF the ABI’s, in case someone just wants to keep the ArmeABI and you don’t have one in your library, both of which can cause a crash.
However:
The NDK R16B has deprecated Armeabi, and r17C has removed support for armeabi directly, only lowering the NDK version if there is a need to generate armeABI. (Thanks in the comment section @when did I say that?)
Developer.android.com/ndk/downloa…
To ensure compatibility, we must manually declare which ABI we need to compile in the library build.gradle:
defaultConfig {
externalNativeBuild {
cmake {
cppFlags ""
abiFilters 'arm64-v8a'.'armeabi'.'armeabi-v7a'.'x86'.'x86_64'}}}Copy the code
This way your library will contain the five ABI’s, ensuring that all new and old models will at least not crash. If your access party thinks your SO is too large, he can set the filter during app compilation. “I already have it, you can choose.”
# # 7 resource resources
The naming of resources within the library should not interfere with access parties
I believe you have had similar experience in the usual development process: once the introduction of some third-party libraries, when writing their own code, want to call a resource file, a prompt, IDE prompt is all the resources in the third-party library, and their app resources have to look for a long time.
XML xxx_layout. XML color. XML. So naturally it will be indexed by the IDE as well. As usual, an application should not directly reference resources from third-party libraries, which can easily lead to problems.
For example, if someone else updates this string of values, or simply remove them, your app will kneel.
For example, our SDK is called MeizuLibrarySdk, so when I define strings. XML, I will write:
<string name="mls_hello"> Hello </string> <string name="mls_world"> world < / string >Copy the code
For example, if I wanted to define a color, I would write in colors.xml:
<color name="mls_blue">#8124F6</color>
Copy the code
I believe you should have found that every resource will start with MLS, so there is a benefit, is someone in reference to your library, when using the code prompt, as long as you see MLS resources, you know it is in your library, do not use. But that’s not enough, because Android Studio will still indicate your resources when someone is writing code:
Is there a way for library developers to declare to Android Studio what resources they want exposed and what they don’t want exposed?
Of course there is. We can create a public. XML file in the library under res/values:
<! Declare to Android Studio that I only want to expose string resources with this name --> <public name="mls_hello" type="string" />
Copy the code
This way, if you try to reference mls_world in your app, Android Studio will warn you that you are referencing a private resource.
Details of this method can be found in the official documentation:
Developer.android.com/studio/proj…
But for some reason, it worked for me in ’15 or’ 16. However, after upgrading to Android Studio 3.3 + Gradle Plugin 3.1.3, I found that the IDE no longer warns me, and can also compile. I don’t know what the bug is. However, the official documentation still does not remove the description of this usage, probably a bug in the plugin.
##8 Third-party dependencies
What JCenter() can reference, don’t pack into your own
In line with the principle of “don’t reinvent the wheel”, we will inevitably rely on some third-party libraries ourselves when developing them. Like Gson for parsing JSON, or Picasso for loading images.
These libraries themselves are JAR files, so there will be some third-party library authors when using these libraries, download the corresponding JAR to liBS to participate in compilation, and finally compile into their own JAR or AAR. The subscriber’s project may already rely on these libraries. Once duplicated, an error will occur, indicating that duplicated class was found.
This is the exact opposite of Gradle’s dependency management. The correct principle should be:
Use compileOnly for libraries that third-party apps can fetch themselves from JCenter/MavenCentral, if your library also depends on them
For example, if I need to make web requests in my library, Google suggests that Retrofit is the best library to use. I should write this in my library build.gradle:
compileOnly "Com. Squareup. Retrofit2: retrofit: 2.4.0." "
Copy the code
CompileOnly indicates that subsequent libraries are only valid at compile time, but not your library’s package. So you just tell your referencers to add a reference to their app module build.gradle, like this:
implementation "com.squareup.retrofit2:retrofit:$versions.retrofit"
Copy the code
The benefit of this is that if the referrer’s project already relies on Retrofit, everyone is happy, nothing is added, and the $versions above means that the referrer can decide which version of Retrofit he wants to use. Generally speaking, anything greater than or equal to the version you’re compiling your library with won’t be a big problem, unless Retrofit itself has changed the API so much that it can’t be compiled. This once again puts the option in the hands of your referrer, without worrying about conflicts or versions that don’t match what you’re using.
Use a single file to unify the version of the dependency library
If you have a complex project with many modules, I recommend you to use a versions.gradle file to unify the versions of all module dependencies.
I didn’t invent this trick, but Google did it in the official demo of Architecture-Components. The demo Project contains a large number of modules, including library and app, and all modules need the same version of the dependency library. Take buildToolsVersion as an example, you should not rely on 27.1.1. I rely on 28.0.0 like this.
I put the link below and recommend that you learn how this file is written and how it unifies all modules.
Github.com/googlesampl…
# # 9 API design
About API design, because everyone’s library to implement the function different, so there is no way to specific list, but still in here to share some attention points, actually these note points as long as you can stand in the perspective of the access to consider, most would have thought that the problem is that you would when writing the libraries to access people consider more for you.
Don’t dance in someone else’s Application class
Exposing an init() method for your callers to initialize in the Application class is something many library authors like to do. But if you think about it the other way around, we’ve all seen a lot of performance optimization articles, and the first one is usually to ask you to check your Application class, are you doing too many time-consuming operations?
Because Application is the first thing you need to do after you start it, if you do something time-consuming in it, you will delay the Activity loading, which is easy to overlook. So if you are a library author, please:
-
Don’t do anything time-consuming in your init() method
-
Don’t even offer an init() method that puts it in your Application class and says “best suggest asynchrony.” It’s just like being a troll
Unified entry, with one platform class to contain all functionality
The platform class here is my own name, you can call it XXXManager, XXXProxy, XXXService, XXXPlatform, make it a singleton, or write all the internal methods as static methods.
Don’t make your caller try to figure out which class to instantiate. All the methods are in the same class anyway. Just call the corresponding method when you get the instance. This unified entry reduces maintenance costs and your callers will thank you.
All constants are defined into a class
if (code == 10012) { //do something}
Copy the code
What is 10012? Is it the return code defined in your library? So why not write it as a constant and expose it to your caller?
@Keep
class DemoResult private constructor(){@keep companion object {/** * failed to pay, cause: failed to connect to network, please check network Settings */ const val CODE_ERROR_CONFIG_ERROR: Int = 10012 const val MSG_ERROR_CONFIG_ERROR: String ="Configuration error. Please check parameters.". }}Copy the code
This way, your callers can click their mouse, come in and look at your class, and quickly match the error code to the error message. If they’re a little lazy, they can even present the prompts you’ve defined directly to the user.
And if one day your server colleague tells you that 10012 needs to be changed to a different value, you just need to change your own code. For the access to the library, it will still be demoresult.code_error_config_error, and you don’t need to change anything. So convenient access to the matter why not?
Helps access users check the validity of incoming parameters
If your API has requirements for the parameters passed in. It is recommended that the parameters be checked in the first step of method execution. If the caller passes an invalid argument, throw an exception. Many developers feel that throwing exceptions is unacceptable, because it is a direct manifestation of app crash on the Android platform after all.
However, rather than crash the APP in the hands of users, it is better to crash the app in the development stage so that developers can immediately notice and fix it.
If you use Kotlin, everything is much easier. For example, I now have an entity like this:
data class StudentInfo(val name: String)
Copy the code
StudentInfo must have a name, and I declare that name is not null. If you instantiate Student in Kotlin and pass null name, it will not compile directly. For Java, the class file Kotlin helped us generate already does this:
public StudentInfo(@NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, "name");
super();
this.name = var1;
}
Copy the code
Continue with the checkParameterIsNotNull() method:
public static void checkParameterIsNotNull(Object value, String paramName) {
if(value == null) { throwParameterIsNullException(paramName); }}Copy the code
ThrowParameterIsNullException () is a relatively simple throw exceptions.
private static void throwParameterIsNullException(String paramName) {
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
// #0 Thread.getStackTrace()
// #1 Intrinsics.throwParameterIsNullException
// #2 Intrinsics.checkParameterIsNotNull
// #3 our caller
StackTraceElement caller = stackTraceElements[3];
String className = caller.getClassName();
String methodName = caller.getMethodName();
IllegalArgumentException exception =
new IllegalArgumentException("Parameter specified as non-null is null: " +
"method " + className + "." + methodName +
", parameter " + paramName);
throw sanitizeStackTrace(exception);
}
Copy the code
So even if you’re using Java and you try to say Student Student = new Student(null), the runtime will crash and tell you that name can’t be null. The intermodal SDK has a lot of parameter checking using this feature of Kotlin, so I have a lot less code, the compiler will automatically generate it for me after compiling.
Here to recommend you consult an android. Support. The v4. Util. The Preconditions, the sealed inside a large amount of data type for the check, source and a look at will understand. We hope that when you write a library, you can do a good job of checking the validity of the incoming parameters, to find problems in the development phase, but also to ensure that the runtime does not crash with unexpected values.
Some regret
So far, I’ve basically shared with you all the experiences and pitfalls of SDK development. Of course, nothing is perfect in this world, and there are still many deficiencies in our intermodal SDK, such as:
-
It is not published to mavenCentral() and requires the developer to manually download the AAR and add it to the compilation
-
The SDK relies on Picasso to load images, which should be abstracted and implemented by access parties using their own solutions
-
Our SDK is composed of 7 AArs, each of which is maintained by a small team. Developers need to copy all of them to a directory when they access, which is somewhat redundant and bloated
Some of these deficiencies are due to insufficient consideration at the beginning of the project, and some are limited by the project architecture. We’ll evaluate each one and try to make our SDK better and better. You are also welcome to post your own Android Library mistakes or tips in the comments section, and I will update it gradually in the future, so that we can work together to make more standard and good wheels.