Android組件化

最近剛剛實踐了組件化,開始聽到組件化的時候覺得有點畏懼,比較陌生,但是真正去做的時候并沒有你想象中的那么困難。當然,這也是我第一次組件化,如果有什么不對你別罵我。
要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é)的拆分,可能會有差別,可供參考


組件化簡單拆分.png

從上圖可以看出,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è)置。


單獨和分離配置.png

如上圖,我們在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中。


不同的AndroidManifest.png

就這么簡單,配置完成后只需要修改isDebug,就可以在APP和library中自由切換了。

如果就這樣,你會遇到很多糟心的問題,比如你的APP中有一個activity_main.xml的布局文件,同時你的Module中也有這么一個文件且兩個文件中都有一個相同的id的控件,你就會遇上問題了。當然不僅僅包括這個問題,還有其他很多資源沖突的問題也可能發(fā)生。

那么,我們該如何解決這個問題呢?有一個方法,但不算是解決,算是預防。那就是我們規(guī)定一種方式,杜絕資源沖突的問題。比如:im組件里面我的資源文件都是以im_這種方式開頭命名的,share組件里面我們都是以share_這種方式開頭命名的。當然,為了讓你記住這個約定,可以在Module的gradle中進行如下配置:

resourcePrefix "im_"
資源沖突處理1.png

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


資源沖突處理2.png

組件之間交互

通過以上操作,基本已經(jīng)確立了組件化的框架,隨著組件多起來,組件之間也會存在一些交互(上文提到的A組件涉及到B組件的一些交互)。因為我們的組件間是不存在互相依賴的,那我們該如何進行組件間的交互呢?

這個時候我們需要引入一個Router的概念了,我們這些交互都需要經(jīng)過Router中轉(zhuǎn)

組件與路由的關(guān)系.png

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

和之前的相比只是多了一個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上。


單獨編譯和整合為APP.png
組件整合為APP.gif
Share組件單獨編譯運行.gif
Im組件單獨編譯運行.gif

最后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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容