Android组件化架构案例实战

本文介绍ARouter路由框架在组件化项目中的应用,包括配置步骤、实战案例及效果展示。

目录

写在前面

一、ARouter路由框架

1.1、ARouter基本介绍

1.2、ARouter接入说明

二、组件化项目配置

2.1、Gradle配置

2.2、app module

2.3、业务组件

三、ARouter代码实战


写在前面

金秋尚徘徊,冬令逐沓来!早上才知昨日悄然立冬,节日快乐哈!例行问候一下,各位可以前排落座,品茶赏月,顺便一观鄙人之拙作,就此谢过!

前几天我写了一篇《安卓组件化架构设计》的文章,那篇文章介绍了如何实现组件化路由框架,想自己写路由框架的可以看一看哈。今天这篇是给大家带来了一个案例实操,使用的是阿里开源的第三方路由框架ARouter,毕竟人家是大厂有专门的团队在维护,应该更稳定一些吧,而且使用的人还是很多的,所以决定搞一下。但是需要说明的是:组件化只是一种思想,把思想转化为实践的具体方式多种多样,自己能实现类似的路由框架当然更好啊!

先来看一下今天的案例效果吧:

最左侧动图是集成化模式下运行的效果,右侧四张图分别是四个业务模块独立运行的结果,最后一张图是集成化和组件化模式下安装到手机桌面上的效果:

          

项目完整代码已上传GitHub:https://github.com/JArchie/AndroidComponentDesign

一、ARouter路由框架

1.1、ARouter基本介绍

今天既然是组件化项目架构实战案例,那么咱们首先需要考虑一下组件化项目的路由该如何实现?业内耳熟能详的肯定是阿里巴巴开源的一款ARouter路由框架了,咱们今天也用它来实现,毕竟是阿里的,大厂有保障,当然了有能力的可以自行开发,能力一般的求稳的可以接着往下看了。

项目官方中文文档:https://github.com/alibaba/ARouter/blob/master/README_CN.md

ARouter官方的功能介绍如下:

  1. 支持直接解析标准URL进行跳转,并自动注入参数到目标页面中
  2. 支持多模块工程使用
  3. 支持添加多个拦截器,自定义拦截顺序
  4. 支持依赖注入,可单独作为依赖注入框架使用
  5. 支持InstantRun
  6. 支持MultiDex(Google方案)
  7. 映射关系按组分类、多级管理,按需初始化
  8. 支持用户指定全局降级与局部降级策略
  9. 页面、拦截器、服务等组件均自动注册到框架
  10. 支持多种方式配置转场动画
  11. 支持获取Fragment
  12. 完全支持Kotlin以及混编(配置见文末 其他#5)
  13. 支持第三方 App 加固(使用 arouter-register 实现自动注册)
  14. 支持生成路由文档
  15. 提供 IDE 插件便捷的关联路径和目标类
  16. 支持增量编译(开启文档生成后无法增量编译)

典型的应用场景有:

  1. 从外部URL映射到内部页面,以及参数传递与解析
  2. 跨模块页面跳转,模块间解耦
  3. 拦截跳转过程,处理登陆、埋点等逻辑
  4. 跨模块API调用,通过控制反转来做组件解耦

这些大家随意瞅瞅就行了,不必深究,下面咱们重点来说一下它的用法。

1.2、ARouter接入说明

①、添加依赖

需要在你的module的build.gradle文件中添加以下内容:

android {
    defaultConfig {
        //以下内容必须放在defaultConfig闭包中
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
}

dependencies {
    // 替换成最新版本, 需要注意的是api
    // 要与compiler匹配使用,均使用最新版可以保证兼容
    implementation 'com.alibaba:arouter-api:1.5.0'
    annotationProcessor 'com.alibaba:arouter-compiler:1.2.2'
}

上面添加的是注解处理器传递参数的一个配置,就按照上面那样配就OK了,下面的两个第一个是arouter的api的依赖库,第二个是arouter的注解处理器,看过我上一篇组件化分析文章的人应该知道这个注解处理器的作用是干嘛的了,其实就是帮助我们自动生成一些类文件。

②、ARouter初始化

官方推荐放在Application中进行初始化,我们照着做就OK了:

if (isDebug()) {           // 这两行必须写在init之前,否则这些配置在init过程中将无效
    ARouter.openLog();     // 打印日志
    ARouter.openDebug();   // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(mApplication); // 尽可能早,推荐在Application中初始化

③、添加注解

// 在支持路由的页面上添加注解(必选)
// 这里的路径需要注意的是至少需要有两级,/xx/xx
@Route(path = "/test/activity")
public class YourActivity extend Activity {
    ...
}

注意这里必须要有两级,格式也必须按照它的这种格式来,要有两个"/",第一级实际上是你的moduleName,第二级是对应module下的Activity或者Fragment。

④、发起路由

// 1. 应用内简单的跳转(通过URL跳转在'进阶用法'中)
ARouter.getInstance().build("/test/activity").navigation();

// 2. 跳转并携带参数
ARouter.getInstance().build("/test/1")
            .withLong("key1", 666L)
            .withString("key3", "888")
            .withObject("key4", new Test("Jack", "Rose"))
            .navigation();

采用了构建者模式传入要跳转的路径,可携带不同类型的参数。

关于ARouter的使用方式基本上就是上面的几步,其它比较细节性的使用可以具体的参考官方文档的说明,我这里就不再一一细说了,下面来看我们案例中的具体实现。

二、组件化项目配置

关于Gradle的一些语法说明,我就不再说了,具体参考上一篇组件化分析中的介绍:Gradle语法介绍,接下来直接来看我们今天要做的一个实际的案例。

这里同样的还是分为四个业务模块:首页、产品、订单、个人中心,如下图所示:

所以,按照这个思路我们开始进行组件化项目的架构设计。

首先,app壳工程只是一个壳子,没有什么实际的业务,我们新建四个Android Library作为我们的四个业务组件,以ft_开头指代业务,将公共的业务再抽离出一个Common组件作为公共业务基础库,非业务组件我们以lib_开头,命名都做到见名知意就行了,实际开发中你还可以继续对组件进行拆分,比如:可以抽取网络请求组件network,图片加载组件imageLoader,各种第三方能力集成组件ability等等,最终我们按照这个原则将项目框架搭建起来,搭建完成之后的代码结构看起来还是很美观的,如下图所示:

咱们的四个业务组件也可以独立作为application单独调试,这里我们仿照上一次文章中介绍的还是全局使用一个布尔类型的变量来进行控制,集成化模式时它们四个就是Android Library,作为app module的依赖库使用,组件化模式时可以作为独立的app运行。

好了,关于案例的情况就分析到这里,下面来看下实际的操作吧!

2.1、Gradle配置

首先对于Gradle配置方面,我们同样采用抽离公共配置文件自定义一个config.gradle:

//添加多个自定义属性,可以通过ext代码块
ext {

    //生产/开发环境(或者叫做正式/测试环境)
    //false:组件化模式(子模块可以独立运行)true:集成化模式(打包整个项目apk,子模块不可独立运行)
    isRelease = true

    //生产/开发环境域名地址
    hostUrl = [
            debug  : "http://www.111.com/debug/",
            release: "http://www.222.com/release/"
    ]

    //建立Map存储,对象名、key都可以自定义,groovy的语法糖非常灵活(字典)
    androidConfig = [
            compileSdkVersion: 29,
            buildToolsVersion: "29.0.2",
            minSdkVersion    : 14,
            targetSdkVersion : 29,
            versionCode      : 1,
            versionName      : "1.0"
    ]

    //ApplicationId(应用包名)
    appId = [
            app     : "com.jarchie.component",
            home    : "com.jarchie.component.home",
            product : "com.jarchie.component.product",
            order   : "com.jarchie.component.order",
            personal: "com.jarchie.component.personal"
    ]

    //依赖库版本:调用处的语法为:${keyName}
    depVersions = [
            appcompatVersion  : "1.2.0",
            consVersion       : "2.0.1",
            recyclerVersion   : "1.0.0",
            materialVersion   : "1.0.0",
            aApiVersion       : "1.5.0",
            aCompilerVersion  : "1.2.2",
            vlayoutVersion    : "1.0.3",
            bannerVersion     : "1.4.9",
            marqueeviewVersion: "1.3.1",
            glideVersion      : "3.7.0"
    ]

    //依赖库
    depConfig = [
            appcompat       : "androidx.appcompat:appcompat:${depVersions.appcompatVersion}",
            constraintlayout: "androidx.constraintlayout:constraintlayout:${depVersions.consVersion}",
            recyclerview    : "androidx.recyclerview:recyclerview:${depVersions.recyclerVersion}",
            material        : "com.google.android.material:material:${depVersions.materialVersion}",
            arouterApi      : "com.alibaba:arouter-api:${depVersions.aApiVersion}",
            arouterCompiler : "com.alibaba:arouter-compiler:${depVersions.aCompilerVersion}",
            vlayout         : "com.alibaba.android:vlayout:${depVersions.vlayoutVersion}",
            banner          : "com.youth.banner:banner:${depVersions.bannerVersion}",
            marqueeview     : "com.sunfusheng:marqueeview:${depVersions.marqueeviewVersion}",
            glide           : "com.github.bumptech.glide:glide:${depVersions.glideVersion}"
    ]

}

然后在根工程的build.gradle文件中引入:

//根目录下的build.gradle头部引入自定义config.gradle,相当于layout布局中加入include标签
apply from: "config.gradle"

buildscript {
    repositories {
        //添加阿里云镜像
        maven{
            url "http://maven.aliyun.com/nexus/content/groups/public/"
        }
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.3'
    }
}

allprojects {
    repositories {
        maven{
            url "http://maven.aliyun.com/nexus/content/groups/public/"
        }
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

接着来修改app module的build.gradle文件,使用公共配置中的参数:

apply plugin: 'com.android.application'

//赋值与引用
def androidConfig = rootProject.ext.androidConfig
def appId = rootProject.ext.appId
def depConfig = rootProject.ext.depConfig
def hostUrl = rootProject.ext.hostUrl

android {
    //上面定义了三个属性,分别指向公共配置config.gradle中定义的集合,然后根据集合中的key来获取对应的值
    compileSdkVersion androidConfig.compileSdkVersion
    buildToolsVersion androidConfig.buildToolsVersion
    defaultConfig {
        applicationId appId.app
        minSdkVersion androidConfig.minSdkVersion
        targetSdkVersion androidConfig.targetSdkVersion
        versionCode androidConfig.versionCode
        versionName androidConfig.versionName
        buildConfigField("boolean", "isRelease", String.valueOf(isRelease))

        //在gradle文件中配置选项参数值(用于APT传参接收)
        //注意:必须写在defaultConfig节点下
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }

    }

    //签名配置有个隐形的坑(必须写在buildTypes之前)
    signingConfigs {
        debug {
            //注意:填错了编译不通过还找不到问题
            storeFile file('/Users/Jarchie/.android/debug.keystore')
            storePassword "android"
            keyAlias "androiddebugkey"
            keyPassword "android"
        }
        release {
            //签名证书文件
            storeFile file('/Users/Jarchie/Desktop/AndroidProjects/AndroidComponentDesign/component.jks')
            //签名证书文件的密码
            storePassword "123456"
            //密钥别名
            keyAlias "zujian"
            //密钥密码
            keyPassword "123456"
            //是否开启v2打包
            v2SigningEnabled true
        }
    }

    buildTypes {
        //public void buildConfigField(@NonNull String type,@NonNull String name,@NonNull String value){}
        //此方法接收三个非空参数:①确定值的类型;②指定key的名字;③传值(必须是String)
        //注意:不能在android根节点,只能在defaultConfig或者buildTypes节点下
        debug {
            signingConfig signingConfigs.debug
            buildConfigField("String", "DEBUG_URL", "\"${hostUrl.debug}\"")
        }
        release {
            minifyEnabled false
            signingConfig signingConfigs.release
            buildConfigField("String", "RELEASE_URL", "\"${hostUrl.release}\"")
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation project(':lib_common')
    annotationProcessor depConfig.arouterCompiler

    //如果是集成化模式,在发布版本时,各个模块都不能独立运行了
    if (isRelease) {
        implementation project(':ft_home')
        implementation project(':ft_product')
        implementation project(':ft_order')
        implementation project(':ft_personal')
    }
}

其它几个业务组件也都是类似的,大家对照着app module进行修改就行了,需要注意的是业务组件在集成化模式的环境下是没有applicationId这一项的,需要分情况处理。

2.2、app module

这里我抽离了一个lib_common公共依赖库,将需要依赖的第三方库都放进这个module中进行依赖,然后各个组件直接依赖lib_common就可以了:

dependencies {
    api fileTree(dir: 'libs', include: ['*.jar'])
    //依赖第三方库
    api depConfig.appcompat
    api depConfig.constraintlayout
    api depConfig.recyclerview
    api depConfig.material
    api depConfig.arouterApi
    api depConfig.vlayout
    api depConfig.banner
    api depConfig.marqueeview
    api depConfig.glide
}

在lib_common库中,我定义了一些Base基类,将公共代码全都放在这个库中,并且将自定义的全局Application类也放在这里,在application进行ARouter的初始化操作:

/**
 * 作者:created by Jarchie
 * 时间:2020/9/9 17:16:25
 * 邮箱:jarchie520@gmail.com
 * 说明:应用Application类
 */
public class BaseApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        if (BuildConfig.DEBUG) {
            ARouter.openLog();
            ARouter.openDebug();
        }
        ARouter.init(this);
    }
}

对于app module中,我们只定义了一个类就是项目的主Activity,用它来承载四个Fragment,个人觉得这样写反而简单些,没必要为了架构而架构搞的花里胡哨的:

/**
 * 作者:created by Jarchie
 * 时间:2020/9/9 10:16:25
 * 邮箱:jarchie520@gmail.com
 * 说明:应用主Activity
 */
public class MainActivity extends BaseActivity implements BottomNavigationView.OnNavigationItemSelectedListener {
    private BottomNavigationView mNavigationView;
    private Fragment mHomeFgt, mProductFgt, mOrderFgt, mPersonalFgt;

    @Override
    public int getLayoutId() {
        return R.layout.activity_main;
    }

    @Override
    protected void initView() {
        mNavigationView = findViewById(R.id.main_navigation);
        setSelect(0);
    }

    @Override
    protected void initListener() {
        mNavigationView.setOnNavigationItemSelectedListener(this);
    }

    @Override
    protected void initData() {

    }

    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
        switch (menuItem.getItemId()){
            case R.id.tab1:
                setSelect(0);
                return true;
            case R.id.tab2:
                setSelect(1);
                return true;
            case R.id.tab3:
                setSelect(2);
                return true;
            case R.id.tab4:
                setSelect(3);
                return true;

        }
        return false;
    }

    //切换Fragment选项
    private void setSelect(int pos) {
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        hideFragment(transaction);
        switch (pos) {
            case 0: //首页
                if (mHomeFgt == null) {
                    mHomeFgt = new HomeFragment();
                    transaction.add(R.id.id_content, mHomeFgt);
                } else {
                    transaction.show(mHomeFgt);
                }
                break;
            case 1: //产品
                if (mProductFgt == null) {
                    mProductFgt = new ProductFragment();
                    transaction.add(R.id.id_content, mProductFgt);
                } else {
                    transaction.show(mProductFgt);
                }
                break;
            case 2: //订单
                if (mOrderFgt == null) {
                    mOrderFgt = new OrderFragment();
                    transaction.add(R.id.id_content, mOrderFgt);
                } else {
                    transaction.show(mOrderFgt);
                }
                break;
            case 3: //我的
                if (mPersonalFgt == null) {
                    mPersonalFgt = new PersonalFragment();
                    transaction.add(R.id.id_content, mPersonalFgt);
                } else {
                    transaction.show(mPersonalFgt);
                }
                break;
        }
        transaction.commitAllowingStateLoss();
    }

    //隐藏Fragment
    private void hideFragment(FragmentTransaction transaction) {
        if (mHomeFgt != null) {
            transaction.hide(mHomeFgt);
        }
        if (mProductFgt != null) {
            transaction.hide(mProductFgt);
        }
        if (mOrderFgt != null) {
            transaction.hide(mOrderFgt);
        }
        if (mPersonalFgt != null) {
            transaction.hide(mPersonalFgt);
        }
    }
}

2.3、业务组件

对于我们的业务组件,首先要分两种情况来看,一种是集成化模式下它是作为组件库为上层提供依赖,一种是组件化模式下作为独立应用调试运行。

这里有两个点需要注意一下,第一点上面也已经说过了,集成化模式下它是没有applicationId的,这点需要注意:

if (!isRelease) { //如果是集成化模式,不能有applicationId
    applicationId appId.home //组件化模式独立运行时才可以有applicationId
}

第二点对于清单文件Manifest这两种情况也是不一样的,所以我们在src/main文件夹下新建了一个debug包,将单独运行测试时需要跑的manifest文件定义在这里,组件化模式集成打包的清单文件仍然放在src/main目录下,为啥它俩不同呢,需要说明一下,因为集成化打包时你的app启动的activity肯定是app module中的MainActivity,而单独运行时你需要针对这个业务模块定义测试的启动项Activity,在我们的代码中的src/main/java/packageName目录下还新建了测试代码的debug包,这个包用来存放单独调试时需要新建的类文件,这样一说相信你应该能明白了,我们来看一下它二者的具体定义吧:

集成化:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.jarchie.component.home">
    <!-- if you want to load images from the internet -->
    <uses-permission android:name="android.permission.INTERNET" />

    <!-- if you want to load images from a file OR from the internet -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".debug.Home_MainActivity"/>
    </application>

</manifest>

独立应用:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.jarchie.component.home">
    <!-- if you want to load images from the internet -->
    <uses-permission android:name="android.permission.INTERNET" />

    <!-- if you want to load images from a file OR from the internet -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".debug.Home_MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

所以这也就是为什么我们在每个业务组件module中进行了这样一项配置的原因,并且排除掉debug包下的测试类文件:

    //配置资源路径,方便测试环境,打包不集成到正式环境
    sourceSets{
        main{
            if (!isRelease){
                //如果是组件化模式,需要单独运行时
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            }else {
                //集成化模式,整个项目打包到apk
                manifest.srcFile 'src/main/AndroidManifest.xml'
                java{
                    //release时debug目录下文件不需要合并到主工程
                    exclude '**/debug/**'
                }
            }
        }
    }

OK,说到这里相信你已经对这个项目的整体架构有了一个比较清晰的认识了,接着我们来测试一下路由能否正常使用吧。

三、ARouter代码实战

首先来说一下我们测试ARouter使用情况的应用场景:点击首页模块即app module中的”新品“按钮,让它跳转到产品模块即product module中的产品页面NewProductActivity中,并且还携带了参数String=”新品“这样一个字符串,由于本人比较懒,所以NewProductActivity中的内容,我是直接把ProductFragment拿过来了,原本的ProductFragment的标题是”产品“,为了区分是否接收到字符串,我们在跳转之后将ProductFragment的标题修改为带有接收字符串新品的标题,应该都能明白吧,不明白的回到上面看我发的动态图你就明白了。

OK,首先在lib_common中新增一个全局的PageUrl类,这个类用来存放组件化需要跳转页面的所有路径,这也是为了方便管理:

public class PathUrl {

    //跳转新品页面
    public static final String PATH_NEW_PRODUCT="/product/NewProductActivity";
}

然后在HomeFragment中编写跳转的代码:

ARouter.getInstance().build(PathUrl.PATH_NEW_PRODUCT)
                                        .withString("title", ITEM_NAMES[position])
                                        .navigation();

接着在NewProductActivity页面中接收参数title,并将title再传到内部的ProductFragment中:

/**
 * 作者: 乔布奇
 * 日期: 2020-11-01 11:32
 * 邮箱: jarchie520@gmail.com
 * 描述: 新品页面
 */
@Route(path = PathUrl.PATH_NEW_PRODUCT)
public class NewProductActivity extends BaseActivity {
    @Autowired
    public String title;

    private ProductFragment mFragment;

    @Override
    public int getLayoutId() {
        return R.layout.activity_new_product_layout;
    }

    @Override
    protected void initView() {
        ARouter.getInstance().inject(this);
        mFragment = new ProductFragment();
        if (!TextUtils.isEmpty(title)) {
            Bundle bundle = new Bundle();
            bundle.putString("title", title);
            mFragment.setArguments(bundle);
        }
        getSupportFragmentManager()
                .beginTransaction()
                .add(R.id.mContent, new ProductFragment())
                .commit();
    }
}

最后在ProductFragment中再次修改标题:

/**
 * 作者: 乔布奇
 * 日期: 2020-11-01 20:31
 * 邮箱: jarchie520@gmail.com
 * 描述: 产品模块
 */
public class ProductFragment extends BaseFragment {
    private TextView mTitle;
    private String title;

    @Override
    public int getLayoutId() {
        return R.layout.fragment_product_layout;
    }

    @Override
    protected void initView(View view) {
        title = getActivity().getIntent().getStringExtra("title");
        mTitle = view.findViewById(R.id.mTitle);
        if (!TextUtils.isEmpty(title)) {
            mTitle.setText("ARouter" + title + "测试页面");
        }
    }
}

这个具体的效果在一开始展示的效果图中都已经有了,这里就不再发了哈!

OK,关于ARouter在组件化中的具体实践就简单说这么多,实际上ARouter还有很多其它的用法,这个需要大家自己去发掘,比如自定义拦截器,解耦服务Service等等,这里就不多说了,毕竟我这只是个小案例,相信大家在实际使用的过程中都能够一点一点的给探索出来的。

天也不早了,人也都早了,应该是到饭点了,各位早点休息尅饭吧,咱们下期再会!

祝:前程似锦,工作顺利!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值