一掏导、組件化
作為一個(gè)單工程擼到底的開發(fā)人員享怀,想試著將項(xiàng)目進(jìn)行組件化改造,說動就動趟咆。畢竟技術(shù)都是寫出來的添瓷,看著文章感覺懂了,但是實(shí)際開發(fā)中還是能遇到各種各樣的問題值纱,開始搞起來鳞贷。
1.1 為什么使用組件化
一直使用單工程擼到底,項(xiàng)目越來越大導(dǎo)致出現(xiàn)了不少的問題:
查找問題慢:定位問題虐唠,需要在多個(gè)代碼混合的模塊中尋找和跳轉(zhuǎn)搀愧。
開發(fā)維護(hù)成本增加:避免代碼的改動影響其它業(yè)務(wù)的功能,導(dǎo)致開發(fā)和維護(hù)成本不斷增加疆偿。
編譯時(shí)間長:項(xiàng)目工程越大咱筛,編譯完整代碼所花費(fèi)的時(shí)間越長。
開發(fā)效率低:多人協(xié)作開發(fā)時(shí)杆故,開發(fā)風(fēng)格不一迅箩,又很難將業(yè)務(wù)完全分割,大家互相影響处铛,導(dǎo)致開發(fā)效率低下饲趋。
代碼復(fù)用性差:寫過的代碼很難抽離出來再次利用。
1.2 模塊化與組件化
1.2.1 模塊
將一個(gè)程序按照其功能做拆分撤蟆,分成相互獨(dú)立的模塊奕塑,以便于每個(gè)模塊只包含與其功能相關(guān)的內(nèi)容,比如登錄模塊枫疆、首頁模塊等等爵川。
1.2.2 組件
組件指的是單一的功能組件,如登錄組件息楔、視頻組件寝贡、支付組件 等扒披,每個(gè)組件都可以以一個(gè)單獨(dú)的 module 開發(fā),并且可以單獨(dú)抽出來作為 SDK 對外發(fā)布使用圃泡〉福可以說往往一個(gè)模塊包含了一個(gè)或多個(gè)組件显拜。
1.3 組件化的優(yōu)勢
組件化基于可重用的目的坎藐,將應(yīng)用拆分成多個(gè)獨(dú)立組件,以減少耦合:
加快編譯速度:每個(gè)業(yè)務(wù)功能都是一個(gè)單獨(dú)的工程胚鸯,可獨(dú)立編譯運(yùn)行风秤,拆分后代碼量較少鳖目,編譯自然變快。
解耦:通過關(guān)注點(diǎn)分離的形式缤弦,將App分離成多個(gè)模塊领迈,每個(gè)模塊都是一個(gè)組件。
提高開發(fā)效率:多人開發(fā)中碍沐,每個(gè)組件模塊由單人負(fù)責(zé)狸捅,降低了開發(fā)之間溝通的成本,減少因代碼風(fēng)格不一而產(chǎn)生的相互影響累提。
代碼復(fù)用:類似我們引用的第三方庫尘喝,可以將基礎(chǔ)組件或功能組件剝離。在新項(xiàng)目微調(diào)或直接使用斋陪。
1.4 組件化需要解決的問題
組件分層:怎么將一個(gè)項(xiàng)目分成多個(gè)組件朽褪、組件間的依賴關(guān)系是怎么樣的?
組件單獨(dú)運(yùn)行和集成調(diào)試:組件是如何獨(dú)立運(yùn)行和集成調(diào)試的?
組件間通信:主項(xiàng)目與組件鳍贾、組件與組件之間如何通信就變成關(guān)鍵?
二鞍匾、組件分層
組件依賴關(guān)系是上層依賴下層,修改頻率是上層高于下層骑科。先上一張圖:
2.1 基礎(chǔ)組件
基礎(chǔ)公共模塊橡淑,最底層的庫:
- 封裝公用的基礎(chǔ)組件;
- 網(wǎng)絡(luò)訪問框架、圖片加載框架等主流的第三方庫;
- 各種第三方SDK咆爽。
2.2 common組件(lib_common)
- 支撐業(yè)務(wù)組件梁棠、功能組件的基礎(chǔ)(BaseActivity/BaseFragment等基礎(chǔ)能力;
- 依賴基礎(chǔ)組件層;
- 業(yè)務(wù)組件、功能組件所需的基礎(chǔ)能力只需要依賴common組件即可獲得斗埂。
2.3 功能組件
- 依賴基礎(chǔ)組件層;
- 對一些公用的功能業(yè)務(wù)進(jìn)行封裝與實(shí)現(xiàn);
- 業(yè)務(wù)組件可以在library和application之間切換符糊,但是最后打包時(shí)必須是library ;
2.4 業(yè)務(wù)組件
- 可直接依賴基礎(chǔ)組件層;同時(shí)也能依賴公用的一些功能組件;
- 各組件之間不存在依賴關(guān)系,通過路由進(jìn)行通信;
- 業(yè)務(wù)組件可以在library和application之間切換呛凶,但是最后打包時(shí)必須是library ;
2.5 主工程(app)
只依賴各業(yè)務(wù)組件;
除了一些全局的配置和主Activity之外男娄,不包含任何業(yè)務(wù)代碼,是應(yīng)用的入口;
2.6 完成后項(xiàng)目
這只是個(gè)大概,并不是說必須這樣模闲,可以按照自己的方式來建瘫。比如:你覺得基礎(chǔ)組件比較多導(dǎo)致project里面的項(xiàng)目太多,那么你可以創(chuàng)建一個(gè)lib_base尸折,然在lib_base里面再創(chuàng)建其他基礎(chǔ)組件即可啰脚。
三、組件單獨(dú)調(diào)試
3.1 創(chuàng)建組件(收藏)
library和application之間切換:選擇第一項(xiàng)实夹。
始終是library:選擇第二項(xiàng)
這樣盡可能的減少變動項(xiàng)橄浓,當(dāng)然這僅僅是個(gè)建議,看個(gè)人習(xí)慣吧亮航。
因?yàn)樵蹅儎?chuàng)建的是一個(gè)module荸实,所以在AndridManifest中添加android:exported="true"屬性可直接構(gòu)建一個(gè)APK。下面咱們看看如何生成不同的工程類型塞赂。
3.2 動態(tài)配置組件的工程類型
在 AndroidStudio 開發(fā) Android 項(xiàng)目時(shí)泪勒,使用的是 Gradle 來構(gòu)建,具體來說使用的是 Android Gradle 插件來構(gòu)建宴猾,Android Gradle 中提供了三種插件,在開發(fā)中可以通過配置不同的插件來構(gòu)建不同的工程叼旋。
3.2.1 build.gradle(module)
//構(gòu)建后輸出一個(gè) APK 安裝包
apply plugin: 'com.android.application'
//構(gòu)建后輸出 ARR 包
apply plugin: 'com.android.library'
//配置一個(gè) Android Test 工程
apply plugin: 'com.android.test'
獨(dú)立調(diào)試:設(shè)置為 Application 插件仇哆。
集成調(diào)試:設(shè)置為 Library 插件。
3.2.2 設(shè)置gradle.properties
isDebug = true 獨(dú)立調(diào)試
3.2.3 動態(tài)配制插件(build.gradle)
//注意gradle.properties中的數(shù)據(jù)類型都是String類型夫植,使用其他數(shù)據(jù)類型需要自行轉(zhuǎn)換
if(isDebug.toBoolean()){
//構(gòu)建后輸出一個(gè) APK 安裝包
apply plugin: 'com.android.application'
}else{
//構(gòu)建后輸出 ARR 包
apply plugin: 'com.android.library'
}
3.3 動態(tài)配置組件的 ApplicationId 和 AndroidManifest 文件
一個(gè) APP 是只有一個(gè) ApplicationId 讹剔,所以在單獨(dú)調(diào)試和集成調(diào)試組件的 ApplicationId 應(yīng)該是不同的。
單獨(dú)調(diào)試時(shí)也是需要有一個(gè)啟動頁详民,當(dāng)集成調(diào)試時(shí)主工程和組件的AndroidManifest文件合并會產(chǎn)生多個(gè)啟動頁延欠。
根據(jù)上面動態(tài)配制插件的經(jīng)驗(yàn),我們也需要在build.gradle中動態(tài)配制ApplicationId 和 AndroidManifest 文件沈跨。
3.3.1 準(zhǔn)備兩個(gè)不同路徑的 AndroidManifest 文件
有什么不同由捎?咱們一起看看具體內(nèi)容。
3.3.2 src/main/debug/AndroidManifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.scc.module.collect">
<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/Theme.SccMall">
<activity android:name=".CollectActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
3.3.3 src/main/AndroidManifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.scc.module.collect">
<application
android:allowBackup="true"
android:supportsRtl="true"
>
<activity android:name=".CollectActivity"/>
</application>
</manifest>
3.3.4 動態(tài)配制(build.gradle)
defaultConfig {
if(isDebug.toBoolean()){
//獨(dú)立調(diào)試的時(shí)候才能設(shè)置applicationId
applicationId "com.scc.module.collect"
}
}
sourceSets {
main {
if (isDebug.toBoolean()) {
//獨(dú)立調(diào)試
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
//集成調(diào)試
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
3.4 實(shí)現(xiàn)效果
3.4.1 獨(dú)立調(diào)試
isDebug = true
3.4.2 集成調(diào)試
isDebug = false
四饿凛、Gradle配置統(tǒng)一管理
4.1 config.gradle
當(dāng)我們需要進(jìn)行插件版本狞玛、依賴庫版本升級時(shí),項(xiàng)目多的話改起來很麻煩涧窒,這時(shí)就需要我們對Gradle配置統(tǒng)一管理心肪。如下:
具體內(nèi)容
ext{
//組件獨(dú)立調(diào)試開關(guān), 每次更改值后要同步工程
isDebug = true
android = [
// 編譯 SDK 版本
compileSdkVersion: 31,
// 最低兼容 Android 版本
minSdkVersion : 21,
// 最高兼容 Android 版本
targetSdkVersion : 31,
// 當(dāng)前版本編號
versionCode : 1,
// 當(dāng)前版本信息
versionName : "1.0.0"
]
applicationid = [
app:"com.scc.sccmall",
main:"com.scc.module.main",
webview:"com.scc.module.webview",
login:"com.scc.module.login",
collect:"com.scc.module.collect"
]
dependencies = [
"appcompat" :'androidx.appcompat:appcompat:1.2.0',
"material" :'com.google.android.material:material:1.3.0',
"constraintlayout" :'androidx.constraintlayout:constraintlayout:2.0.1',
"livedata" :'androidx.lifecycle:lifecycle-livedata:2.4.0',
"viewmodel" :'androidx.lifecycle:lifecycle-viewmodel:2.4.0',
"legacyv4" :'androidx.legacy:legacy-support-v4:1.0.0',
"splashscreen" :'androidx.core:core-splashscreen:1.0.0-alpha01'
]
libARouter= 'com.alibaba:arouter-api:1.5.2'
libARouterCompiler = 'com.alibaba:arouter-compiler:1.5.2'
libGson = 'com.google.code.gson:gson:2.8.9'
}
4.2 添加配制文件build.gradle(project)
apply from:"config.gradle"
4.3 其他組件使用
//build.gradle
//注意gradle.properties中的數(shù)據(jù)類型都是String類型,使用其他數(shù)據(jù)類型需要自行轉(zhuǎn)換
if(isDebug.toBoolean()){
//構(gòu)建后輸出一個(gè) APK 安裝包
apply plugin: 'com.android.application'
}else{
//構(gòu)建后輸出 ARR 包
apply plugin: 'com.android.library'
}
android {
compileSdkVersion 31
defaultConfig {
if(isDebug.toBoolean()){
//獨(dú)立調(diào)試的時(shí)候才能設(shè)置applicationId
applicationId "com.scc.module.collect"
}
minSdkVersion 21
targetSdkVersion 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
sourceSets {
main {
if (isDebug.toBoolean()) {
//獨(dú)立調(diào)試
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
//集成調(diào)試
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
// implementation root.dependencies.appcompat
// implementation root.dependencies.material
// implementation root.dependencies.constraintlayout
// implementation root.dependencies.livedata
// implementation root.dependencies.viewmodel
// implementation root.dependencies.legacyv4
// implementation root.dependencies.splashscreen
// implementation root.libARouter
//上面內(nèi)容在lib_common中已經(jīng)添加咱們直接依賴lib_common
implementation project(':lib_common')
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
五纠吴、組件間界面跳轉(zhuǎn)(ARouter)
5.1 介紹
Android 中的界面跳轉(zhuǎn)那是相當(dāng)簡單硬鞍,但是在組件化開發(fā)中,由于不同組件式?jīng)]有相互依賴的,所以不可以直接訪問彼此的類固该,這時(shí)候就沒辦法通過顯式的方式實(shí)現(xiàn)了锅减。
所以在這里咱們采取更加靈活的一種方式,使用 Alibaba 開源的 ARouter 來實(shí)現(xiàn)蹬音。
一個(gè)用于幫助 Android App 進(jìn)行組件化改造的框架 —— 支持模塊間的路由上煤、通信、解耦
文檔介紹的蠻詳細(xì)的著淆,感興趣的可以自己實(shí)踐一下劫狠。這里做個(gè)簡單的使用。
5.2 使用
5.2.1 添加依賴
先在統(tǒng)一的config.gradle添加版本等信息
ext{
...
libARouter= 'com.alibaba:arouter-api:1.5.2'
libARouterCompiler = 'com.alibaba:arouter-compiler:1.5.2'
}
因?yàn)樗械墓δ芙M件和業(yè)務(wù)組件都依賴lib_common永部,那么咱們先從lib_common開始配制
lib_common
dependencies {
api root.libARouter
...
}
其他組件(如collect)
android {
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
//如果項(xiàng)目內(nèi)有多個(gè)annotationProcessor独泞,則修改為以下設(shè)置
//arguments += [AROUTER_MODULE_NAME: project.getName()]
}
}
}
}
dependencies {
//arouter-compiler的注解依賴需要所有使用 ARouter 的 module 都添加依賴
annotationProcessor root.libARouterCompiler
...
}
5.2.2 添加注解
你要跳轉(zhuǎn)的Activity
// 在支持路由的頁面上添加注解(必選)
// 這里的路徑需要注意的是至少需要有兩級,/xx/xx
@Route(path = "/collect/CollectActivity")
public class CollectActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_collect);
}
}
5.2.3 初始化SDK(主項(xiàng)目Application)
public class App extends BaseApplication {
@Override
public void onCreate() {
super.onCreate();
if (isDebug()) { // 這兩行必須寫在init之前苔埋,否則這些配置在init過程中將無效
ARouter.openLog(); // 打印日志
ARouter.openDebug(); // 開啟調(diào)試模式(如果在InstantRun模式下運(yùn)行懦砂,必須開啟調(diào)試模式!線上版本需要關(guān)閉,否則有安全風(fēng)險(xiǎn))
}
ARouter.init(this); // 盡可能早组橄,推薦在Application中初始化
}
private boolean isDebug() {
return BuildConfig.DEBUG;
}
}
5.3 發(fā)起路由操作
5.3.1 應(yīng)用內(nèi)簡單的跳轉(zhuǎn)
ARouter.getInstance().build("/collect/CollectActivity").navigation();
這里是用module_main的HomeFragment跳轉(zhuǎn)至module_collect的CollectActivity界面荞膘,兩個(gè)module中不存在依賴關(guān)系。"/collect/CollectActivity"
在上面已注冊就不多描述了玉工。
效果如下:
5.3.2 跳轉(zhuǎn)并攜帶參數(shù)
這里是用module_main的MineFragment的Adapter跳轉(zhuǎn)至module_webview的WebViewActivity界面羽资,兩個(gè)module中同樣不存在依賴關(guān)系。
啟動方
ARouter.getInstance().build("/webview/WebViewActivity")
.withString("url", bean.getUrl())
.withString("content",bean.getName())
.navigation();
這里傳了兩個(gè)參數(shù)url和name到WebViewActivity遵班,下面咱們看看WebViewActivity怎么接收屠升。
接收方
//為每一個(gè)參數(shù)聲明一個(gè)字段,并使用 @Autowired 標(biāo)注
//URL中不能傳遞Parcelable類型數(shù)據(jù)狭郑,通過ARouter api可以傳遞Parcelable對象
//添加注解(必選)
@Route(path = "/webview/WebViewActivity")
public class WebViewActivity extends BaseActivity<ActivityWebviewBinding, WebViewViewModel> {
//發(fā)送方和接收方定義的key名稱相同則無需處理
@Autowired
public String url;
//通過name來映射URL中的不同參數(shù)
//發(fā)送方定義key為content腹暖,我們用title來接收
@Autowired(name = "content")
public String title;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//注入?yún)?shù)和服務(wù)(這里用到@Autowired所以要設(shè)置)
//不使用自動注入,可不寫,如CollectActivity沒接收參數(shù)就沒有設(shè)置
ARouter.getInstance().inject(this);
binding.btnBoom.setText(String.format("%s,你來啦", title));
//加載鏈接
initWebView(binding.wbAbout, url);
}
}
上效果圖:
搞定翰萨,更多高級玩法可自行探索脏答。
5.3.3 小記(ARouter目標(biāo)不存在)
W/ARouter::: ARouter::There is no route match the path
這里出現(xiàn)個(gè)小問題,配置注釋都好好的缨历,但是發(fā)送發(fā)無論如何都找不到設(shè)置好的Activity以蕴。嘗試方案:
- Clean Project
- Rebuild Project
- 在下圖也能找到ARouter內(nèi)容。
后來修改Activity名稱好了辛孵。
六丛肮、組件間通信(數(shù)據(jù)傳遞)
界面跳轉(zhuǎn)搞定了,那么數(shù)據(jù)傳遞怎么辦魄缚,我在module_main中使用懸浮窗宝与,但是需要判斷這個(gè)用戶是否已登錄焚廊,再執(zhí)行后續(xù)邏輯,這個(gè)要怎么辦习劫?這里我們可以采用 接口 + ARouter 的方式來解決咆瘟。
在這里可以添加一個(gè) componentbase 模塊,這個(gè)模塊被所有的組件依賴诽里。
這里我們通過 module_main組件 中調(diào)用 module_login組件 中的方法來獲取登錄狀態(tài)這個(gè)場景來演示袒餐。
6.1 通過依賴注入解耦:服務(wù)管理(一) 暴露服務(wù)
6.1.1 創(chuàng)建 componentbase 模塊(lib)
6.1.2 創(chuàng)建接口并繼承IProvider
注意:接口必須繼承IProvider,是為了使用ARouter的實(shí)現(xiàn)注入谤狡。
6.1.3 在module_login組件中實(shí)現(xiàn)接口
lib_common
所有業(yè)務(wù)組件和功能組件都依賴lib_common灸眼,所以咱們直接在lib_common添加依賴即可
dependencies {
...
api project(":lib_componentbase")
}
module_login
dependencies {
...
implementation project(':lib_common')
}
實(shí)現(xiàn)接口
//實(shí)現(xiàn)接口
@Route(path = "/login/AccountServiceImpl")
public class AccountServiceImpl implements IAccountService {
@Override
public boolean isLogin() {
MLog.e("AccountServiceImpl.isLogin");
return true;
}
@Override
public String getAccountId() {
MLog.e("AccountServiceImpl.getAccountId");
return "1000";
}
@Override
public void init(Context context) {
}
}
6.2 通過依賴注入解耦:服務(wù)管理(二) 發(fā)現(xiàn)服務(wù)
6.2.1 在module_main中調(diào)用調(diào)用是否已登入
public class HomeFragment extends BaseFragment<FragmentHomeBinding> {
@Autowired
IAccountService accountService;
@Override
public void onViewCreated(@NonNull @NotNull View view, @Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ARouter.getInstance().inject(this);
binding.frgmentHomeFab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
MLog.e("Login:"+accountService.isLogin());
MLog.e("AccountId:"+accountService.getAccountId());
}
});
}
}
運(yùn)行結(jié)果:
E/-SCC-: AccountServiceImpl.isLogin
E/-SCC-: Login:true
E/-SCC-: AccountServiceImpl.getAccountId
E/-SCC-: AccountId:1000
七、總結(jié)
本文介紹了組件化墓懂、組件分層焰宣、解決了組件的獨(dú)立調(diào)試、集成調(diào)試捕仔、頁面跳轉(zhuǎn)匕积、組件通信等。
其實(shí)會了這些后你基本可以搭建自己的組件化項(xiàng)目了榜跌。其實(shí)最大的問題還是分組分層闪唆、組件劃分。這個(gè)就需要根據(jù)你的實(shí)際情況來設(shè)置钓葫。
本項(xiàng)目比較糙苞氮,后面會慢慢完善。比如添加Gilde瓤逼、添加MMVK、添加Room等库物。
本文轉(zhuǎn)自 https://juejin.cn/post/7033954652315975688霸旗,如有侵權(quán),請聯(lián)系刪除戚揭。