Architecture(5)電商APP組件化探索

概述

組件化緣由

記得剛開始接觸Android開發(fā)的時候,只知道MVC分層架構(gòu)概荷,而且感覺Model秕岛,View以及Controller太簡單了,也能稱之為分層架構(gòu)误证,隨便寫就是MVC继薛。就像在接觸設計模式之前,你可能已經(jīng)寫了無數(shù)個單例模式愈捅,只是那個時候你可能并不知道遏考,你已經(jīng)在用設計模式了,你不會去想是用DCL還是使用內(nèi)部類實現(xiàn)的單例優(yōu)雅蓝谨。

后來當一個類中的代碼上千行之后灌具,就開始想著抽取公共方法作為工具類,使用封裝譬巫、繼承以及多態(tài)來優(yōu)化自己的代碼咖楣,直到隨著業(yè)務的發(fā)展,在View層的邏輯越來越多芦昔,無法抽取時截歉,發(fā)現(xiàn)MVC的天花板其實很低,Activity跟Fragment作為View層經(jīng)常會跟Model層糾纏不清烟零,及時進行抽取之后瘪松,也還是很臃腫。MVP的出現(xiàn)锨阿,徹底解決了這個問題宵睦,解耦Model層跟View層,使得整個項目的代碼顯得更加簡潔墅诡。

在項目初期的的時候壳嚎,感覺MVP還是很不錯的,當項目逐漸變大的時候末早,每次你改動了很小的一部分烟馅,你也需要重新編譯整個APP,舉個例子然磷,就拿購物車來說郑趁,我修改了數(shù)量比狂的樣式,我需要重新編譯整個APP姿搜,為了加快速度寡润,我可能要開啟InstantRun捆憎,可能要使用Freeline來加速編譯,這并不是我想要的梭纹,而且在使用InstantRun之后躲惰,output目錄下生成的apk是差量包,只能供開發(fā)調(diào)試变抽,給測試是無法安裝的础拨,我要是想通過腳本上傳到fir給測試人員,那又得打一個全量的包绍载,并且InstantRun也不是很穩(wěn)定诡宗。

組件化效果

毫無懸念,組件化勢在必行逛钻,在網(wǎng)上看了很多相關的資料僚焦,對組件化有一個初步的了解锰提,然后就開始組件化了曙痘,下面1以我自己的項目為例屉符,放兩張組件化之前跟之后的圖對比一下齿税。

模塊化VS組件化

可以明顯的發(fā)現(xiàn)我們的Module變多了耐版,就像MVC切換到MVP之后敞掘,需要寫很多的Presenter組件化最大的好處就是可以模塊可以單獨開發(fā)調(diào)試裁赠,這樣效率一下子就上來了辩恼,還是拿購物車舉例选侨,購物車實際上就只有一個界面锐想,也就是一個Fragment融蹂,加上啟動頁跟Fragment的父Activity旺订,也就兩個界面,可以說想慢都慢不下來超燃,下面就我在組件化過程中遇到的一些問題進行總結(jié)一下区拳。

正文

指導思想

組件拆分

組件化的目的在于將一個project劃分成業(yè)務組件、基礎組件意乓、路由組件樱调。其中業(yè)務組件是相互隔離的,可以單獨調(diào)試届良,基礎組件提供業(yè)務組件所公用的功能笆凌,路由組件為業(yè)務組件之間通信提供支持。

一般來講士葫,一個APP可以由一個app殼乞而,然后集成多個Module,這是理想的情況慢显,但是從運營的需求到產(chǎn)品的設計到UI出圖晦闰,可能你就會對組件化很絕望放祟,并不是那么的理想,很多時候我們程序入口所在的Module實際上跟其它很多Module是關聯(lián)的呻右,實際上沒法拆分跪妥,本文將會以這種比較復雜的情況進行組件化分析。

組件隔離

組件化的一個很大的特性在于可以單獨調(diào)試声滥,但是由于業(yè)務組件之間的隔離眉撵,所以導致了多個組件之間無法進行通信,其實我覺得是很正常的落塑,既然是單獨調(diào)試纽疟,就必然不應該跟其它的Module間進行依賴,不管是編譯期還是運行期都應如此憾赁,不然組件化就沒有任何意義了污朽,但是由于我們的業(yè)務組件都是相互關聯(lián)的,如果不依賴其他的組件的話龙考,作為一個單獨的APP運行有時候是需要參數(shù)的蟆肆,鑒于此,我們可以在Application初始化的時候晦款,新增一個頁面作為參數(shù)配置炎功,或者直接在Application中固定寫死。

核心法則

不管我們?nèi)绾蝿澐只航Γ绾我蕾嚿咚穑M件間的關系都要嚴格遵守一個準則:編譯器隔離,運行期按需依賴坛怪。

整體架構(gòu)

Component

通過組件化將項目按照業(yè)務進行化分成GoodsModule淤齐,CartModuleUserModule袜匿,OrderModule四個模塊更啄,模塊間通過RouterModule進行通信,也就是說業(yè)務組件依賴于路由組件沉帮,RouterModule依賴于Base锈死,也就是BaseModuleLibraryModule穆壕,基礎庫跟第三方庫待牵,然后MainModule實際上相當于程序的入口跟容器,通過MainModule依賴上述四個Module喇勋,完成整個APP的打包缨该。

當然在單獨調(diào)試的時候,GoodsModule川背,CartModule贰拿,UserModule蛤袒,OrderModule又各自成為一個APP,可以單獨進行調(diào)試膨更,這樣就實現(xiàn)了APP的組件化妙真,下面就組件化過程中遇到的一些問題總結(jié)一下。

組件化分析

在組件化的過程中荚守,由于Module之間是隔離的珍德,所以就產(chǎn)生了一系?列問題,現(xiàn)在就組件化前后的遇到的問題總結(jié)如下:

  • 組件劃分:如何根據(jù)業(yè)務對項目進行Module劃分
  • 模式切換:如何使得APP在單獨調(diào)試跟整體調(diào)試自由切換
  • 資源沖突:當我們創(chuàng)建了多個Module的時候矗漾,如何解決相同資源文件名合并的沖突
  • 依賴關系:多個Module之間如何引用一些共同的library以及工具類
  • 組件通信:組件化之后锈候,Module之間是相互隔離的,如何進行UI跳轉(zhuǎn)以及方法調(diào)用
  • 入口參數(shù):我們知道組件之間是有聯(lián)系的敞贡,所以在單獨調(diào)試的時候如何拿到其它的Module傳遞過來的參數(shù)

接下來會根據(jù)?這幾個問題泵琳,提出對應的解決方法

組件劃分

業(yè)務劃分

由于我們做的是一個電商項目,網(wǎng)上也查找了很多資料誊役,感覺他們舉的例子都有些過于簡單获列,因為模塊間基本上沒有什么耦合,所以很好拆分势木,不過還是很感謝他們提供了一種解決思路蛛倦。玩過京東歌懒,淘寶都知道啦桌,大致分為幾個大的模塊:商品模塊,購物車模塊及皂,訂單模塊甫男,用戶模塊。沒錯验烧,我也是這么拆分我們APP的板驳。但是拆著拆著就發(fā)現(xiàn)問題了,模塊間耦合性太高碍拆,我們過了SplashActivity之后就是MainActivity若治,看圖說話

home

所以網(wǎng)上的一些一進來就是一個空的APP殼的方法并不適用,從一開始就遇到了這個棘手的問題感混,有點尷尬端幼,按照之前的模塊劃分,在用戶登陸的情況下MainModule一進來就必須拿到GoodsModule弧满,CartModule以及UserModule中的三個Fragment婆跑。所以首先必須得解決這個問題,很顯然之前的使用一個APP殼來合并多個Module的情況并不適用庭呜,起初我直接定義了一個MainModule滑进,然后讓他直接引用多個Module犀忱,那么MainModule就承擔了APP殼的功能,這樣一來扶关,就可以解決MainModule對其它Module的引用問題阴汇,但是違背了組件化的業(yè)務組件隔離的原則。

所以不能讓MainModule依賴另外三個Module节槐,但是如果我不引用其他的Module鲫寄,那么很顯然我無法拿到這四個Fragment的引用,有一點可以很明確疯淫,那就是編譯期業(yè)務Module之間必須不可見地来,這點是毫無疑問的。但是運行期是可見的熙掺,因為所有的Module在運行期間肯定都是通過直接或者間接依賴未斑,不然有些Module就沒用了,在運行時獲取實例币绩,那么很自然地就會想到反射了蜡秽,沒錯就是反射。

依賴劃分

除了業(yè)務模塊之外缆镣,我們還會有一些公用的工具類以及資源文件芽突,也就是Base類,比如說多個Module共同使用的資源文件董瞻,我們都可以放在一個Module里面寞蚌,另外就是還有第三方依賴,這里我新建了兩個Module一個是BaseModule钠糊,一個是LibraryModule挟秤。整體關系如下

業(yè)務組件——>路由組件——>基礎組件

模式切換

定義開關

切換的時候需要一個開關,來表示是單個Module間運行還是多個Module間運行抄伍,很容易想到是一個布boolean類型的標志艘刚,可能你也想到了,在gradle.properties中來定義截珍,網(wǎng)上好像都是這么做的攀甚,實際上我們還可以在BaseModule以及LibraryModule定義,原因很簡單岗喉,只需要所有的Module中都能夠訪問就行了秋度,只要遵循這個原則都是OK的,只是在gradle.properties中定義跟使用都比較方便沈堡。

isDebug=false//Debug還是Release
isModuleRun=true//是否單Module運行

這里我不僅僅定義了isModuleRun静陈,還定義了isDebug,是不是感覺有些奇怪,不是可以通過BuildConfig.Debug來判斷當前是否是Debug模式么鲸拥,因為我們的url配置信息都是寫在BaseModule中以便于所有的Module調(diào)用拐格,他是一個Library,關于Library這里還有一個問題注意下刑赶,由于Library的Module打包方式是使用release模式打包的捏浊,所以BuildConfig.Debug永遠是false,所以我們需要額外定義一個變量isDebug撞叨,然后手動在Debug跟Release中進行切換金踪,然后在BaseModule的gradle中進行判斷

if (isDebug.toBoolean()) {
    //debug模式
    buildConfigField "String", "AlphaUrl", "\"${url["debug"]}\""

} else {
    //release模式
    buildConfigField "String", "AlphaUrl", "\"${url["release"]}\""

}
使用開關
Application

isModuleRun為false的時候,Application跟AndroidManifest都是以Library的形式參與編譯牵敷,不需要啟動的Activity以及自定義的Application反之則需要胡岔。

isModuleRun=false

無序修改

<application
    android:allowBackup="true"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
 </application>

isModuleRun=false

在main/debug目錄下新建一個AndroidManifest.xml文件

<application
    android:name=".debug.GoodsApplication"
    android:allowBackup="true"
    android:label="@string/goods_name"
    android:supportsRtl="true"
    tools:replace="android:label"
    android:theme="@style/AppTheme">
    <activity android:name=".GoodsActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
 </application>

引用方式

在Module的gradle目錄下進行引用

修改插件

if (isModuleRun.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

新增applicationId

if (isModuleRun.toBoolean()) {
    applicationId "com.wustor.cartmoudle"
}

切換AndroidManifest文件

sourceSets {
    main {
        if (isModuleRun.toBoolean()) {
            manifest.srcFile 'src/main/debug/AndroidManifest.xml'
        } else {
            manifest.srcFile 'src/main/AndroidManifest.xml'
            java {
                //全部Module一起編譯的時候剔除debug目錄
                exclude '**/debug/**'
            }
        }
    }
}

資源沖突

假如我們在CartModule中定義了一個Application,然后在當前Module中的strings.xml中定義了app_name枷餐,同時在OrderModule中的strings.xml中也定義了這個app_name靶瘸,那么合并你的時候就會出現(xiàn)沖突,我們只可以通過將上述字段分別改成cart_name跟order_name來解決這個問題毛肋,在嚴格的開發(fā)規(guī)范下怨咪,可以通過這種差異化命名來解決,因為不同的Module基本上資源文件的名稱基本都不一樣润匙,即時沖突也是少量的沖突诗眨,很容易解決。

當然除了這種方式之外可以在build.gradle中給資源文件名添加前綴

resourcePrefix "cart_"

可以強行檢查孕讳,命名都需要價格前綴匠楚,這樣反而違背了組件化的初衷,使得操作變麻煩了卫病,不過感覺這種方式不是很有必要油啤,當然有時候還可能出現(xiàn)圖片名字相同典徘,這個其實可以還原到組件化之前的項目中分析蟀苛,是不可能發(fā)生的事情,所以歸根到底還是沒有良好的開發(fā)規(guī)范跟開發(fā)習慣造成逮诲,沒必要為這種去做一些修改帜平,畢竟約定大于配置

依賴配置

通過最開始的整體架構(gòu)圖可以看出來梅鹦,凡是能夠在Library跟Application之間進行切換的Module毫無疑問是需要依賴我們Base的兩個Module的裆甩,其實可以合并成一個Module,我這里分了兩個齐唆,一個是BaseModule嗤栓,一個是LibraryModule。下面通過build.gradle中的配置來梳理一下他們的依賴關系:

MainModule
dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile project(':routermodule')
}

編譯期間組件進行隔離,所以MainModule只依賴了RouterModule茉帅,剛才說的還有在運行期按需依賴叨叙,這里是通過gradle的腳本實現(xiàn)控制的

//編譯期組件隔離,運行期組件按需依賴
//mainModule需要跟cartModule,goodsModule,usersModule進行交互堪澎,所以在運行期添加了依賴
def tasks = project.gradle.startParameter.taskNames
for (String task : tasks) {
    def upperName = task.toUpperCase()
    if (upperName.contains("ASSEMBLE") || upperName.contains("INSTALL")) {
        dependencies.add("compile", project.project(':' + 'cartmodule'))
        dependencies.add("compile", project.project(':' + 'goodsmodule'))
        dependencies.add("compile", project.project(':' + 'usermodule'))
        dependencies.add("compile", project.project(':' + 'ordermodule'))
    }
}
BusinessModule

這里指的是Goods/Cart/User/OrderModule擂错,其實是平行的

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile project(':routermodule')
}

業(yè)務Module依賴于RouterModule

RouterModule
dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile project(':modulelib')
    compile 'com.alibaba:arouter-api:1.2.1.1'
}

RouterModule依賴了LibraryModule

BaseModule
dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    testCompile 'junit:junit:4.12'
    compile project(':librarymodule')
}

BaseModule作為一個基礎庫,依賴了LibraryModule

LibraryModule

這個作為最底層的勞苦大眾樱蛤,實際上就是提供了一個依賴钮呀,所以就沒有什么好依賴,只能自己跟自己玩兒昨凡。

所以到這里的話爽醋,基本的依賴關系已經(jīng)很清楚了,知道了整個架構(gòu)圖便脊,接下來進行施工也就很簡單了

組件通信

其實在當初進行模塊劃分的時候子房,是根據(jù)業(yè)務來的,所以當我們進入到一個模塊之后就轧,大部分邏輯應該還是在這個模塊內(nèi)進行處理的证杭,但是偶爾還是會跟別的Module進行打交道,看一個界面

router

就拿GoodsModuleCartModule來說妒御,這兩個Module是可以進行相互跳轉(zhuǎn)的解愤,在GoodsModule的列表頁面點擊購物車圖標可以進入到CartModule的購物車列表,購物車列表點擊商品也可以進入GoodsModule的商品詳情頁乎莉。除了這個跳轉(zhuǎn)實際上還有變量的獲取送讲,比如在首頁,我需要同時獲取到GoodsModule中的HomeFragment惋啃、SortFragment哼鬓,CartModule中的CartFragment,UserModule中的MineFragment边灭。我是在MainModule中直接依賴了四個業(yè)務Module异希,實際上可以不這樣,我們也可以使用Arouter來進行獲取Fragment的實例绒瘦。

獲取實例

其實這里的實例大多數(shù)情況下指的就是Fragment称簿,下面以Fragment為例,別的實例如法炮制即可

  • 反射獲取

由于模塊間是隔離的惰帽,所以我們沒辦法直接創(chuàng)建Fragment的實例憨降,那么這個時候其實很容易想到的就是反射,發(fā)射可謂無所不能该酗,下面貼一下代碼授药。

//獲取Fragment實例
public static Fragment getFragment(String className) {
    Fragment fragment;
    try {
        Class fragmentClass = Class.forName(className);
        fragment = (Fragment) fragmentClass.newInstance();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
    return fragment;
}
  • Arouter

Arouter是阿里巴巴退出的一款路由框架,在組件中進行路由操作表方便,下面舉例說明

目標Fragment中加入注解

@Route(path = "cart/fragment")
public class CartFragement extends BaseFragment{
}

在任何地方獲取實例

Fragmetn fragment = (Fragment) ARouter.getInstance().build("/cart/fragment").navigation();
方法調(diào)用

在不同的Module之間都存在方法的調(diào)用悔叽,我們可以在每個Module里面定義一個接口航邢,并且實現(xiàn)這個接口,然后在需要調(diào)用的地方獲取到這個接口骄蝇,然后進行方法調(diào)用即可膳殷。為了統(tǒng)一管理,我們把每個Module的接口都定義在RouterModule里面九火,然后由于各個業(yè)務Module都依賴于這個RouteModule赚窃,然后只需要通過反射獲取到這個接口,進行方法調(diào)用就可以了岔激。

ModuleService

ModuleCall?

Module之間回調(diào)的接口

public interface ModuleCall {
   //調(diào)用init方法可以傳遞Context參數(shù)
    void initContext(Context context);
}

Service接口繼承自ModuleCall可以定義一些回調(diào)方法供本身之外的其他Module進行調(diào)用

public interface AppService extends ModuleCall {
    //TODO 調(diào)用方法自定義
    void showHome();
    void finish();

}

Impl實現(xiàn)類則是對應在每個Module中的具體回調(diào)勒极,是實現(xiàn)Service接口的直接子類

public class AppServiceImpl implements AppService {
    @Override
    public void showHome() {
    }
    @Override
    public void finish() {
    }
    @Override
    public void initContext(Context context) {
    }
}

下面還是通過反射跟Arouter兩種方式進行說明

  • 反射調(diào)用

    public static Object getModuleCall(String name) {
        T t;
        try {
            Class aClass = Class.forName(name);
            t = (T) aClass.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return t;
    }
    

獲取接口

AppService appService = (AppService) ReflectUtils.getModuleCall(Path.APP_SERVICE);

其實跟獲取Fragment實例一樣,通過類名來獲取對應的接口虑鼎,然后調(diào)用對應的方法就行辱匿,有一點需要注意的就是,如果獲取的接口之后調(diào)用的方法需要傳入Context參數(shù)炫彩,那么在調(diào)用接口方法之前必須先調(diào)用initContext方法才能使用傳入的Context匾七,不然會報空指針異常。

  • Arouter

Arouter中有一個IProvider接口江兢,如下

public interface IProvider {
    void init(Context var1);
}

其實IProvider跟上面的ModuleCall是一樣的昨忆,只不過他在獲取到接口實例之后,就會調(diào)用initContext方法杉允,其中的Context來自ARouter.init(this)中傳入的參數(shù)邑贴,不需要我們再手動調(diào)用initContext。

目標類中注入路徑

@Route(path = Path.APP_SERVICE)
public class AppServiceImpl implements AppService {
    private Context mContext;
    @Override
    public void showHome() {
        Log.d("go--->", "home--->");
    }

    @Override
    public void finish() {
    }

    @Override
    public void init(Context context) {
        mContext = context;
    }
}

任意地方獲取目標類

AppService appService = (AppService) RouterUtils.navigation(Path.APP_SERVICE);

然后調(diào)用方法即可

UI跳轉(zhuǎn)

跳轉(zhuǎn)基本上指的就是Activity之間的跳轉(zhuǎn)叔磷,廢話不多說拢驾,依舊是Arouter跟反射

  • 反射

    //將類名轉(zhuǎn)化為目標類
    public static void startActivityWithName(Context context, String name) {
        try {
            Class clazz = Class.forName(name);
            startActivity(context, clazz);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
    //獲取Intent
    public static Intent getIntent(Context context, Class clazz) {
        return new Intent(context, clazz);
    }
    //啟動Activity
    public static void startActivity(Context context, Class clazz) {
        context.startActivity(getIntent(context, clazz));
    }
    
  • Arouter

    將目標Activity注冊到Arouter

    @Route(path = Path.CART_MOUDLE_CART)
    public class CartActivity extends BaseActivity<UselessPresenter, UselessBean> {
    }
    

    啟動目標Activity

     ARouter.getInstance().build(Path.CART_MOUDLE_CART).navigation()
    

入口參數(shù)

Application

當組件單獨運行的時候,每個Module自成一個APK改基,那么就意味著會有多個Application繁疤,很顯然我們不愿意重復寫這么多代碼,所以我們只需要定義一個ModuleApplication即可寥裂,其它的Application直接繼承此ModuleApplication就OK了嵌洼,看一下結(jié)構(gòu)圖:

ModuleApplication

實際上所有的邏輯都是在ModuleApplication中,業(yè)務Module分別有自己的子類封恰,通過子類可以對Application做一些自己的定制化操作。

無參原因

之前在網(wǎng)上看到過攜程以及得到的組件化褐啡,他們從MainModule進入到別的Module貌似都是不需要傳參數(shù)的诺舔,所以不管是組件單獨調(diào)試還是所有的Module一起遠行對于從ModuleA跳轉(zhuǎn)到ModuleB都是不需要傳參的。但是很多時候不同的Module間跳轉(zhuǎn)是需要傳參的,就拿購物車來說低飒,我單獨調(diào)試的時候是需要知道用戶的加密的userId许昨,才能向服務器請求數(shù)據(jù),如果是多個Module一起運行褥赊,訪問購物車的時候糕档,是可以從別的Module取到userId的,單獨調(diào)試的時候就沒法獲取到拌喉,也就是入口的時候沒有參數(shù)對購物車進行初始化速那。

解決方式

因為當我們在組件化進行調(diào)試的時候,我們每個Module在cartmodule/src/main/debug目錄下有自己的Application尿背,對于入口參數(shù)比較簡單的情況端仰,我們可以直接在Application中寫死,而對于一些比較復雜的或者動態(tài)的參數(shù)田藐,我們可以繼續(xù)在此目錄下新疆一個Activity來配置我們單Module調(diào)試所需要的參數(shù)荔烧,然后在整個項目進行編譯的時候剔除debug目錄下的文件。

sourceSets {
    main {
        if (isModuleRun.toBoolean()) {
            manifest.srcFile 'src/main/debug/AndroidManifest.xml'
        } else {
            manifest.srcFile 'src/main/AndroidManifest.xml'
            java {
                //release 時 debug 目錄下文件不需要合并到主工程
                exclude '**/debug/**'
            }
        }
    }
}

總結(jié)

項目組件化運行了一段時間汽久,通過劃分Module鹤竭,單獨調(diào)試,確實大大提升了開發(fā)效率景醇,隨著使用的時間的推移诺擅,也在對組件化的理解也進一步加深,也在不斷地完善啡直,下面幾點是在組件化過程中總結(jié)的一些經(jīng)驗烁涌。

  • Module劃分:在劃分Module的時候沒必要劃分地太細,但是要嚴格按照業(yè)務來劃分酒觅,這樣單獨調(diào)試對于習作開發(fā)才有意義撮执。
  • Module隔離:業(yè)務Module之間應該是相互隔離不可見的,不能相互依賴舷丹,如果相互之間需要通信抒钱,則必須經(jīng)過路由轉(zhuǎn)發(fā),便于統(tǒng)一管理颜凯。
  • 面向接口編程:不管是也業(yè)務Module還是BaseModule谋币、LibraryModule以及RouterModule,在對外提供服務的時候盡可能的以接口的形式症概,不同的Module對外提供的服務接口應該都有一個共同的抽象父類蕾额,便于管理。
  • 防止循環(huán)依賴:循環(huán)依賴就是A依賴B彼城,B依賴A,在運行期間動態(tài)添加依賴的時候诅蝶,一定要考慮這個依賴是否被添加到項目中去了退个,所謂添加到項目中就是但凡被其它的Module進行依賴過就算添加進項目中,不然很容易造成循環(huán)依賴调炬。

代碼下載

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末语盈,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子缰泡,更是在濱河造成了極大的恐慌刀荒,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棘钞,死亡現(xiàn)場離奇詭異缠借,居然都是意外死亡,警方通過查閱死者的電腦和手機武翎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門烈炭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人宝恶,你說我怎么就攤上這事符隙。” “怎么了垫毙?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵霹疫,是天一觀的道長。 經(jīng)常有香客問我综芥,道長丽蝎,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任膀藐,我火速辦了婚禮屠阻,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘额各。我一直安慰自己国觉,他們只是感情好,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布虾啦。 她就那樣靜靜地躺著麻诀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪傲醉。 梳的紋絲不亂的頭發(fā)上蝇闭,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機與錄音硬毕,去河邊找鬼呻引。 笑死,一個胖子當著我的面吹牛昭殉,可吹牛的內(nèi)容都是我干的苞七。 我是一名探鬼主播藐守,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼挪丢,長吁一口氣:“原來是場噩夢啊……” “哼蹂风!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起乾蓬,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤惠啄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后任内,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體撵渡,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年死嗦,在試婚紗的時候發(fā)現(xiàn)自己被綠了趋距。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡越除,死狀恐怖节腐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情摘盆,我是刑警寧澤翼雀,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站孩擂,受9級特大地震影響狼渊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜类垦,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一狈邑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蚤认,春花似錦米苹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至氯析,卻和暖如春亏较,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背掩缓。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工雪情, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人你辣。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓巡通,卻偏偏與公主長得像尘执,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子宴凉,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容