Android组件化
西邮王嘉尔 人气:0什么是组件化?
一个大型APP版本一定会不断的迭代,APP里的功能也会随之增加,项目的业务也会变的越来越复杂,这样导致项目代码也变的越来越多,开发效率也会随之下降。并且单一工程下代码耦合严重,每修改一处代码后都要重新编译,非常耗时,单独修改的一个模块无法单独测试。
组件化架构的目的是让各个业务变得相对独立,各个组件在组件模式下可以独立开发调试,集成模式下又可以集成到“app壳工程”中,从而得到一个具有完整功能的APP。
组件化每一个组件都可以是一个APP可以单独修改调试,而不影响总项目。
为什么使用组件化?
编译速度: 可以但需测试单一模块,极大提高了开发速度
超级解耦: 极度降低了模块间的耦合,便于后期的维护和更新
功能重用: 某一块的功能在另外的组件化项目中使用只需要单独依赖这一模块即可
便于团队开发: 组件化架构是团队开发必然会选择的一种开发方式,它能有效的使团队更好的协作
一步步搭建组件化
这里以演示为例,只设置登录这一个功能组件
组件化开发要注意的几点问题 :
- 要注意包名和资源文件命名冲突问题
Gradle
中的版本号的统一管理- 组件在
AppIication
和Library
之间如何做到随意切换 AndroidManifest. xml
文件的区分Library
不能在Gradle
文件中有applicationId
这里以演示为例,只设置登录和个人中心这两个功能组件
1.新建模块
并且在module里新建一个activity
到这里我们看到login
和我们的app
都在有一个绿点证明创建成功
个人中心member
模块创建同理,并且每个模块目前都可以独立运行。
2.统一Gradle版本号
每一个模块都是一个application
,所以每个模块都会有一个build.gradle
,各个模块里面的配置不同,我们需要重新统一Gradle
在主模块创建config.gradle
在config.gradle
里去添加一些版本号
ext{ android = [ compileSdkVersion :30, buildToolsVersion: "30.0.2", applicationId :"activitytest.com.example.moduletest", minSdkVersion: 29, targetSdkVersion :30, versionCode :1, versionName :"1.0", ] androidxDeps = [ "appcompat": 'androidx.appcompat:appcompat:1.1.0', "material": 'com.google.android.material:material:1.1.0', "constaraintlayout": 'androidx.constraintlayout:constraintlayout:1.1.3', ] commonDeps = [ "arouter_api" : 'com.alibaba:arouter-api:1.5.1', "glide" : 'com.github.bumptech.glide:glide:4.11.0' ] annotationDeps = [ "arouter_compiler" : 'com.alibaba:arouter-compiler:1.5.1' ] retrofitDeps = [ "retrofit" : 'com.squareup.retrofit2:retrofit:2.9.0', "converter" : 'com.squareup.retrofit2:converter-gson:2.9.0', "rxjava" : 'io.reactivex.rxjava2:rxjava:2.2.20', "rxandroid" : 'io.reactivex.rxjava2:rxandroid:2.1.1', "adapter" : 'com.squareup.retrofit2:adapter-rxjava2:2.9.0' ] androidxLibs = androidxDeps.values() commonLibs = commonDeps.values() annotationLibs = annotationDeps.values() retrofitLibs = retrofitDeps.values() }
在主模块的build.gradle
里添加
apply from: "config.gradle"
在各模块中去引用这些版本号
引用格式如下,两种写法均可
compileSdkVersion rootProject.ext.android["compileSdkVersion"] buildToolsVersion rootProject.ext.android.buildToolsVersion
引用前:
引用后:
并且使用同样的方法,我们还可以统一我们的依赖库在config.gradle
里去添加我们要依赖的库,并在各个模块中去添加依赖
implementation rootProject.ext.dependencies.publicImplementation
也可以采用第二种写法
dependencies = [ "appcompat" : 'androidx.appcompat:appcompat:1.2.0', "material" : 'com.google.android.material:material:1.2.1', "constraintLayout" : 'androidx.constraintlayout:constraintlayout:2.0.4',//约束性布局 //test "junit" : "junit:junit:4.13.1", "testExtJunit" : 'androidx.test.ext:junit:1.1.2',//测试依赖,新建项目时会默认添加,一般不建议添加 "espressoCore" : 'androidx.test.espresso:espresso-core:3.3.0',//测试依赖,新建项目时会默认添加,一般不建议添加 ]
添加依赖:
dependencies { implementation rootProject.ext.dependencies.appcompat implementation rootProject.ext.dependencies["constraintLayout"] testImplementation rootProject.ext.dependencies["junit"] androidTestImplementation rootProject.ext.dependencies["testExtJunit"] androidTestImplementation rootProject.ext.dependencies["espressoCore"] }
3.创建基础库
和新建module
一样,这里需要新建一个library
我们把它命名为Baselibs
同样需要统一版本号,由于这是一个library
模块,所以它不需要applicationId
我们一样可以把它写进config.gradle
other:[path:':Baselibs']
在每个模块去调用
implementation project(rootProject.ext.dependencies.other)
同理,当本地库为单独所用,我们可以直接调用,而不需要将其写入config.gradle
,两种方法选择合适使用即可。
implementation project(':Baselibs')
但有时因为gradle
版本问题,我们可能无法依赖到这些公共库,因为我们在config.gradle
里是以数组形式定义的,这时我们可以同for-each循环的方法将其依次导入config.gradle
里
dependencies = [ ...... other:[':Baselibs'] ]
其他模块的build.gradle
dependencies { ...... rootProject.ext.dependencies.other.each{ implementation project(it) }
4.组件模式和集成模式转换
在主模块gradle.properties
里添加布尔类型选项。
在各个模块的build.gradle
里添加更改语句
if(is_Module.toBoolean()){ apply plugin: 'com.android.application' }else{ apply plugin: 'com.android.library' }
每个模块的applicationId
也需要处理
if(is_Module.toBoolean()){ applicationId "activitytest.com.example.login" }
当我们将is_module
改为false时,再次运行编译器我们的模块都不能单独运行了
在app模块中添加判断依赖就可以在集成模式下将各模块添加到app主模块中
// 每加入一个新的模块,就需要在下面对应的添加一行 if (is_Module.toBoolean())]) { implementation project(path:':login') implementation project(path:':member') }
5.AndroidManifest的切换
为了单独开发加载不同的AndroidManifest
这里需要重新区分下。
在组件模块里的main
文件里新建manifest
文件夹
并且重写一个AndroidManifest.xml
文件,集成模式下,业务组件的表单是绝对不能拥有自己的 Application 和 launch 的 Activity的,也不能声明APP名称、图标等属性,总之app壳工程有的属性,业务组件都不能有,在这个表单中只声明了应用的主题,而且这个主题还是跟app壳工程中的主题是一致的
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.login"> <application android:theme="@style/Theme.MoudleTest"> <activity android:name=".LoginActivity"> </activity> </application> </manifest>
并且我们还要使其在不同的模式下加载不同的AndroidManifest
只需在各模块的build.gradle
里添加更改语句
sourceSets { main { if (is_Module.toBoolean()) { manifest.srcFile 'src/main/AndroidManifest.xml' } else { manifest.srcFile 'src/main/mainfest/AndroidManifest.xml' } } }
6.*业务Application切换
每个模块在运行时都会有自己的application,而在组件化开发过程中,我们的主模块只能有一个application,但在单独运行时又需要自己的application这里就需要配置一下。
在业务模块添加新文件夹命名module
在里面建一个application文件
并且我们在build.gradle
文件里配置module
文件夹使其在单独运行时能够运行单独的application
在配置manifest的语句中添加java.srcDir 'src/main/module'
sourceSets { main { if (is_Module.toBoolean()) { manifest.srcFile 'src/main/AndroidManifest.xml' java.srcDir 'src/main/module' } else { manifest.srcFile 'src/main/manifest/AndroidManifest.xml' } } }
同时我们在basic
基础层内新建application,用于加载一些数据的初始化
public class BaseApplication extends Application { @Override public void onCreate() { super.onCreate(); Log.e("fff","baseapplication"); } }
在业务模块内module
里重写该模块的application
public class LoginApplication extends BaseApplication { @Override public void onCreate() { super.onCreate(); } }
至此,组件化框架搭建结束
组件之间的跳转
这里采用阿里巴巴的开源库ARouter
来实现跳转功能,我会在以后的文章单独拿出一篇来一步步去解读Arouter
源码,让我们自己去搭建一个自己的路由
一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦
由 github 上 ARouter 的介绍可以知道,它可以实现组件间的路由功能。路由是指从一个接口上收到数据包,根据数据路由包的目的地址进行定向并转发到另一个接口的过程。这里可以体现出路由跳转的特点,非常适合组件化解耦。
要使用 ARouter 进行界面跳转,需要我们的组件对 Arouter 添加依赖,因为所有的组件都依赖了 Baselibs
模块,所以我们在 Baselibs
模块中添加 ARouter 的依赖即可。其它组件共同依赖的库也最好都放到 Baselibs
中统一依赖。
这里需要注意的是,arouter-compiler
的依赖需要所有使用到 ARouter 的模块和组件中都单独添加,不然无法在 apt 中生成索引文件,也就无法跳转成功。并且在每一个使用到 ARouter 的模块和组件的 build.gradle 文件中,其 android{} 中的 javaCompileOptions 中也需要添加特定配置。
1.添加依赖
在Baselibs
里的build.gradle
添加依赖
dependencies { api 'com.alibaba:arouter-api:1.3.1' // arouter-compiler 的注解依赖需要所有使用 ARouter 的 module 都添加依赖 annotationProcessor 'com.alibaba:arouter-compiler:1.1.4' } // 所有使用到 ARouter 的组件和模块的 build.gradle android { defaultConfig { ... javaCompileOptions { annotationProcessorOptions { arguments = [ moduleName : project.getName() ] } } } } dependencies { ... implementation project (':base') annotationProcessor 'com.alibaba:arouter-compiler:1.1.4' }
主模块需要对跳转模块进行依赖:
// 主项目的 build.gradle 需要添加对 login 组件和 share 组件的依赖 dependencies { // ... 其他 implementation project(':login') implementation project(':share') }
2.初始化ARouter
添加了对 ARouter 的依赖后,还需要在项目的 Application 中将 ARouter 初始化,我们这里将 ARouter 的初始化工作放到主模块Application 的 onCreate()
方法中,在应用启动的同时将 ARouter 初始化。
public class MainApplication extends Application { @Override public void onCreate() { super.onCreate(); // 初始化 ARouter if (isDebug()) { // 这两行必须写在init之前,否则这些配置在init过程中将无效 // 打印日志 ARouter.openLog(); // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险) ARouter.openDebug(); } // 初始化 ARouter ARouter.init(this); } private boolean isDebug() { return BuildConfig.DEBUG; } }
3.添加跳转
这里我们在首页添加登录
和分享
两个跳转页面。
login.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ARouter.getInstance().build("/login/login").navigation(); } });
share.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ARouter.getInstance().build("/share/share").navigation(); } });
然后,需要在登录和分享组件中分别添加 LoginActivity
和 ShareActivity
,然后分别为两个 Activity 添加注解 Route,其中path
是跳转的路径,这里的路径需要注意的是至少需要有两级,/xx/xx
@Route(path = "/login/login") public class Login extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); } }
@Route(path = "/share/share") public class Share extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_share); } }
这样就可以实现跳转了。
组件之间的数据传递
由于主项目与组件,组件与组件之间都是不可以直接使用类的相互引用来进行数据传递的,那么在开发过程中如果有组件间的数据传递时应该如何解决呢,这里我们可以采用 [接口 + 实现] 的方式来解决。
在Baselibs
基础库里定义组件可以对外提供访问自身数据的抽象方法的 Service。并且提供了一个 ServiceFactory
,每个组件中都要提供一个类实现自己对应的 Service 中的抽象方法。在组件加载后,需要创建一个实现类的对象,然后将实现了 Service 的类的对象添加到ServiceFactory
中。这样在不同组件交互时就可以通过 ServiceFactory
获取想要调用的组件的接口实现,然后调用其中的特定方法就可以实现组件间的数据传递与方法调用。
当然,ServiceFactory
中也会提供所有的 Service 的空实现,在组件单独调试或部分集成调试时避免出现由于实现类对象为空引起的空指针异常。
下面我们就按照这个方法来解决组件间数据传递与方法的相互调用这个问题,这里我们通过分享组件 中调用 登录组件 中的方法来获取登录状态是否登录这个场景来演示。
1.定义接口
其中 service文件夹中定义接口,LoginService
接口中定义了 Login 组件向外提供的数据传递的接口方法,EmptyService
中是 service 中定义的接口的空实现,ServiceFactory
接收组件中实现的接口对象的注册以及向外提供特定组件的接口实现。
LoginService
public interface LoginService { /** * 是否已经登录 * @return */ boolean isLogin(); /** * 获取登录用户的 Password * @return */ String getPassword(); }
EmptyService
public class EmptyService implements LoginService { @Override public boolean isLogin() { return false; } @Override public String getPassword() { return null; } }
ServiceFactory
public class ServiceFactory { private LoginService loginService; private ServiceFactory(){ /** * 禁止外部创建 ServiceFactory 对象 */ private ServiceFactory() { } /** * 通过静态内部类方式实现 ServiceFactory 的单例 */ public static ServiceFactory getInstance() { return Inner.serviceFactory; } private static class Inner { private static ServiceFactory serviceFactory = new ServiceFactory(); } /** * 接收 Login 组件实现的 Service 实例 */ public void setLoginService(LoginService loginService){ this.loginService = loginService; } /** * 返回 Login 组件的 Service 实例 */ public LoginService getLoginService(){ if(loginService == null){ return new EmptyService(); }else{ return loginService; } } }
2.实现接口
在login模块
public class AccountService implements LoginService { private boolean login; private String password; public AccountService(boolean login, String password) { this.login = login; this.password = password; } @Override public boolean isLogin() { return login; } @Override public String getPassword() { return password; } }
这里新建一个Util类用来存储登录数据
public class LoginUtil { static boolean isLogin = false; static String password = null; }
实现一下登录操作
login = (Button)findViewById(R.id.login); login.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { LoginUtil.isLogin = true; LoginUtil.password = "admin"; ServiceFactory.getInstance().setLoginService(new AccountService(LoginUtil.isLogin,LoginUtil.password)); } });
在login模块的application里定义ServiceFactory类
public class LoginApplication extends Application { @Override public void onCreate() { super.onCreate(); ServiceFactory.getInstance().setLoginService(new AccountService(LoginUtil.isLogin,LoginUtil.password)); } }
在分享模块获取登录信息
share = (Button)findViewById(R.id.share); share.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(ServiceFactory.getInstance().getLoginService().isLogin()){ Toast.makeText(ShareActivity.this,"分享成功!",Toast.LENGTH_SHORT).show(); }else{ Toast.makeText(ShareActivity.this,"分享失败,请先登录!",Toast.LENGTH_SHORT).show(); } } });
一个项目时只能有一个 Application 的,Login 作为组件时,主模块的 Application 类会初始化,而 Login 组件中的 Applicaiton 不会初始化。确实是存在这个问题的,我们这里先将 Service 的注册放到其活动里,稍后我们会解决 Login 作为组件时 Appliaciton 不会初始化的问题。
组件Application的动态切换
在主模块中有 Application 等情况下,组件在集中调试时其 Applicaiton 不会初始化的问题。而我们组件的 Service 在 ServiceFactory 的注册又必须放到组件初始化的地方。
为了解决这个问题可以将组件的 Service 类强引用到主 Module 的 Application 中进行初始化,这就必须要求主模块可以直接访问组件中的类。而我们又不想在开发过程中主模块能访问组件中的类,这里可以通过反射来实现组件 Application 的初始化。
1.定义抽象类 BaseApplication 继承 Application
在Baselibs
基础库模块
public abstract class BaseApplication extends Application { /** * Application 初始化 */ public abstract void initModuleApp(Application application); /** * 所有 Application 初始化后的自定义操作 */ public abstract void initModuleData(Application application); //其他需要调用的方法 }
2.所有的组件的 Application 都继承 BaseApplication
这里我们以Login模块为例
public class LoginApplication extends BaseApplication{ @Override public void onCreate() { super.onCreate(); initModuleApp(this); initModuleData(this); } @Override public void initModuleApp(Application application) { ServiceFactory.getInstance().setLoginService(new AccountService(LoginUtil.isLogin,LoginUtil.password)); } @Override public void initModuleData(Application application) { } }
3.定义 AppConfig 类
在Baselibs
模块定义一个静态的 String 数组,我们将需要初始化的组件的 Application 的完整类名放入到这个数组中。
public class AppConfig { private static final String LoginApp = "com.example.login.LoginApplication"; public static String[] moduleApps = { LoginApp }; }
4.主模块application实现两个初始化方法
// 主 Module 的 Applicaiton public class MainApplication extends BaseApp { @Override public void onCreate() { super.onCreate(); // 初始化组件 Application initModuleApp(this); // 其他操作 // 所有 Application 初始化后的操作 initModuleData(this); } @Override public void initModuleApp(Application application) { for (String moduleApp : AppConfig.moduleApps) { try { Class clazz = Class.forName(moduleApp); BaseApp baseApp = (BaseApp) clazz.newInstance(); baseApp.initModuleApp(this); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } } } @Override public void initModuleData(Application application) { for (String moduleApp : AppConfig.moduleApps) { try { Class clazz = Class.forName(moduleApp); BaseApp baseApp = (BaseApp) clazz.newInstance(); baseApp.initModuleData(this); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } } } }
到这里我们就通过反射,完成了组件 Application 的初始化操作,也实现了组件与化中的解耦需求。
主模块使用其他组件的 Fragment
我们在开发过程中经常使用 Fragment。一般情况下,我们都是直接通过访问具体 Fragment 类的方式实现 Fragment 的实例化,但是现在为了实现模块与组件间的解耦,在移除组件时不会由于引用的 Fragment 不存在而编译失败,我们就不能模块中直接访问组件的 Fragment 类。
这里介绍两种方法
1.ARouter
这里可以采用ARouter直接调用
fragment = (Fragment) ARouter.getInstance().build("/login/fragment").navigation();
2.反射
我们还是以Login
模块为例,假如在该模块创建一个用户界面,命名为UserFragment
首先,在 Login
组件中创建 UserFragment
,然后在 LoginService
接口中添加newUserFragment
方法返回一个Fragment
,在Login
组件中的 AccountService
和 Baselibs
中 LoginService
的空实现类中实现这个方法,然后在主模块中通过 ServiceFactory
获取 LoginService
的实现类对象,调用其 newUserFragment
即可获取到 UserFragment
的实例。
// Baselibs 模块的 LoginService public interface LoginService { //其他代码... Fragment newUserFragment(Activity activity, int containerId, FragmentManager manager, Bundle bundle, String tag); }
// Login 组件中的 AccountService public class AccountService implements LoginService { // 其他代码 ... @Override public Fragment newUserFragment(Activity activity, int containerId, FragmentManager manager, Bundle bundle, String tag) { FragmentTransaction transaction = manager.beginTransaction(); // 创建 UserFragment 实例,并添加到 Activity 中 Fragment userFragment = new UserFragment(); transaction.add(containerId, userFragment, tag); transaction.commit(); return userFragment; } }
// 主模块的 FragmentActivity public class FragmentActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_fragment); // 通过组件提供的 Service 实现 Fragment 的实例化 ServiceFactory.getInstance().getAccountService().newUserFragment(this, R.id.layout_fragment, getSupportFragmentManager(), null, ""); } }
加载全部内容