Android组件化架构实现(一)

什么是组件化?
组件化开发就是将一个app分成多个模块,每个模块都是一个组件(Module),开发的过程中我们可以让这些组件相互依赖或者单独调试部分组件等,但是最终发布的时候是将这些组件合并统一成一个apk,这就是组件化开发。
为什么要用组件化开发?
对于大型app,业务逻辑随着app迭代越来越复杂,代码量越来越多就会存在如下问题。
1.代码耦合度严重,改一个细微的地方有可能牵扯很多文件,牵一发而动全身。
2.修改bug编译调试非常慢,对于大项目来说rebuild一次app需要5分 10分的很常见,假如只修改一个很小的问题编译却要等上10分钟效率太低。
3.功能动态插拔,例如个推,Umeng都是单独的module,如果想用其他推送平台来代替个推,只需要将个推的module删除那么所有个推的依赖库、资源文件、代码、权限等都一并删除了,不会留在项目中一些垃圾资源。
4.在多团队开发的时候代码的冲突也会非常严重,每次提交都需要解决代码冲突和沟通占用了大量的时间,影响开发效率。
5.修改工程就需要整体的回归测试,由于代码耦合度严重,修改一个小bug有可能牵扯很多代码,所以每次都需要回归测试。
基于以上问题,组件化是最好的选择。(组件化不是所有项目都适合,还是要根据公司自身来选用)

本篇文章是组件化实现的第一步。抽取公共组件、开源项目。

Library.png

一、抽取build.gradle中的dependencies

将每个module中dependencies下面依赖的aar抽取出来放在公共的位置;
这样做的好处是需要依赖的地方只要写公共的标志就可以实现依赖;
如果后期需要修改依赖或者每次升级版本都比较方便只需要更改公共位置标志的Value就可以;
由于aar可以多次依赖所以不用担心重复依赖的问题。
如下图文讲解:
在工程中增加dependencies.gradle文件

增加dependencies.gradle文件.png

在project的build.gradle中最顶端增加如下代码,将这个文件引入

//引入全局的设置
apply from: 'dependencies.gradle'
                ......

这个文件用来定义applicationIdversionCodeversionNamecompileSdktargetSdkminSdkbuildTools、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文件中的versionslibraries,如下代码:

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-library
open-source-code :

开源代码,或者自己写的开源代码,一般不需要修改或者少量修改,可以依赖aar或者jar。
每个开源代码都是一个module,他们之前也存在相互依赖关系,
如下图:


open-source-code.png
third-party-plugin :

第三方插件,个推、Umeng等都是第三方插件。
如果app需要个推,直接添加依赖,那么就可以引入个推,如果想更换推送平台,直接删除个推依赖,那么个推相关的代码、资源、权限等都会被删除,不会对app产生垃圾文件。
如下图:


third-party-plugin-getui.png
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多个文件,这是绝对不允许的。
为了解决这个问题我们做了代码隔离(只是简单的展示隔离的原理,真正的隔离代码要比这个复杂的多),如下代码:


ImageLoader隔离.png
//我们定义自己的图片加载接口
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.各种基类代码:

BaseActivityBaseApplicationBaseFragmentBaseAdapter,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路由配置,全局的PathActivity传递数据的Key
2.多个module需要的JavaBean,例如UserInfo
3.多个module需要的静态常量

坑:

1.App主项目依赖本地aar库。依赖路径如下图:

App.png

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’

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容