Android進階知識樹——必須會的組件化技術

1、概述

筆者從事智能傢俱行業的開發工作,也是從公司創業團隊工作到現在,對於公司的項目從1.0版本開始接手一直到現在,雖說項目不是很大但麻雀雖小五臟俱全,在項目和團隊的不斷擴大、暴露出的問題也不段增多,組件化勢在必行,本文就根據整個項目的發展,總結下組件化的實踐流程;

1.0版本
在最初的1.0版本中只是針對一個智能設備的操控和數據交互,項目本身就很簡單此時也基本單人開發,所以所有的功能代碼都直接在app中開發,但隨着業務的增長和對未來的規劃,項目進入2.0階段
在這裏插入圖片描述
2.0階段的業務比1.0增加了電商、社區、內容等業務模塊,同時智能設備也由原來的單一設備變成多個設備,此時如果只在app中開發,會導致單個Module中代碼急劇膨脹,代碼耦合度高,而且業務增多後團隊面臨擴張,此時業務模塊之間的耦合,在多人協作開發時也暴露出來,而且由於行業的需求有時會有臨時的Demo和定製化的應用,在原來的項目上很難實現這些需求,此時必須對原來的項目代碼進行組件化操作;

2、組件化基礎

在進行組件化操作之前,先區分兩個概念:模塊化和組件化

  • 組件化:單一的功能組件,要求能獨立開發並且脫離業務程序,實現組件的複用,如:藍牙組件、音樂組件
  • 模塊化:模塊化主要針對業務,將單獨的業務功能分離開發,每個功能模塊之間進行代碼解耦,在編譯時可以自由的添加或減少模塊,如:社區模塊、電商模塊等

由上面的介紹知道,組件化針對更細更單一的業務,功能模塊粒度較大,針對某個方面的整體業務,當然業務當中可能使用很多的獨立組件,按照組件化的需求項目的架構進入3.0
在這裏插入圖片描述
上面已智能、內容兩個模塊爲例,在項目組件化操作後的架構圖,架構從下向上依次爲:

  • 基礎層:主要封裝常用的工具類和一些封裝的基礎庫,如數據存儲、網絡請求等
  • 組件層:針對單一的供分離解耦出獨立的功能組件
  • 業務模塊層:針對獨立相近的業務模塊進行分離,根據各自的需求引入相應的功能組件
  • APP層:APP層爲項目的最頂層,將所有的功能模塊組合在APP框架中實現真個APP編譯

3、組件化

由上面的3.0版本架構知道,項目中包含多個功能組件和業務模塊,在開發中要保證組件間不能耦合,業務木塊依賴於組件,但業務模塊之間也不能相互引用,否則違背了組件化的原則;

  • 組件化的最終目的
  1. 實現組件間、模塊間的代碼解耦和代碼隔離,減少項目的維護成本
  2. 實現組件的複用
  3. 實現功能組件和業務模塊的單獨調試和整體編譯,減少項目的開發編譯時間
  • 組件化要解決的問題
  1. 實現組件既能單獨編譯也能整體編譯,縮短程序的編譯時間
  2. 組件和Module中如何動態配置Application
  3. 組件間的數據傳遞
  4. 組件和模塊間的界面跳轉
  5. 主項目與業務模塊間的解耦,從而實現增加和刪除模塊
    在這裏插入圖片描述
3.1、組件的單獨調試
  • 在Android開發中,Gradle提供三種構建形式:
  1. App 插件,id: com.android.application
  2. Library 插件,id: com.android.libray
  3. Test 插件,id: com.android.test

在我們實際開發中app 構建形式爲application,最終編譯成APK文件,其餘所依賴的Module編譯形式爲library,最終已arr形式尋在提供API調用,換句話說只要修改組件的編譯形式即可實現單獨編譯的功能,所以在組件下創建gradle.properties文件用於控制構建形式

isRunAlone = false

在build.gradle中根據isRunAlone的變量修改構建形式

if (isRunAlone.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
  • 配置applicationId
    if (isRunAlone.toBoolean()) {
            applicationId "com.alex.kotlin.content"
        }
  • 配置AndroidManifest文件

在組件化單獨編譯和整體編譯時,註冊清單中所需要的內容不同,如單獨編譯需要額外的啓動頁,且單獨編譯時也休要配置不同的Application,此時在main文件加下創建manifest/AndroidMenifest.xml文件,根據單獨編譯的需要設置內容。

  1. 整體編譯
    在這裏插入圖片描述
  2. 單獨編譯
    在這裏插入圖片描述
  3. 在build.gradle中配置不同的文件路徑
sourceSets {
        main {
            if (isRunAlone.toBoolean()) {
                manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }

到此編譯配置完成,在需要單獨編譯時只需要修改isRunAlone爲true即可;

3.2、組件動態初始化Application

由上面配置的兩個註冊清單文件中可見,在App整體編譯時組件使用的是全局的Application,在單獨編譯時使用的是AutoApplication,大家都知道一個程序中只有一個Application類,那組件中需要初始化的代碼都配置在自己的AutoApplication中,那整體編譯時如何初始化呢?可能有同學說整體編譯時個組件和模塊是可見的,直接調用AutoApplication類完成初始化,但此種情況主項目就無法實現模塊的自由增減,而且當代碼隔離時AutoApplication就不可見了,這裏採用一種配置+反射的方式舒適化各組件的Application,具體實現如下:

  • 在base組件中聲明BaseApp抽象類,BaseApp繼承Application類
abstract class BaseApp : Application(){
    /**
     * 初始化Module中的Application
     */
    abstract fun initModuleApp(application: Application)
}
  • 在組件中實現此BaseApp類,在initModuleApp()配置整體編譯時時初始化的代碼
class AutoApplication : BaseApp() {
    override fun onCreate() { //單獨編譯時初始化
        super.onCreate()
        MultiDex.install(this)
        AppUtils.setContext(this)
        initModuleApp(this)
        ServiceFactory.getServiceFactory().loginToService = AutoLoginService()
    }
    override fun initModuleApp(application: Application) { //整體編譯
        ServiceFactory.getServiceFactory().serviceIntelligence = AutoIntelligenceService()
    }
}
  • 在Base組件中創建AppConfig類,配置初始化時要加載的BaseApp的子類
object AppConfig {
    private const val BASE_APPLICATION = "com.pomelos.base.BaseApplication"
    private const val CONTENT_APPLICATION = "com.alex.kotlin.content.ContentApplication"
    private const val AUTO_APPLICATION = "com.alex.kotlin.intelligence.AutoApplication"

    val APPLICATION_ARRAY = arrayListOf(BASE_APPLICATION, CONTENT_APPLICATION, AUTO_APPLICATION)
}
  • 在主Application中反射調用所有的Application
public class GlobalApplication extends BaseApp {
	@SuppressLint("StaticFieldLeak")
	private static GlobalApplication instance;
	public GlobalApplication() {}
	@SuppressWarnings("AlibabaAvoidManuallyCreateThread")
	@Override
	public void onCreate() {
		super.onCreate();
		MultiDex.install(this);
		AppUtils.setContext(this);
		if (BuildConfig.DEBUG) {
			//開啓Debug
			ARouter.openDebug();
			//開啓打印日誌
			ARouter.openLog();
		}
		//初始化ARouter
		ARouter.init(this);
		ServiceFactory.Companion.getServiceFactory().setLoginToService(new AppLoginService());
		//初始化組件的Application
		initModuleApp(this);
	}
	@Override
	public void initModuleApp(@NotNull Application application) {
		for (String applicationName : AppConfig.INSTANCE.getAPPLICATION_ARRAY()) { //遍歷所有配置的Application
			try {
				Class clazz = Class.forName(applicationName); //反射執行
				BaseApp baseApp = (BaseApp) clazz.newInstance(); //創建實例
				baseApp.initModuleApp(application); // 執行初始化
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
			} 
		}
	}
}

以上通過在AppConfig中配置所有的Application的路徑,在主Application執行時反射創建每個實例,調用對應的initModuleApp()完成所有的配置,不知有沒有注意到在AutoApplication中同樣在onCreate()中初始化了內容,此處是爲了在單獨編譯時調用;

3.3、組件間的數據傳遞

在項目中因爲有時需要打包不同需求的APK,所以我將login單獨分離出成組件同一登錄行爲,那麼在特務模塊依賴Login之後即可實現登錄功能,但每個單獨的業務獨立編譯時會產生多個APK,這些APK都需要獲取登錄狀態及跳轉相應的首界面,那麼在保證程序解耦的情況下如何實現呢?答案及時使用註冊接口實現;

  1. 在Base組件中聲明LoginToService接口
interface LoginToService {
    /**
     * 實現登錄後的去向
     */
    fun goToSuccess()
}
  1. 在base中創建ServiceFactory,同時單例對外提供調用
class ServiceFactory private constructor() {
    companion object {
        fun getServiceFactory(): ServiceFactory {
            return Inner.serviceFactory
        }
    }
    private object Inner {
        val serviceFactory = ServiceFactory()
    }
}
  1. 在ServiceFactory中聲明LoginToService對象,同時提供LoginToService的空實現
var loginToService: LoginToService? = null
        get() {
            if (field == null) {
                field = EmptyLoginService()
            }
            return field
        }
  1. 在對應的業務模塊中實現LoginToService,重寫方法設置需要跳轉的界面
class AppLoginService : LoginToService { //App模塊
    override fun goToSuccess() {
        val intent = Intent(AppUtils.getContext(), MainActivity::class.java)
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
        AppUtils.getContext().startActivity(intent)
    }
}

class AutoLoginService : LoginToService { // 智能模塊
    override fun goToSuccess() {
        val intent = Intent(AppUtils.getContext(), AutoMainActivity::class.java)
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
        AppUtils.getContext().startActivity(intent)
    }
}
  1. 在初始化Application中向ServiceFactory註冊各自的實例
ServiceFactory.getServiceFactory().serviceIntelligence = AutoIntelligenceService()
  1. 在login組件中完成登錄後即可調用ServiceFactory中註冊對象的方法實現跳轉
override fun loadSuccess(loginBean: LoginEntity) {
        ServiceFactory.getServiceFactory().loginToService?.goToSuccess()
    }

各組件通過向base組件中的ServiceFactory註冊的方式,對外提供執行的功能,因爲ServiceFactory單例調用,所以在其他組件中通過ServiceFactory獲取註冊的實例後即可執行方法,爲了在減去組件或模塊時防止報錯,在base中同樣提供了服務的空實現;

3.4、組件間的界面跳轉

關於頁面跳轉推薦使用阿里的ARoute框架,詳情見另一篇文章:Android框架源碼分析——以Arouter爲例談談學習開源框架的最佳姿勢

3.5、主項目與業務模塊間的解耦

在一般項目中,主app的首界面都來自不同的業務模塊組成,最常見的就是使用不同組件的Fragment和ViewPager組合,但此時主App需要獲取組件中的Fragment實例,按照組件化的思想不能直接使用,否則主APP和組件、模塊間又會耦合在一起,此處也是採用接口模式處理,過程和數據交互大致相同;

  • 在base組件中聲明接口,在對應的模塊中實現接口
interface ContentService {
    /**
     * 返回實例化的Fragment
     */
    fun newInstanceFragment(): BaseCompatFragment?
}
// 內容模塊實現
class ContentServiceImpl : ContentService {
    override fun newInstanceFragment(): BaseCompatFragment? {
        return ContentBaseFragment.newInstance() //提供Fragment對象
    }
}
  • 在初始化Application過程中註冊服務
   ServiceFactory.getServiceFactory().serviceContent = ContentServiceImpl()
  • 在主App中通過ServiceFactory獲取
  mFragments[SECOND] = ServiceFactory.getServiceFactory().serviceContent?.newInstanceFragment()
3.6、其他問題
  • 代碼隔離

雖然經歷組件化將代碼解耦,但在開發中如果依賴的組件或模塊中的方法總是可見,萬一在開發中使用了其中的代碼,那程序程序又會耦合在一起,如何能讓組件和模塊中的方法不可見呢?答案就在runtimeOnly依賴,他可以在開發過程中隔離代碼,在編譯時代碼可見

    runtimeOnly project(':content')
    runtimeOnly project(':intelligence')
  • 資源隔離

runtimeOnly依賴實現了代碼隔離,但對資源並沒有效果,使用中還是可能會直接引用資源,爲了防止這種現象,爲每個組件的資源加上特有的前綴

  resourcePrefix "auto_"

此時該Module下的資源都必須以auto_開頭否則會警告;
在這裏插入圖片描述

  • ContentProvider

由於項目中使用到了ContentProvider,(不瞭解的點擊Android進階知識樹——ContentProvider使用和工作過程詳解)在整體編譯安裝在手機後可以正常運行,此時要單獨編譯時總是提示安裝失敗,最終原因就是兩個Apk中的ContentProvider和權限一致導致,那如何保證單獨編譯和整體編譯時權限不同,從而安裝成功呢?我們首先在上面的連個Menifest文件中配置Provider

  • 單獨編譯
  <provider
            android:name=".database.MyContentProvider"
            android:authorities="com.alex.kotlin.intelligence.database.MyContentProvider"
            android:exported="false" />
  • 整體編譯
<provider
            android:name=".database.MyContentProvider"
            android:authorities="com.findtech.threePomelos.database.MyContentProvider"
            android:exported="false" />

這樣兩個權限不同的Provider即可安裝成功,在使用時需要根據權限執行ContentProvider,那麼如何在代碼中根據不同編譯類型,拼接對應的執行權限呢?此處使用在build.gradle中配置BuildConfig來處理,將權限直接配置在BuildConfig中,在使用時直接獲取即可

 if (isRunAlone.toBoolean()) {
            buildConfigField 'String','AUTHORITY','"com.alex.kotlin.intelligence.database.MyContentProvider"'
        }else {
            buildConfigField 'String','AUTHORITY','"com.findtech.threePomelos.database.MyContentProvider"'
        }
        
   const val AUTHORITY = BuildConfig.AUTHORITY //使用

4、總結

解決上面的所有問題後,項目的組件化基本可以實現,但具體的劃分粒度和細節,需要自身結合業務和經驗去處理,可能有些需要直接分離組件,也可能小的功能需要放在base組件中共享,而且每個人針對每個項目的處理方式也不同,只要理解組件化的思想和方式實現最終的需求即可;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章