最近剛剛實踐了組件化,開始聽到組件化的時候覺得有點畏懼,比較陌生,但是真正去做的時候并沒有你想象中的那么困難。當然,這也是我第一次組件化,如果有什么不對你別罵我。
要Demo的請戳這里,安卓組件化Demo
什么是組件化?
組件化,從字面上看,就是把一個完整的東西拆分成若干個小組件,然后拼接成一個完整的實體。就好比機器人,都是由頭部,軀干,手腳等組件拼裝起來的。但是,我們這一組件和這個機器的組件還是有區(qū)別,區(qū)別在哪里呢?機器人的頭部,軀干,手腳單獨任何一個組件都是無用的,無法實現(xiàn)或者說完成一個指令。我們的組件化里面的任意一個功能組件,都是應(yīng)該可以獨立編譯運行并且可以一起組裝成一個完整的應(yīng)用編譯運行。所以,機器人的這個例子不那么嚴謹,看過數(shù)碼寶貝的同學(暴露年齡了)應(yīng)該知道奧米加獸,由戰(zhàn)斗暴龍獸。??瓤?,扯遠了,大概就是這么個意思。
為什么要組件化?
- 項目龐大,業(yè)務(wù)復雜,組件化可以更加清晰的梳理業(yè)務(wù)邏輯
- 團隊開發(fā),耦合度高,組件化可以讓專門的人開發(fā)維護某個組件
- 項目成熟,功能豐滿,組件化可以快速的將部分功能模塊抽離成獨立的應(yīng)用
- 項目龐大,代碼累積,這個時候最為要命,編譯運行一次幾分鐘就過去了,組件化可以單獨編譯某個模塊,大大的提升了開發(fā)效率
怎么實現(xiàn)組件化?
寫在前面
組件化屬于最好在項目之初就開始架構(gòu),中途架構(gòu)可能會遇到比較糟心的問題(博主本人),之前代碼不一定都是你寫的,經(jīng)多人之手很難整理,有時候不得不維護兩套,保留之前的。
首先,組件化拆分后的結(jié)構(gòu)
這是我自己總結(jié)的拆分,可能會有差別,可供參考

從上圖可以看出,BASE其實也是一個組件,但是這個組件里面沒有業(yè)務(wù)邏輯,只是一些基類和公共資源的整合。同時BASE還依賴了一些其他組件,HTTP網(wǎng)絡(luò)訪問組件,UI組件和其他組件,按照這個邏輯其他一些第三方庫依賴也應(yīng)該在BASE中。
再往上看,我們的業(yè)務(wù)組件(A組件,B組件...N組件)都依賴了BASE組件,但是A組件,B組件...N組件之間沒有依賴。為什么業(yè)務(wù)組件可以依賴BASE組件,而業(yè)務(wù)組件之間不依賴呢?不是不依賴,而是不應(yīng)該依賴,我們組件化的初衷就是分離模塊,使他們之間不產(chǎn)生依賴關(guān)系。有同學要問了,那么,我A組件有可能涉及到B組件里面的交互怎么辦呢?先不著急,這個要等到下面說了,現(xiàn)在我們先理順這張圖。剛剛說組件不應(yīng)該互相依賴,但是業(yè)務(wù)組件確實依賴了BASE組件,BASE組件還依賴了一些其他組件,這是為什么呢?因為,BASE組件和HTTP組件或者UI組件這些組件中根本不存在任何業(yè)務(wù)和邏輯關(guān)系,換句話說,無論把這幾個組件放在哪個項目當中都是OK的。
最上面一層就是我們的APP殼了,APP殼中其實包含的東西很少,但是要整合所有的業(yè)務(wù)組件,然后編譯打包成一個完整的項目。所以APP殼里面依賴了所有的業(yè)務(wù)組件,值得一提的事,因為APP殼里面依賴了所有的業(yè)務(wù)組件,但是APP里面也不應(yīng)該直接使用某個業(yè)務(wù)組件里面的東西。
組件化過程
簡單來說,一個組件就是一個Module,那怎么做到即可單獨編譯運行又可以整合到APP中呢?其實很簡單,因為每個Module都有這樣一個apply plugin: 'com.android.application'或apply plugin: 'com.android.library'配置,所以只要動態(tài)修改這個就可以讓這個Module作為一個APP或者作為一個library存在了。
但是,當我們一個項目有很多個組件的時候我們不可能一個個的去修改。偷懶是我們光榮的程序員的特質(zhì),這個問題其實就是一個if--else能夠解決的,但是我們需要一個全局變量,這個變量可以在gradle.properties這個文件中設(shè)置。

如上圖,我們在gradle.properties這個文件中添加了一個Boolean類型的參數(shù)isDebug,然后我們就可以在Module的gradle中添加這樣一段代碼就OK了。要想切換APP和library就只需修改isDebug,無需一個一個Module去修改了(在gradle中都是字符串類型所以isDebug要強轉(zhuǎn)從Boolean類型)。
if (isDebug.toBoolean()){
apply plugin: 'com.android.application'
}else{
apply plugin: 'com.android.library'
}
這個時候我們又會迎來一個新問題,當一個Module作為APP編譯運行時,它的AndroidManifest是需要自定義的Application的,因為一些第三方庫或者其他一些東西需要在Application中初始化。我們可以在BASE中定義一個Application,在BaseApplication中初始化,每個Module的Application都繼承BaseApplication或者直接使用BaseApplication。說回這個新問題,APP和library的AndroidManifest是不一樣的,那我們這個時候也要想上面一樣通過判斷來選擇使用哪個AndroidManifest,還是在這個Module的gradle添加一段代碼(值得注意的是,如果你copy這份代碼,導包后manifest可能會變成大寫開頭,請一定要用小寫的)。
sourceSets{
main{
if (isDebug.toBoolean()){
manifest.srcFile 'src/main/java/debug/AndroidManifest.xml'
}else{
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
代碼其實就是引用哪個xml文件,但是這里出現(xiàn)了兩個路徑,其中不是debug的xml是自動生成的,debug中的是我們手動創(chuàng)建的,所以我們還要在src/main/java下新建一個debug的package,然后把src/main中的AndroidManifest.xml復制一份到debug中。

就這么簡單,配置完成后只需要修改isDebug,就可以在APP和library中自由切換了。
如果就這樣,你會遇到很多糟心的問題,比如你的APP中有一個activity_main.xml的布局文件,同時你的Module中也有這么一個文件且兩個文件中都有一個相同的id的控件,你就會遇上問題了。當然不僅僅包括這個問題,還有其他很多資源沖突的問題也可能發(fā)生。
那么,我們該如何解決這個問題呢?有一個方法,但不算是解決,算是預防。那就是我們規(guī)定一種方式,杜絕資源沖突的問題。比如:im組件里面我的資源文件都是以im_這種方式開頭命名的,share組件里面我們都是以share_這種方式開頭命名的。當然,為了讓你記住這個約定,可以在Module的gradle中進行如下配置:
resourcePrefix "im_"

再當你創(chuàng)建一個layout時,就會默認以你設(shè)置的約定開頭,這樣就可以有效的避免資源沖突了(好像只有在android的目錄結(jié)構(gòu)下才有效,見下圖)。

組件之間交互
通過以上操作,基本已經(jīng)確立了組件化的框架,隨著組件多起來,組件之間也會存在一些交互(上文提到的A組件涉及到B組件的一些交互)。因為我們的組件間是不存在互相依賴的,那我們該如何進行組件間的交互呢?
這個時候我們需要引入一個Router的概念了,我們這些交互都需要經(jīng)過Router中轉(zhuǎn)

所以我們上面的拆分結(jié)構(gòu)是不足以讓我組件化的,我們還需要添加一個路由,

和之前的相比只是多了一個ROUTER,那我們怎么利用ROUTER來實現(xiàn)組件間的交互和跳轉(zhuǎn)呢?這個時候阿里爸爸就要登場了,有一個叫做 ARouter 的東西,可以協(xié)助我們實現(xiàn)路由。Ok,你可以參考他的文檔,寫的已經(jīng)比較清楚,我這里也會說明一下。
-
Step1 : 了解路由
建一個router的Module,這個Module里面什么也不做只暴露一些服務(wù),配置gradle如下(需要注意的是,圖中紅框框內(nèi)的東西最好在你用到ARouter的Module里面都配置一下)。
路由的gradle配置.png Step2 : 依賴并初始化
BASE組件中依賴ROUTER,并在BaseApplication中初始化,參照 ARouter 。Step3 : 暴露服務(wù)
在我們的ROUTER組件中暴露一些服務(wù),創(chuàng)建一個SayHelloService如下
package com.yxr.router;
import com.alibaba.android.arouter.facade.template.IProvider;
/**
* Created by yxr on 2017/12/9.
*/
public interface SayHelloService extends IProvider {
String sayHello(String name);
}
- Step4 : 實現(xiàn)這個服務(wù)
假設(shè)我這服務(wù)需要在IM這個組件中實現(xiàn),那么我們創(chuàng)建一個SayHelloServiceImp如下
package com.yxr.im;
import android.content.Context;
import com.alibaba.android.arouter.facade.annotation.Route;
import com.yxr.router.SayHelloService;
/**
* Created by yxr on 2017/12/9.
*/
@Route(path = "/im/sayHelloService")
public class SayHelloServiceImp implements SayHelloService{
@Override
public String sayHello(String name) {
return "hello," + name;
}
@Override
public void init(Context context) {
}
}
- Step5 發(fā)現(xiàn)服務(wù)
假設(shè)我們需要在SHARE組件中使用到這個服務(wù),只需要發(fā)現(xiàn)這個服務(wù)(需要注意,單獨運行某個組件時,其他組件的并不會編譯進去,所以導致 ARouter 無法正常工作,所以該判斷的地方還是需要判斷一下),如下:
package com.yxr.share;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.View;
import com.alibaba.android.arouter.facade.annotation.Autowired;
import com.alibaba.android.arouter.facade.annotation.Route;
import com.alibaba.android.arouter.launcher.ARouter;
import com.yxr.base.ui.BaseActivity;
import com.yxr.router.SayHelloService;
/**
* Created by yxr on 2017/12/9.
*/
@Route(path = "/share/shareActivity")
public class ShareActivity extends BaseActivity implements View.OnClickListener {
@Autowired()
SayHelloService sayHelloService;
// 建議使用這種方式,因為接口是可以被多實現(xiàn)的,除非你100%確定SayHelloService這個接口只被一個實現(xiàn)
// @Autowired(name = "/im/sayHelloService")
// SayHelloService sayHelloService;
public ShareActivity() {
ARouter.getInstance().inject(this);
}
@Override
public int contentView() {
return R.layout.share_activity;
}
@Override
public void initView(@Nullable Bundle savedInstanceState) {
}
@Override
public void initListener() {
findViewById(R.id.btnHello).setOnClickListener(this);
findViewById(R.id.btnJumpIm).setOnClickListener(this);
}
@Override
public void initData() {
setCommonTitle(getClass().getSimpleName());
}
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.btnHello){
// 為什么要判斷不為空呢?因為組件間沒有互相依賴,當你單獨運行某個組件時
// 另外一個組件并沒有編譯進來,所以會發(fā)現(xiàn)不了實現(xiàn)這個接口的服務(wù)
if (sayHelloService != null){
toast(sayHelloService.sayHello("組件化"));
}
} else if (id == R.id.btnJumpIm){
ARouter.getInstance().build("/im/imActivity").navigation();
}
}
}
組件間的跳轉(zhuǎn),F(xiàn)ragment獲取
其實上文已經(jīng)使用到了這里要講的東西,和需要注意的地方。具體實現(xiàn)起來也比較簡單,有了上面的基礎(chǔ)后,我不在累述,需要了解更多的可以參考 ARouter 。
哦,對了,附上幾張GIF圖演示,源碼,之后我也會整理一份分享到GIT上。




最后BB兩句
這個文章僅供參考,如果你喜歡可以給我小發(fā)發(fā),如果有批評指教也請你溫柔提點指出。最后,附上一波彩蛋(注意事項之踩坑記錄)。
- 在Module中資源文件判斷不要用switch,用if,如下:
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.btnHello){
} else if (id == R.id.btnJumpIm){
}
}
manifest問題
No signature of method: static org.gradle.api.java.archives.Manifest.srcFile() is applicable for argument types: (java.lang.String) values: [src/main/java/debug/AndroidManifest.xml]<ahref="openFile:D:\ProgramFiles\Android\workspaces_2\MyApplication\wyim\build.gradle">Open File</a>
出現(xiàn)這個異常時把Module中的Manifest.srcFile() 替換成 manifest.srcFile()ARouter問題
用到ARouter的Module里面都配置一下上文說到的配置;
記得在Application中初始化ARouter;
單獨運行某個組件時,其他組件的并不會編譯進去,所以導致 ARouter 無法正常工作,所以該判斷的地方還是需要判斷一下;資源沖突問題
利用gradle的 resourcePrefix "share_" 約定資源文件命名規(guī)范ButterKnife問題
在組件化的時候一定要一開始就注意ButterKnife的問題,具體解決方案請查看 ButterKnife問題解決其他
需要可愛認真的你自己去踩踩
要Demo的請戳這里,安卓組件化Demo
