什么是组件化?
组件化开发就是将一个app分成多个模块,每个模块都是一个组件(Module),开发的过程中我们可以让这些组件相互依赖或者单独调试部分组件等,但是最终发布的时候是将这些组件合并统一成一个apk,这就是组件化开发。
为什么要用组件化开发?
对于大型app,业务逻辑随着app迭代越来越复杂,代码量越来越多就会存在如下问题。
1.代码耦合度严重,改一个细微的地方有可能牵扯很多文件,牵一发而动全身。
2.修改bug编译调试非常慢,对于大项目来说rebuild一次app需要5分 10分的很常见,假如只修改一个很小的问题编译却要等上10分钟效率太低。
3.功能动态插拔,例如个推,Umeng都是单独的module,如果想用其他推送平台来代替个推,只需要将个推的module删除那么所有个推的依赖库、资源文件、代码、权限等都一并删除了,不会留在项目中一些垃圾资源。
4.在多团队开发的时候代码的冲突也会非常严重,每次提交都需要解决代码冲突和沟通占用了大量的时间,影响开发效率。
5.修改工程就需要整体的回归测试,由于代码耦合度严重,修改一个小bug有可能牵扯很多代码,所以每次都需要回归测试。
基于以上问题,组件化是最好的选择。(组件化不是所有项目都适合,还是要根据公司自身来选用)
本篇文章是组件化实现的第一步。抽取公共组件、开源项目。
一、抽取build.gradle
中的dependencies
将每个module中dependencies
下面依赖的aar抽取出来放在公共的位置;
这样做的好处是需要依赖的地方只要写公共的标志就可以实现依赖;
如果后期需要修改依赖或者每次升级版本都比较方便只需要更改公共位置标志的Value就可以;
由于aar可以多次依赖所以不用担心重复依赖的问题。
如下图文讲解:
在工程中增加dependencies.gradle
文件
在project的build.gradle中最顶端增加如下代码,将这个文件引入
//引入全局的设置
apply from: 'dependencies.gradle'
......
这个文件用来定义applicationId
、versionCode
、versionName
、compileSdk
、targetSdk
、minSdk
、buildTools
、aar依赖等。
如下代码
ext.versions = [
applicationId : "com.component.demo",
code : 1,
name : '1.0.0',
minSdk : 11,
targetSdk : 25,
compileSdk : 25,
buildTools : '25.0.2',
supportLibs : '25.3.1',
supportConstrain : '1.0.2',
fastJson : '1.2.33',
universalImageLoader: '1.9.5',
]
ext.libraries = [
supportAnnotations : "com.android.support:support-annotations:$versions.supportLibs",
supportAppCompat : "com.android.support:appcompat-v7:$versions.supportLibs",
supportDesign : "com.android.support:design:$versions.supportLibs",
supportRecyclerView: "com.android.support:recyclerview-v7:$versions.supportLibs",
supportCardView : "com.android.support:cardview-v7:$versions.supportLibs",
supportFragment : "com.android.support:support-fragment:$versions.supportLibs",
constraintLayout : "com.android.support.constraint:constraint-layout:$versions.supportConstrain",
fastJson : "com.alibaba:fastjson:$versions.fastJson",
imageLoader : "com.nostra13.universalimageloader:universal-image-loader:$versions.universalImageLoader",
]
分别在每个module(很可能是多个module)的build.gradle
文件中依赖dependencies.gradle
文件中的versions
、libraries
,如下代码:
apply plugin: 'com.android.application'
android {
// compileSdkVersion 25
// buildToolsVersion "25.0.2"
compileSdkVersion versions.compileSdk
buildToolsVersion versions.buildTools
defaultConfig {
// applicationId "com.component.demo"
// minSdkVersion 11
// targetSdkVersion 25
// versionCode 1
// versionName "1.0"
applicationId versions.applicationId
minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk
versionCode versions.code
versionName versions.name
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
// compile 'com.android.support:appcompat-v7:25.3.1'
// compile 'com.android.support.constraint:constraint-layout:1.0.2'
compile libraries.supportAppCompat
compile libraries.constraintLayout
compile libraries.fastJson
compile libraries.constraintLayout
compile libraries.imageLoader
}
假如需要升级imageLoader
的版本,直接在dependencies.gradle
修改版本号,这样所有依赖libraries.imageLoader
的module都得到了修改,不用每个module都去修改。
如下代码:
ext.versions = [
...
//修改这里
universalImageLoader: '1.9.5',
]
ext.libraries = [
...
imageLoader : "com.nostra13.universalimageloader:universal-image-loader:$versions.universalImageLoader",
]
二、Library分模块
library根据功能、来源分为如下模块
open-source-library :
开源库,或者自己封装的库都是aar和jar文件,不允许有代码的存在。
目的是让依赖的库都保存在一起,方便以后升级修改。
如下图:
open-source-code :
开源代码,或者自己写的开源代码,一般不需要修改或者少量修改,可以依赖aar或者jar。
每个开源代码都是一个module,他们之前也存在相互依赖关系,
如下图:
third-party-plugin :
第三方插件,个推、Umeng等都是第三方插件。
如果app需要个推,直接添加依赖,那么就可以引入个推,如果想更换推送平台,直接删除个推依赖,那么个推相关的代码、资源、权限等都会被删除,不会对app产生垃圾文件。
如下图:
encapsulation-isolation-code :
1.封装:
对依赖库的封装,Utils
工具类、SharedPrefercens
封装、数据库的封装、统计代码的封装、网络库的封装等等。
2.隔离:
对开源库的隔离非常重要,由于开源库有可能停止维护,Android系统升级对开源库的影响,所以开发期间要求更换开源库是很常见的事。如果对开源库进行隔离,更换库只会影响隔离代码,如果不隔离那么开源库会分布在项目的每个角落,还会参与业务逻辑,这样改起来的成本会非常大,而且极其容易出问题。
下面就拿ImageLoader
作为例子吧,
如果不做代码隔离,那么在使用的时候都会调用和引入如下代码,
import com.nostra13.universalimageloader.cache.disc.naming.Md5FileNameGenerator;
import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.assist.QueueProcessingType;
ImageLoader.getInstance().displayImage(String uri, ImageView imageView, DisplayImageOptions options,
ImageLoadingListener listener, ImageLoadingProgressListener progressListener);
像这样的代码,有可能会分布在很多文件中,因为加载图片是个常用的操作,如果我们想替换加载图片的库,那可就麻烦了需要修改N多个文件,这是绝对不允许的。
为了解决这个问题我们做了代码隔离(只是简单的展示隔离的原理,真正的隔离代码要比这个复杂的多),如下代码:
//我们定义自己的图片加载接口
public interface ImageLoaderProxy {
void initImageLoader(Application application);
void displayImage(String uri, ImageView imageView,
ImageLoadingListener listener, ImageLoadingProgressListener progressListener);
}
//我们定义自己的图片加载监听
public interface ImageLoadingListener {
void onLoadingStarted(String imageUri, View view);
void onLoadingFailed(String imageUri, View view, String failReason);
void onLoadingComplete(String imageUri, View view, Bitmap loadedImage);
void onLoadingCancelled(String imageUri, View view);
}
//我们定义自己的图片加载进度
public interface ImageLoadingProgressListener {
void onProgressUpdate(String imageUri, View view, int current, int total);
}
//用universalimageloader来实现ImageLoaderProxy
public class ImageLoaderUtil implements ImageLoaderProxy {
private DisplayImageOptions mDefaultOptions = null;
private static final ImageLoaderUtil IMAGE_LOADING = new ImageLoaderUtil();
public static final ImageLoaderProxy getInstance(){
return IMAGE_LOADING;
}
@Override
public void initImageLoader(Application application) {
if (ImageLoader.getInstance().isInited()) {
ImageLoader.getInstance().destroy();
}
ImageLoaderConfiguration.Builder builder = new ImageLoaderConfiguration.Builder(application)
.memoryCacheExtraOptions(480, 800)
.threadPriority(Thread.NORM_PRIORITY - 2)
.denyCacheImageMultipleSizesInMemory()
.diskCacheFileNameGenerator(new Md5FileNameGenerator())
.tasksProcessingOrder(QueueProcessingType.LIFO)
.memoryCache(new LRULimitedMemoryCache(10 * 1024 * 1024))
.diskCacheSize(50 * 1024 * 1024)
.writeDebugLogs();
ImageLoaderConfiguration config = builder.build();
ImageLoader.getInstance().init(config);
mDefaultOptions = new DisplayImageOptions.Builder()
// .showImageOnLoading(holderId)
// .showImageForEmptyUri(holderId)
// .showImageOnFail(holderId)
.delayBeforeLoading(100)
.bitmapConfig(Bitmap.Config.RGB_565)
.considerExifParams(true)
.cacheInMemory(false)
.cacheOnDisk(true)
.build();
}
@Override
public void displayImage(
String uri,
ImageView imageView,
ImageLoadingListener listener,
ImageLoadingProgressListener progressListener) {
ImageLoader.getInstance().displayImage(
uri,
imageView,
mDefaultOptions,
new ImageLoadingImpl(listener),
new ImageLoadingProgressImpl(progressListener));
}
class ImageLoadingImpl implements com.nostra13.universalimageloader.core.listener.ImageLoadingListener {
private ImageLoadingListener mImageLoadingListener;
public ImageLoadingImpl(ImageLoadingListener imageLoadingListener) {
mImageLoadingListener = imageLoadingListener;
}
@Override
public void onLoadingStarted(String imageUri, View view) {
if (null != mImageLoadingListener) {
mImageLoadingListener.onLoadingStarted(imageUri, view);
}
}
@Override
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
if (null != mImageLoadingListener) {
mImageLoadingListener.onLoadingFailed(imageUri, view, failReason.toString());
}
}
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
if (null != mImageLoadingListener) {
mImageLoadingListener.onLoadingComplete(imageUri, view, loadedImage);
}
}
@Override
public void onLoadingCancelled(String imageUri, View view) {
if (null != mImageLoadingListener) {
mImageLoadingListener.onLoadingCancelled(imageUri, view);
}
}
}
class ImageLoadingProgressImpl implements com.nostra13.universalimageloader.core.listener.ImageLoadingProgressListener {
private ImageLoadingProgressListener mImageLoadingProgressListener;
public ImageLoadingProgressImpl(ImageLoadingProgressListener imageLoadingProgressListener) {
mImageLoadingProgressListener = imageLoadingProgressListener;
}
@Override
public void onProgressUpdate(String imageUri, View view, int current, int total) {
if (null != mImageLoadingProgressListener) {
mImageLoadingProgressListener.onProgressUpdate(imageUri, view, current, total);
}
}
}
}
//使用自己定义的ImageLoaderProxy,使用的地方和universalimageloader没有任何关系都是工程中的代码,真正的做到了代码隔离,假如想换个图片加载库,只需要更改实现ImageLoaderProxy的代码就可以了
import com.component.demo.imageloader.ImageLoaderUtil;
import com.component.demo.imageloader.ImageLoadingListener;
import com.component.demo.imageloader.ImageLoadingProgressListener;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageLoaderUtil.getInstance().initImageLoader(getApplication());
ImageLoaderUtil.getInstance().displayImage("", new ImageView(this), new ImageLoadingListener() {
@Override
public void onLoadingStarted(String imageUri, View view) {
}
@Override
public void onLoadingFailed(String imageUri, View view, String failReason) {
}
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
}
@Override
public void onLoadingCancelled(String imageUri, View view) {
}
},
new ImageLoadingProgressListener() {
@Override
public void onProgressUpdate(String imageUri, View view, int current, int total) {
}
});
}
}
以上代码就是隔离图片加载框架的代码,在MainActivity
中使用的地方和universalimageloader
没有任何关系都是工程中的代码,真正的做到了代码隔离,假如想换个图片加载库,只需要更改实现ImageLoaderProxy
的代码就可以了。
3.自定义控件:
自定义导航栏,自定义TextView
,自定义开关按钮等
4.各种基类代码:
BaseActivity
,BaseApplication
,BaseFragment
,BaseAdapter
,BaseLayout
等。
基类代码处理整个项目公用的配置,如下BaseActivity
代码:
1.网络对话框的统一设置
2.Unbinder
的统一设置
3.EventBus
的统一设置
这里只是简单的展示了Base
的作用,实际上根据业务需求会更加复杂
public class BaseActivity extends AppCompatActivity {
private Unbinder unbinder;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EventBus.getDefault().register(this);
}
@Override
public void onContentChanged() {
super.onContentChanged();
unbinder = ButterKnife.bind(this);
}
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onPause() {
super.onPause();
}
@Override
protected void onStop() {
super.onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
EventBus.getDefault().unregister(this);
if (unbinder != null) {
unbinder.unbind();
unbinder = null;
}
}
protected void showLoadingView() {
// TODO:显示网络加载对话框
}
protected void dismissLoadingView() {
// TODO:隐藏网络加载对话框
}
}
common-dependencies-library :
公共依赖库,所有module都可以依赖这个lib;
1.ARouter
路由配置,全局的Path
,Activity
传递数据的Key
2.多个module需要的JavaBean
,例如UserInfo
3.多个module需要的静态常量
坑:
1.App主项目依赖本地aar库。依赖路径如下图:
build.gradle
配置
//encapsulation-isolation-code build.gradle
repositories { flatDir { dirs 'libs' } }
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile(name: 'pickerview-release', ext: 'aar')
compile(name: 'mupdf-release', ext: 'aar')
compile(name: 'CropImage-release', ext: 'aar')
}
//app build.gradle
repositories {
flatDir { dirs 'libs', '../encapsulation-isolation-code/libs' }
}
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile project(':encapsulation-isolation-code')
}
2.aar混淆问题
对于apply plugin: 'com.android.library'
aar库module混淆的配置用consumerProguardFiles 'proguard-rules.pro'
来指定一个混淆文件,这个指定的混淆文件会打包在aar文件中,当主app引入这个aar库这个混淆文件会影响整个工程。
3.annotationProcessor
annotationProcessor
会扫描代码对注释进行处理,所以每个module都需要单独设置
4.aar版本冲突报错
All com.android.support libraries must use the exact same version specification (mixing versions can lead to runtime crashes). Found versions 25.3.0, 25.2.0, 24.2.0. Examples include com.android.support:support-compat:25.3.0 and com.android.support:support-core-ui:25.2.0 less... (⌘F1)
There are some combinations of libraries, or tools and libraries, that are incompatible, or can lead to bugs. One such incompatibility is compiling with a version of the Android support libraries that is not the latest version (or in particular, a version lower than your targetSdkVersion.)
项目里(依赖或间接依赖)中包含不同版本包容易编译错误,需统一,support v7 appcompat
等包依赖support-v4
,解决办法添加 compile ‘com.android.support:support-v4:25.3.0’