Android 手把手帶你搭建一個組件化項目架構

一、組件化

作為一個單工程擼到底的開發(fā)人員,想試著將項目進行組件化改造,說動就動。畢竟技術都是寫出來的,看著文章感覺懂了,但是實際開發(fā)中還是能遇到各種各樣的問題,開始搞起來。

1.1 為什么使用組件化

一直使用單工程擼到底,項目越來越大導致出現(xiàn)了不少的問題:

  • 查找問題慢:定位問題,需要在多個代碼混合的模塊中尋找和跳轉。

  • 開發(fā)維護成本增加:避免代碼的改動影響其它業(yè)務的功能,導致開發(fā)和維護成本不斷增加。

  • 編譯時間長:項目工程越大,編譯完整代碼所花費的時間越長。

  • 開發(fā)效率低:多人協(xié)作開發(fā)時,開發(fā)風格不一,又很難將業(yè)務完全分割,大家互相影響,導致開發(fā)效率低下。

  • 代碼復用性差:寫過的代碼很難抽離出來再次利用。

1.2 模塊化與組件化

1.2.1 模塊

一個程序按照其功能做拆分,分成相互獨立的模塊,以便于每個模塊只包含與其功能相關的內(nèi)容,比如登錄模塊首頁模塊等等。

1.2.2 組件

組件指的是單一的功能組件,如登錄組件、視頻組件支付組件 等,每個組件都可以以一個單獨的 module 開發(fā),并且可以單獨抽出來作為 SDK 對外發(fā)布使用??梢哉f往往一個模塊包含了一個或多個組件。

1.3 組件化的優(yōu)勢

組件化基于可重用的目的,將應用拆分成多個獨立組件,以減少耦合:

  • 加快編譯速度:每個業(yè)務功能都是一個單獨的工程,可獨立編譯運行,拆分后代碼量較少,編譯自然變快。

  • 解耦:通過關注點分離的形式,將App分離成多個模塊,每個模塊都是一個組件。

  • 提高開發(fā)效率:多人開發(fā)中,每個組件模塊由單人負責,降低了開發(fā)之間溝通的成本,減少因代碼風格不一而產(chǎn)生的相互影響。

  • 代碼復用:類似我們引用的第三方庫,可以將基礎組件或功能組件剝離。在新項目微調(diào)或直接使用。

1.4 組件化需要解決的問題

  • 組件分層:怎么將一個項目分成多個組件、組件間的依賴關系是怎么樣的?

  • 組件單獨運行和集成調(diào)試:組件是如何獨立運行和集成調(diào)試的?

  • 組件間通信:主項目與組件、組件與組件之間如何通信就變成關鍵?

二、組件分層

組件依賴關系是上層依賴下層,修改頻率是上層高于下層。先上一張圖:

image

2.1 基礎組件

基礎公共模塊,最底層的庫:

  • 封裝公用的基礎組件;
  • 網(wǎng)絡訪問框架、圖片加載框架等主流的第三方庫;
  • 各種第三方SDK。

2.2 common組件(lib_common)

  • 支撐業(yè)務組件、功能組件的基礎(BaseActivity/BaseFragment等基礎能力;
  • 依賴基礎組件層;
  • 業(yè)務組件、功能組件所需的基礎能力只需要依賴common組件即可獲得。

2.3 功能組件

  • 依賴基礎組件層;
  • 對一些公用的功能業(yè)務進行封裝與實現(xiàn);
  • 業(yè)務組件可以在library和application之間切換,但是最后打包時必須是library ;

2.4 業(yè)務組件

  • 可直接依賴基礎組件層;同時也能依賴公用的一些功能組件;
  • 各組件之間不存在依賴關系,通過路由進行通信;
  • 業(yè)務組件可以在library和application之間切換,但是最后打包時必須是library ;

2.5 主工程(app)

  • 只依賴各業(yè)務組件;

  • 除了一些全局的配置和主Activity之外,不包含任何業(yè)務代碼,是應用的入口;

2.6 完成后項目

image

這只是個大概,并不是說必須這樣,可以按照自己的方式來。比如:你覺得基礎組件比較多導致project里面的項目太多,那么你可以創(chuàng)建一個lib_base,然在lib_base里面再創(chuàng)建其他基礎組件即可。

三、組件單獨調(diào)試

3.1 創(chuàng)建組件(收藏)

image
  • library和application之間切換:選擇第一項。

  • 始終是library:選擇第二項

這樣盡可能的減少變動項,當然這僅僅是個建議,看個人習慣吧。

因為咱們創(chuàng)建的是一個module,所以在AndridManifest中添加android:exported="true"屬性可直接構建一個APK。下面咱們看看如何生成不同的工程類型。

3.2 動態(tài)配置組件的工程類型

在 AndroidStudio 開發(fā) Android 項目時,使用的是 Gradle 來構建,具體來說使用的是 Android Gradle 插件來構建,Android Gradle 中提供了三種插件,在開發(fā)中可以通過配置不同的插件來構建不同的工程。

3.2.1 build.gradle(module)

//構建后輸出一個 APK 安裝包
apply plugin: 'com.android.application'
//構建后輸出 ARR 包
apply plugin: 'com.android.library'
//配置一個 Android Test 工程
apply plugin: 'com.android.test'

獨立調(diào)試:設置為 Application 插件。

集成調(diào)試:設置為 Library 插件。

3.2.2 設置gradle.properties

image

isDebug = true 獨立調(diào)試

3.2.3 動態(tài)配制插件(build.gradle)

//注意gradle.properties中的數(shù)據(jù)類型都是String類型,使用其他數(shù)據(jù)類型需要自行轉換
if(isDebug.toBoolean()){
    //構建后輸出一個 APK 安裝包
    apply plugin: 'com.android.application'
}else{
    //構建后輸出 ARR 包
    apply plugin: 'com.android.library'
}

3.3 動態(tài)配置組件的 ApplicationId 和 AndroidManifest 文件

  • 一個 APP 是只有一個 ApplicationId ,所以在單獨調(diào)試集成調(diào)試組件的 ApplicationId 應該是不同的。

  • 單獨調(diào)試時也是需要有一個啟動頁,當集成調(diào)試時主工程和組件的AndroidManifest文件合并會產(chǎn)生多個啟動頁。

根據(jù)上面動態(tài)配制插件的經(jīng)驗,我們也需要在build.gradle中動態(tài)配制ApplicationId 和 AndroidManifest 文件。

3.3.1 準備兩個不同路徑的 AndroidManifest 文件

image

有什么不同?咱們一起看看具體內(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()){
            //獨立調(diào)試的時候才能設置applicationId
            applicationId "com.scc.module.collect"
        }
    }
    sourceSets {
        main {
            if (isDebug.toBoolean()) {
                //獨立調(diào)試
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                //集成調(diào)試
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }

3.4 實現(xiàn)效果

3.4.1 獨立調(diào)試

isDebug = true

image

3.4.2 集成調(diào)試

isDebug = false

image

四、Gradle配置統(tǒng)一管理

4.1 config.gradle

當我們需要進行插件版本、依賴庫版本升級時,項目多的話改起來很麻煩,這時就需要我們對Gradle配置統(tǒng)一管理。如下:

image

具體內(nèi)容

ext{
    //組件獨立調(diào)試開關, 每次更改值后要同步工程
    isDebug = true
    android = [
            // 編譯 SDK 版本
            compileSdkVersion: 31,
            // 最低兼容 Android 版本
            minSdkVersion    : 21,
            // 最高兼容 Android 版本
            targetSdkVersion : 31,
            // 當前版本編號
            versionCode      : 1,
            // 當前版本信息
            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ù)類型需要自行轉換
if(isDebug.toBoolean()){
    //構建后輸出一個 APK 安裝包
    apply plugin: 'com.android.application'
}else{
    //構建后輸出 ARR 包
    apply plugin: 'com.android.library'
}
android {
    compileSdkVersion 31

    defaultConfig {
        if(isDebug.toBoolean()){
            //獨立調(diào)試的時候才能設置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()) {
                //獨立調(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'
}

五、組件間界面跳轉(ARouter)

5.1 介紹

Android 中的界面跳轉那是相當簡單,但是在組件化開發(fā)中,由于不同組件式?jīng)]有相互依賴的,所以不可以直接訪問彼此的類,這時候就沒辦法通過顯式的方式實現(xiàn)了。

所以在這里咱們采取更加靈活的一種方式,使用 Alibaba 開源的 ARouter 來實現(xiàn)。

一個用于幫助 Android App 進行組件化改造的框架 —— 支持模塊間的路由、通信、解耦

文檔介紹的蠻詳細的,感興趣的可以自己實踐一下。這里做個簡單的使用。

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è)務組件都依賴lib_common,那么咱們先從lib_common開始配制

lib_common

dependencies {
    api root.libARouter
    ...
}

其他組件(如collect)

android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
                //如果項目內(nèi)有多個annotationProcessor,則修改為以下設置
                //arguments += [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
}

dependencies {
    //arouter-compiler的注解依賴需要所有使用 ARouter 的 module 都添加依賴
    annotationProcessor root.libARouterCompiler
    ...
}

5.2.2 添加注解

你要跳轉的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(主項目Application)

public class App extends BaseApplication {
    @Override
    public void onCreate() {
        super.onCreate();
        if (isDebug()) {           // 這兩行必須寫在init之前,否則這些配置在init過程中將無效
            ARouter.openLog();     // 打印日志
            ARouter.openDebug();   // 開啟調(diào)試模式(如果在InstantRun模式下運行,必須開啟調(diào)試模式!線上版本需要關閉,否則有安全風險)
        }
        ARouter.init(this); // 盡可能早,推薦在Application中初始化
    }
    private boolean isDebug() {
        return BuildConfig.DEBUG;
    }
}

5.3 發(fā)起路由操作

5.3.1 應用內(nèi)簡單的跳轉

ARouter.getInstance().build("/collect/CollectActivity").navigation();

這里是用module_main的HomeFragment跳轉至module_collect的CollectActivity界面,兩個module中不存在依賴關系。"/collect/CollectActivity"在上面已注冊就不多描述了。

效果如下:

image

5.3.2 跳轉并攜帶參數(shù)

這里是用module_main的MineFragment的Adapter跳轉至module_webview的WebViewActivity界面,兩個module中同樣不存在依賴關系。

啟動方

        ARouter.getInstance().build("/webview/WebViewActivity")
                .withString("url", bean.getUrl())
                .withString("content",bean.getName())
                .navigation();

這里傳了兩個參數(shù)urlname到WebViewActivity,下面咱們看看WebViewActivity怎么接收。

接收方

//為每一個參數(shù)聲明一個字段,并使用 @Autowired 標注
//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ù)和服務(這里用到@Autowired所以要設置)
        //不使用自動注入,可不寫,如CollectActivity沒接收參數(shù)就沒有設置
        ARouter.getInstance().inject(this);
        binding.btnBoom.setText(String.format("%s,你來啦", title));
        //加載鏈接
        initWebView(binding.wbAbout, url);
    }
}

上效果圖:

image

搞定,更多高級玩法可自行探索。

5.3.3 小記(ARouter目標不存在)

W/ARouter::: ARouter::There is no route match the path

這里出現(xiàn)個小問題,配置注釋都好好的,但是發(fā)送發(fā)無論如何都找不到設置好的Activity。嘗試方案:

  • Clean Project
  • Rebuild Project
  • 在下圖也能找到ARouter內(nèi)容。

后來修改Activity名稱好了。

image

六、組件間通信(數(shù)據(jù)傳遞)

界面跳轉搞定了,那么數(shù)據(jù)傳遞怎么辦,我在module_main中使用懸浮窗,但是需要判斷這個用戶是否已登錄,再執(zhí)行后續(xù)邏輯,這個要怎么辦?這里我們可以采用 接口 + ARouter 的方式來解決。

在這里可以添加一個 componentbase 模塊,這個模塊被所有的組件依賴。

這里我們通過 module_main組件 中調(diào)用 module_login組件 中的方法來獲取登錄狀態(tài)這個場景來演示。

6.1 通過依賴注入解耦:服務管理(一) 暴露服務

6.1.1 創(chuàng)建 componentbase 模塊(lib)

image

6.1.2 創(chuàng)建接口并繼承IProvider

注意:接口必須繼承IProvider,是為了使用ARouter的實現(xiàn)注入。

image

6.1.3 在module_login組件中實現(xiàn)接口

lib_common

所有業(yè)務組件和功能組件都依賴lib_common,所以咱們直接在lib_common添加依賴即可

dependencies {
    ...
    api project(":lib_componentbase")
}

module_login

dependencies {
    ...
    implementation project(':lib_common')
}

實現(xiàn)接口

//實現(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) {

    }
}

image

6.2 通過依賴注入解耦:服務管理(二) 發(fā)現(xiàn)服務

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());

            }
        });
    }
}

image

運行結果:

E/-SCC-: AccountServiceImpl.isLogin
E/-SCC-: Login:true
E/-SCC-: AccountServiceImpl.getAccountId
E/-SCC-: AccountId:1000

七、總結

本文介紹了組件化、組件分層、解決了組件的獨立調(diào)試、集成調(diào)試、頁面跳轉、組件通信等。

其實會了這些后你基本可以搭建自己的組件化項目了。其實最大的問題還是分組分層、組件劃分。這個就需要根據(jù)你的實際情況來設置。

本項目比較糙,后面會慢慢完善。比如添加Gilde、添加MMVK、添加Room等。

項目傳送門

本文轉自 https://juejin.cn/post/7033954652315975688,如有侵權,請聯(lián)系刪除。

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

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

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