本文会不定期更新,推荐watch下项目。如果喜欢请star,如果觉得有纰漏请提交issue,如果你有更好的点子可以提交pull request。
本文的示例代码主要是基于作者的经验来编写的,若你有其他的技巧和方法可以参与进来一起完善这篇文章。
本文固定连接:https://github.com/tianzhijiexian/Android-Best-Practices
一、需求背景
目前技术圈的人或多或少都开发过库项目,无论是因为要靠它来找工作,还是通过其进行学习交流,亦或是借此来招摇撞骗,总之开发第三方库这件事已经变得越来越流行了。
我个人是很讨厌技术圈娱乐化的,star那么多,issue没人管的现象比比皆是。其实解决issue才能促使实践者更快的学习,仅仅炫耀自己的star数毫无意义。因此,我便写了我开发第三方库的经验,希望本文能帮助到大家。
二、需求
我认为一个好的库作者应该是能满足以下需求的:
- 避免重复造轮子
- 谨慎设计API
- 避免引入其他库
- 尽量用注解代替枚举
- 资源文件加上特殊前缀
- 提供可插拔依赖的方案
- 将Manifest中的参数变量化
- 有多个相关依赖,做聚合依赖
- 根据需求考虑是否提供no-op
- 仅仅在debug模式中引入代码
- 使用JitPack做库的托管仓库
- 严格限制库的大小和方法数
- 快速解决issue,多和提问者沟通
- 不断完善,坚持更新
三、实现
避免重复造轮子
当你产生了一个新奇的想法,想要实现它之前请用一天的时间去分析自己想法的优缺点,然后去Github上搜索有没有类似的库,或者是通过群组来询问相关信息。你必须知道,当你提出一个想法的时候,别人很可能也已经想过了,差别就在于别人是否已经实现。如果实在没有找到和你要做的库类似的东西,那么就开始干吧!
如果有现成的,那么我就不做
如果你搜索到github上有个和你想做的库类似的东西,你完全可以了解其实现后拿来就用,这会节约你很多的时间。我自己写过一个Gradle插件,当我写的差不多的时候我突然意识到github上可能有现成的,所以我立刻停止了开发,进行搜索。果真找到了一个十分类似的库,阅读源码后发现其思路和我几乎完全一致。
如果现成的不够好,我可以让它变的更好
当我们搜索到了一个和自己想法类似的库后,很可能会发现它和自己的想法有些差异,或者是有些bug。这时候千万不要呵呵一声,然后自己开始狂妄的写代码。我更加希望的是通过issue联系到作者,提出问题,如果可能的话给出自己的解决方案和pr。我们的时间很宝贵,为何不花时间来维护同一个东西呢。
如果现成的太糟,由我来让其脱胎换骨
Github上有很多很多作者,那么自然就产生了社会性。我的提交和留言很可能被作者无视,或者作者早就转行了,他的项目等于死了。遇到这样的情况,我的做法是frok人家的代码,然后自己开始维护。DebugDrawer就是一个例子。他原本的作者已经很久不更新了,而且issue也没人回复,不得已的情况下我只能自己维护了。自己拿来维护可以,但一定要记得fork人家的代码,你要时刻记住,你是踩在前人的肩膀上,不要狂妄。
谨慎设计API
如果你的库是给人用的,那么请在设计api层面多花点时间。因为一旦有人用了你的库,你就有了历史负担。如果你后期随意地改变方法名和参数,使用者会来打你的,所以经验就很重要了。我可以简单给出一个建议:把内部类写到参数靠后的位置,把context放在参数的前部:
public interface Test{
// context在前,内部类在后
void test(Context context, View.OnClickListener listener);
}
如果后期实在要改名字和废弃方法,可以采用@Deprecated
来做标记,把要变动的东西先标记为废弃,过了几个版本后再删除掉。
避免引入其他库
你的代码本身就是一个库了,因此我强烈不建议你的代码还引入别人的库。友盟推送的代码就是一个典型的反例,一个推送库引入了okhttp、okio等其他库,臃肿不堪,完全没有让我使用的欲望。
一个第三方库引入其他第三方库有很多坏处,使用者可能会遇到版本冲突的问题(比如:友盟反馈和友盟推送同时使用),方法数还会极速增多。
你可能会说appcompat
这个库基本所有第三方库都会引入的,有没有什么好的办法可以避免呢?好在我们有provided
关键字。provided
可以将你需要的库引入,但是并不会将其打包到aar里面以CommonAdapter为例:
dependencies {
provided 'com.android.support:recyclerview-v7:23.2.1'
provided 'com.android.databinding:baseLibrary:1.0'
provided "org.projectlombok:lombok:1.12.6"
}
CommonAdapter依赖了三个库,但是都用了私有依赖的方式来做的。
首先,我能确定使用这个库的人,肯定使用了recyclerView,所以我通过私有依赖的方式将recyclerView的代码剔除,那么recyclerView的最终版本由使用者来定。
其次,如果使用者的项目使用了DataBinding这个库,那么可以采用数据绑定的形式来做界面的更新操作,但我并非强制使用者必须依赖db,所以我也将db的库剔除,并且在代码里做了这样的判断:
public class DataBindingJudgement {
public static final boolean SUPPORT_DATABINDING;
static {
boolean hasDependency;
try {
Class.forName("android.databinding.ObservableList");
hasDependency = true;
} catch (ClassNotFoundException e) {
hasDependency = false;
}
SUPPORT_DATABINDING = hasDependency;
}
}
public CommonAdapter(@Nullable List<T> data, int viewTypeCount) {
if (DataBindingJudgement.SUPPORT_DATABINDING && data instanceof ObservableList) {
// 判断是否有db的依赖
((ObservableList<T>) data).addOnListChangedCallback(new ObservableList.OnListChangedCallback<ObservableList<T>>() {
@Override
public void onChanged(ObservableList<T> sender) {
notifyDataSetChanged();
}
});
}
//...
}
上面的代码的意思是如果你用了db,那么commonAdapter就支持了数据绑定,如果没有用到db,也不影响,还可以用传统方式来做。
最后,我为了增加代码可维护性,我引入了lombok。无论使用者是否用了lombok,都和我无关,这种情况采用provided的方式也是最合理的。
尽量用注解代替枚举
通过注解代替枚举可以减少内存开销,并且在AS越来越智能的提示下,编码方式和枚举几乎一致。以ShareLoginLib为例,我在编码的时候会用注解来进行参数的表示,这里一般会使用有限值的int来做。
@Retention(RetentionPolicy.SOURCE)
@IntDef({ContentType.TEXT, ContentType.PIC, ContentType.WEBPAGE, ContentType.MUSIC})
public @interface ContentType {
int TEXT = 1, PIC = 2, WEBPAGE = 3, MUSIC = 4;
}
但是如果你需要将注解暴露给使用者,那么我推荐采用string的形式来做,因为string的值不会像int那样随着打包而改变,此外string有很高的可读性。在AS目前还没智能识别变量的情况下,我强烈建议用有限值的String来代替枚举。
@Retention(RetentionPolicy.SOURCE)
@StringDef({LoginType.WEIXIN, LoginType.WEIBO, LoginType.QQ})
public @interface LoginType {
String WEIXIN = "WEIXIN", WEIBO = "WEIBO", QQ = "QQ";
}
给资源文件加上特殊前缀
第三方库的资源会和使用者的项目进行合并,资源的名字需要特殊注意,以DebugDrawer为例,我在layout资源前面都会加特殊的前缀(debugDrawer->dd)
除了layout文件,color等也应该注意,库作者多注意这些细节点,会给使用者省去很多麻烦,减少不必要的冲突和问题。
在access文件中也应该建立一个子目录:
这样可以防止多个库用了同一个同一个资源,然后产生覆盖的问题。
提供可插拔依赖的方案
我们制作的库很可能会用到回调,我希望给已经使用了rxjava项目的使用者rxjava的回调,给没有使用rxjava的用户提供默认的接口回调。
首先,通过provided来依赖rxjava:
provided 'io.reactivex:rxjava:1.1.3'
然后在代码中提供使用rxjava和传统的两种方法:
这样使用者就可以选择性的使用不同的方法来接收回调了。
将Manifest中的参数变量化
在制作第三方登录、分享的SDK时,我发现需要在manifest中定义一些key,但是我不希望写死,而是交由使用者进行填写。因此我将key变量化:
使用的时候只需要在gradle中进行如下配置即可:
android {
compileSdkVersion 23
buildToolsVersion '23.0.2'
defaultConfig {
applicationId "com.liulishuo.engzo"
minSdkVersion 15
targetSdkVersion 23
manifestPlaceholders = [
// 这里需要换成:tencent+你的AppId
"tencentAuthId": "tencent123456",
]
}
}
最终打包生成的manifest就会自动合并成下图:
值得注意的是${applicationId}是一个默认的变量,随着实际项目中的参数而定,所以在需要在manifest中指定具体包名的时候可以采取如下方式:
实际项目中强烈建议把这个值定写成包名:
有多个相关依赖,做聚合依赖
DebugDrawer有多个用来debug的库,使用者可以根据选择进行依赖。但是这些库都是和DebugDrawer密切相关的,所以开发者应该建议使用者将他们应该一并依赖进来,这样以后删除库的时候也很方便。
debugCompile(["com.github.tianzhijiexian:DebugDrawer:1.0.0",
"jp.wasabeef:takt:1.0.1",
"com.jakewharton.scalpel:scalpel:1.4.6"
])
根据需求考虑是否提供no-op
如果你开发的库可能只需要在debug时才用到,但库提供的类或方法需要写入现有的代码中,那么就可以采用no-op的方案。所谓no-op就是希望某些代码仅仅存在于debug环境中,在release版本中,可能就是保留了了一些代码接口,但是并不提供实现。
以leakcanary为例,它的文档中就给出了no-op的依赖。
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta2'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
}
我举一个no-op的例子:
// debug版依赖的代码
public void doSomeThing(){
int i = 1,sum;
for(int i =0;i<100000;i++){
sum += i;
}
// 省略一千行代码
}
// release版依赖的代码
public void doSomeThing(){
// no-op
}
no-op的库仅仅提供了一个方法壳,让不需要出现再release包中的代码消失。
仅仅在debug模式中引入代码
如果你的库代码仅仅需要出现在debug模式中,并且对于使用者现有的代码没任何影响,那么你可以建议使用你的库的人通过debugCompile
进行依赖。
以stetho为例,我先将其用debug模式进行依赖。
debugCompile "com.facebook.stetho:stetho:1.3.1"
然后在src下建立debug/java的目录,接着建立一个DebugApplication的类:
public class DebugApplication extends ReleaseApplication {
@Override
public void onCreate() {
super.onCreate();
Stetho.initialize(
Stetho.newInitializerBuilder(this)
.enableDumpapp(Stetho.defaultDumperPluginsProvider(this))
.enableWebKitInspector(
Stetho.defaultInspectorModulesProvider(this)).build());
}
}
最后,在manifest文件中进行application的替换:
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
>
<application
android:name=".DebugApplication"
android:allowBackup="true"
android:icon="@drawable/debug_icon"
tools:replace="android:name,android:icon"
/>
</manifest>
这样我们就会在debug时自动用debugApplication作为application对象,并且使用里面stetho的代码,在release版本中还是采用ReleaseApplication
,以此来减少无用代码的引入。
使用JitPack做库的托管仓库
我们的代码大多都是存在github上面,jitpack可以快速将你的github项目变成可以被使用者进行依赖的库。
这是我的一个库的例子:https://jitpack.io/#tianzhijiexian/AppBar
我们可以通过tag和commit进行库版本的选择,选择完毕后就可以看到依赖的方式:
jitpack让我们提交库变得简单快速,但需要注意它并不能支持多个module的库,这是一个劣势。
jitpack还提供了java文档的在线浏览,如果你的库需要提供文档支持,那么它绝对是一个很好的选择。
配置的方式是在lib的build.gradle中添加如下代码:
// build a jar with source files
task sourcesJar(type: Jar) {
from android.sourceSets.main.java.srcDirs
classifier = 'sources'
}
task javadoc(type: Javadoc) {
failOnError false
source = android.sourceSets.main.java.sourceFiles
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
classpath += configurations.compile
}
javadoc {
options {
encoding "UTF-8"
charSet 'UTF-8'
author true
version true
links "http://docs.oracle.com/javase/7/docs/api"
}
}
// build a jar with javadoc
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
}
artifacts {
archives sourcesJar
archives javadocJar
}
接着把代码push到github上后我们就可以在线浏览文档了。
如果你想详细了解jitpack,jitpack的官方doc写得很清楚,查看即可。
Tips
下面说个小的tips,我们希望使用者可以明确知道当前工程最新的版本是多少,但是每次手动改readme很麻烦,jitpack可以通过插入link的方式来自动获得jitpack上的最新版本。进入https://jitpack.io/#tianzhijiexian/UIBlock/58d865ecbd 选择完版本后就可以看到最下方的提示了。
最后将svg粘贴到readme开头就可以了,这个标签还可以让我们快速从工程跳转到jitpack,十分实用。
严格限制库的大小和方法数
以我自身的经验,一个小型库的方法数不应该超过300,所以需要时刻留意自己是否在做一个单一功能的库。这个300自然不是权威指标,我只是希望库开发者应该尽可能让自己的库轻量干净,减少使用者引入库的负担。一个第三方库的方法数和大小都是使用者会考虑的点,所以我推荐使用:MethodsCount来进行库方法数目的检测:
我们还可以通过图表来量化自己库的方法数和大小,下面就是ShareLoginLib的走势图:
MethodsCount还提供了as插件以便于我们了解自己依赖的库大小,安装后的效果如下:
也可以采用谷歌推荐的方式进行依赖关系的检测(不常用):
支持SourceGraph,让使用者可以快速浏览项目代码
Github一个不好的地方就是代码是不能相互跳转的,所以阅读起来很累,如果我要引入一个库,那么就必须clone下来然后通过idea打开才行。这样的流程对于库的前期调研来说成本很高,所以我希望利用SourceGraph让在线阅读代码的体验提升一个量级。
一个简单的演示:
你在安装完SourceGraph的Chrome插件后,就会发现支持SourceGraph的代码上方就会显示一个icon。
现在,你就可以利用sourcegraph进行跳转和插件文档了。
想要体验更多,可以浏览:
https://github.com/tianzhijiexian/CommonAdapter/blob/master/adapter/src/main/java/kale/adapter/BasePagerAdapter.java
快速解决issue,多和提问者沟通
一个优秀的开源库自然要经历很多issue,作为库开发者需要对issue有一定的敏感度,不要因为自己太忙而放任不管。
我分享下我的做法:
- 如果提问者是理解上的问题,可以在解答后更新到ReadMe中,以防止别人有同样的疑问。
- 如果是小的bug,那么最好快速修复,并且由提交者验证问题,验证后由提交者关闭issue。
- 如果是难以解决的bug,或最近自己没有时间,那么应快速告知提问者,说明情况。
不断完善,坚持更新
完善和维护一个库确实需要很大的精力,如果你的库是真的希望给别人用的,那么就应该能有付出时间和精力的准备。因为你既然做了这件事,那么就需要为此负责。我平时也非常忙,但是我还是努力地做着这些事情,所以我相信你也可以的!