Android组件化开发思想与实践

什么是组件化

项目按功能拆分红功若干个组件,每一个组件负责相应的功能,如login、pay、live。组件化与模块化相似,但不一样的是模块化是以业务为导向,组件化是以功能为导向。组件化的颗粒度更细,一个模块里可能包含多个组件。实际开发中通常是模块化与组件化相结合的方式。java

为何要组件化

(1)提升复用性避免重复造轮子,不一样的项目能够共用同一组件,提升开发效率,下降维护成本。android

(2)项目按功能拆分红组件,组件之间作到低耦合、高内聚,有利于代码维护,某个组件须要改动,不会影响到其余组件。git

组件化方案

组件化是一种思想,团队在使用组件化的过程当中没必要拘泥于形式,能够根据本身负责的项目大小和业务需求的须要制定合适的方案,以下图就是一种组件化结构设计。 github

组件化结构图.png

  • 宿主app数据库

    在组件化中,app能够认为是一个入口,一个宿主空壳,负责生成app和加载初始化操做。api

  • 业务层安全

    每一个模块表明了一个业务,模块之间相互隔离解耦,方便维护和复用。bash

  • 公共层网络

    既然是base,顾名思义,这里面包含了公共的类库。如Basexxx、Arouter、ButterKnife、工具类等app

  • 基础层

    提供基础服务功能,如图片加载、网络、数据库、视频播放、直播等。

注:以上结构只是示例,其中层级的划分和层级命名并非定性的,只为更好的理解组件化。

组件化面临的问题

跳转和路由

Activity跳转分为显示和隐示:

//显示跳转
Intent intent = new Intent(cotext,LoginActivity.class);
startActvity(intent)
复制代码
//隐示跳转
Intent intent = new Intent();
intent.setClassName("app包名" , "activity路径");
intent.setComponent(new Component(new Component("app报名" , "activity路径")));
startActivity(intent);
复制代码

一、显示跳转,直接依赖,不符合组件化解耦隔离的要求。

二、对于隐示跳转,若是移除B的话,那么在A进行跳转时就会出现异常崩溃,咱们经过下面的方式来进行安全处理

//隐示跳转
Intent intent = new Intent();
intent.setClassName("app包名" , "activity路径");
intent.setComponent(new Component(new Component("app报名" , "activity路径")));
if (intent.resolveActivity(getPackageManager()) != null) {
    startActivity(intent);
}
startActivity(intent);
复制代码

原生推荐使用隐示跳转,不过在组件化项目中,为了更优雅的实现组件间的页面跳转能够结合路由神器ARouter,ARouter相似中转站经过索引的方式无需依赖,达到了组件间解耦的目的。

Aouter使用方式以下:

一、由于ARouter是全部模块层组件都会用到因此咱们能够在Base中引入

api 'com.alibaba:arouter-api:1.5.0'
annotationProcessor 'com.alibaba:arouter-compiler:1.2.2'
复制代码

二、在每一个子module里添加

android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
}
复制代码

annotationProcessor会经过javaCompileOptions这个配置来获取当前module的名字。

三、在Appliction里对ARouter进行初始化,由于ARouter是全部的模块层组件都会用到,因此它的初始化放在BaseAppliction中完成。

public class BaseApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        initRouter(this);
    }

    public void initRouter(Application application) {
        if (BuildConfig.DEBUG) {    // 这两行必须写在init以前,不然这些配置在init过程当中将无效
            ARouter.openLog();      //打印日志
            ARouter.openDebug();    // 开启调试模式(若是在InstantRun模式下运行,必须开启调试模式!线上版本须要关闭,不然有安全风险)
        }
        ARouter.init(application);  //尽量早,推荐在Application中初始化
    }
}
复制代码

四、在Activity中添加注解Route

public interface RouterPaths {
    String LOGIN_ACTIVITY = "/login/login_activity";
}
复制代码
// 在支持路由的页面上添加注解(必选)
// 这里的路径须要注意的是至少须要有两级,/xx/xx
@Route(path = RouterPaths.LOGIN_ACTIVITY)
public class LoginActivity extends BaseActivity {
}
复制代码

path是指跳转路径,要求至少两级,即/xx/xx的形式,第一个xx是指group,若是不一样module中出现相同的group会报错,因此建议group用module名称标识。

五、发起跳转操做

ARouter.getInstance().build(RouterPaths. LOGIN_ACTIVITY).navigation();
复制代码

ARouter的还有不少其余功能,这里不做详细说明。

Aplication动态加载

Application做为程序的入口一般作一些初始化,如上面提到的ARouter,因为ARouter是全部模块层组件都要用到,因此把它放在BaseApplication进行初始化。若是某个初始化操做只属于某个模块,为了下降耦合,咱们应该把该初始化操做放在对应模块module的Application里。以下:

一、在BaseModule定义接口

public interface BaseApplicationImpl {
    void init();
    ...
}
复制代码

二、在ModuleConfig中进行配置

public interface ModuleConfig {

    String LOGIN = "com.linda.login.LoginApplication";

    String DETAIL = "com.linda.detail.DetailApplication";

    String PAY = "com.linda.pay.PayApplication";

    String[] modules = {
            LOGIN, DETAIL, PAY
    };

}
复制代码

三、在BaseApplicatiion经过反射的方式获取各个module中Application的实例并调用init方法。

public abstract class BaseApplication extends Application implements BaseApplicationImpl {

    @Override
    public void onCreate() {
        super.onCreate();
        initComponent();
        initARouter();
    }

    /**
     * 初始化各组件
     */
    public void initComponent() {
        for (String module : ModuleConfig.modules) {
            try {
                Class clazz = Class.forName(module);
                BaseApplicationImpl baseApplication = (BaseApplicationImpl) clazz.newInstance();
                baseApplication.init();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        }
    }
    ...
}
复制代码

四、子module中实现init方法,并进行相关初始化操做

public class LiveApplication extends BaseApplication {
    public void init() {
    //在这里作一些的Live相关的初始化操做
    }
}
复制代码
模块间通讯

BroadcastReceiver:系统提供,比较笨重,使用不够优雅。

EventBus:使用简单优雅,将发送这与接收者解耦,2.x使用反射方式比较耗性能,3.x使用注解方式比反射快得多。

可是有些状况是BroadcastReceiver、EventBus解决不了的,例如想在detail模块中获取mine模块中的数据。由于detail和mine都依赖了base,因此咱们能够借助base来实现。

一、在base中定义接口并继承ARouter的IProvider。

public interface IMineDataProvider extends IProvider {
    String getMineData();
}
复制代码

二、在mine模块中新建MineDataProvider类实现IMineDataProvider,并实现getMineData方法

@Route(path = RouterPaths.MINE_DATA_PROVIDER)
public class MineDataProvider implements IMineDataProvider {

    @Override
    public String getMineData() {
        return "***已获取到mine模块中的数据***";
    }

    @Override
    public void init(Context context) {

    }
}
复制代码

三、在detail中获取MineDataProvider实例并调用IMineDataProvider接口中定义的方法

IMineDataProvider mineDataProvider = (IMineDataProvider) ARouter.getInstance().build(RouterPaths.MINE_DATA_PROVIDER).navigation();
     if (mineDataProvider != null) {
          mGetMineData.setText(mineDataProvider.getMineData());
     }
复制代码
资源冲突

组件化项目中有不少个module,这就不免会出现module中资源命名相同而引发引用错误的状况。为此咱们能够在每一个module的build.gradle文件进行以下配置(例如login模块)。

resourcePrefix "login_"
复制代码

全部的资源必须以指定的字符串(建议module名称)作前缀,否则会报错。不过这种方式只限定与xml文件,对图片资源无效,图片资源仍须要手动修改。

//布局文件命名示例
login_activity_login.xml
复制代码
<resources>
    <!--字符串资源命名示例-->
    <string name="login_app_name">Login</string>
</resources>
复制代码
单个组件运行调试

当项目愈来愈庞大时,编译或运行一次就须要花费很长时间,而组件化能够经过配置对每一个模块进行单独调试,大大提升了开发效率。 咱们须要对每一个module进行以下配置:

一、在项目根目录新建common_config.gradle文件并声明变量isModuleDebug;

project.ext {
    //是否容许module单独调试
    isModuleDebug = false
}
复制代码

二、引入common_config配置,另外由于组件化中每一个module都是一个library,如要单独运行调试须要将library换成application,在module的build.gradle中文件中作以下修改:

//引入common_config配置
apply from: "${rootProject.rootDir}/common_config.gradle"

if (project.ext.isModuleDebug.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
复制代码
android {
    defaultConfig {
        if (project.ext.isModuleDebug.toBoolean()) {
            // 单独调试时须要添加 applicationId 
            applicationId "com.linda.login"
        }
        ...
    }
    
    sourceSets {
        main {
             //在须要单独调试的module的src/main目录下新建manifest目录和AndroidManifest文件
            // 单独调试与集成调试时使用不一样的 AndroidManifest.xml 文件
            if (project.ext.isModuleDebug.toBoolean()) {
                manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }
}
复制代码

关于两个清单文件的不一样之处以下:

<!--单独调试-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.linda.login">
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/login_app_name"
        android:supportsRtl="true"
        android:theme="@style/base_AppTheme">
        <activity android:name=".ui.LoginActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>
复制代码
<!-- 集成调试-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.linda.login">
    <application
        android:allowBackup="true"
        android:label="@string/login_app_name"
        android:supportsRtl="true"
        android:theme="@style/base_AppTheme">
        <activity android:name=".ui.LoginActivity" />
    </application>
</manifest>
复制代码

三、若是module单独调试,那么在app就不能再依赖此module,由于此时app和module都是project,project之间不能相互依赖,在app的build.gradle文件中作以下修改

dependencies {
    if (!project.ext.isModuleDebug) {
        implementation project(path: ':detail')
        implementation project(path: ':login')
        implementation project(path: ':pay')
    }
    implementation project(path: ':main')
    implementation project(path: ':home')
    implementation project(path: ':mine')
}
复制代码

四、最后将isModuleDebug改成true,而后编译,即可以看到login、detail、pay模块能够独立运行调试了。

组件单独运行调试.png

组件单独运行调试.png

组件化Demo下载地址

相关文章
相关标签/搜索