筆者從事智能傢俱行業的開發工做,也是從公司創業團隊工做到如今,對於公司的項目從1.0版本開始接手一直到如今,雖然說項目不是很大但麻雀雖小五臟俱全,在項目和團隊的不斷擴大、暴露出的問題也不段增多,組件化勢在必行,本文就根據整個項目的發展,總結下組件化的實踐流程;java
在最初的1.0版本中只是針對一個智能設備的操控和數據交互,項目自己就很簡單此時也基本單人開發,因此全部的功能代碼都直接在app中開發,但隨着業務的增加和對將來的規劃,項目進入2.0階段 2.0階段的業務比1.0增長了電商、社區、內容等業務模塊,同時智能設備也由原來的單一設備變成多個設備,此時若是隻在app中開發,會致使單個Module中代碼急劇膨脹,代碼耦合度高,並且業務增多後團隊面臨擴張,此時業務模塊之間的耦合,在多人協做開發時也暴露出來,並且因爲行業的需求有時會有臨時的Demo和定製化的應用,在原來的項目上很難實現這些需求,此時必須對原來的項目代碼進行組件化操做;在進行組件化操做以前,先區分兩個概念:模塊化和組件化android
由上面的介紹知道,組件化針對更細更單一的業務,功能模塊粒度較大,針對某個方面的總體業務,固然業務當中可能使用不少的獨立組件,按照組件化的需求項目的架構進入3.0 網絡
上面已智能、內容兩個模塊爲例,在項目組件化操做後的架構圖,架構從下向上依次爲:由上面的3.0版本架構知道,項目中包含多個功能組件和業務模塊,在開發中要保證組件間不能耦合,業務木塊依賴於組件,但業務模塊之間也不能相互引用,不然違背了組件化的原則;架構
在咱們實際開發中app 構建形式爲application,最終編譯成APK文件,其他所依賴的Module編譯形式爲library,最終已arr形式尋在提供API調用,換句話說只要修改組件的編譯形式便可實現單獨編譯的功能,因此在組件下建立gradle.properties文件用於控制構建形式app
isRunAlone = false
複製代碼
在build.gradle中根據isRunAlone的變量修改構建形式框架
if (isRunAlone.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
複製代碼
if (isRunAlone.toBoolean()) {
applicationId "com.alex.kotlin.content"
}
複製代碼
在組件化單獨編譯和總體編譯時,註冊清單中所須要的內容不一樣,如單獨編譯須要額外的啓動頁,且單獨編譯時也休要配置不一樣的Application,此時在main文件加下建立manifest/AndroidMenifest.xml文件,根據單獨編譯的須要設置內容。ide
sourceSets {
main {
if (isRunAlone.toBoolean()) {
manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
複製代碼
到此編譯配置完成,在須要單獨編譯時只須要修改isRunAlone爲true便可;模塊化
由上面配置的兩個註冊清單文件中可見,在App總體編譯時組件使用的是全局的Application,在單獨編譯時使用的是AutoApplication,你們都知道一個程序中只有一個Application類,那組件中須要初始化的代碼都配置在本身的AutoApplication中,那總體編譯時如何初始化呢?可能有同窗說總體編譯時個組件和模塊是可見的,直接調用AutoApplication類完成初始化,但此種狀況主項目就沒法實現模塊的自由增減,並且當代碼隔離時AutoApplication就不可見了,這裏採用一種配置+反射的方式溫馨化各組件的Application,具體實現以下:工具
abstract class BaseApp : Application(){
/**
* 初始化Module中的Application
*/
abstract fun initModuleApp(application: Application)
}
複製代碼
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()
}
}
複製代碼
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)
}
複製代碼
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()中初始化了內容,此處是爲了在單獨編譯時調用;源碼分析
在項目中由於有時須要打包不一樣需求的APK,因此我將login單獨分離出成組件同一登陸行爲,那麼在特務模塊依賴Login以後便可實現登陸功能,但每一個單獨的業務獨立編譯時會產生多個APK,這些APK都須要獲取登陸狀態及跳轉相應的首界面,那麼在保證程序解耦的狀況下如何實現呢?答案及時使用註冊接口實現;
interface LoginToService {
/** * 實現登陸後的去向 */
fun goToSuccess()
}
複製代碼
class ServiceFactory private constructor() {
companion object {
fun getServiceFactory(): ServiceFactory {
return Inner.serviceFactory
}
}
private object Inner {
val serviceFactory = ServiceFactory()
}
}
複製代碼
var loginToService: LoginToService? = null
get() {
if (field == null) {
field = EmptyLoginService()
}
return field
}
複製代碼
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)
}
}
複製代碼
ServiceFactory.getServiceFactory().serviceIntelligence = AutoIntelligenceService()
複製代碼
override fun loadSuccess(loginBean: LoginEntity) {
ServiceFactory.getServiceFactory().loginToService?.goToSuccess()
}
複製代碼
各組件經過向base組件中的ServiceFactory註冊的方式,對外提供執行的功能,由於ServiceFactory單例調用,因此在其餘組件中經過ServiceFactory獲取註冊的實例後便可執行方法,爲了在減去組件或模塊時防止報錯,在base中一樣提供了服務的空實現;
關於頁面跳轉推薦使用阿里的ARoute框架,詳情見另外一篇文章:Android框架源碼分析——以Arouter爲例談談學習開源框架的最佳姿式
在通常項目中,主app的首界面都來自不一樣的業務模塊組成,最多見的就是使用不一樣組件的Fragment和ViewPager組合,但此時主App須要獲取組件中的Fragment實例,按照組件化的思想不能直接使用,不然主APP和組件、模塊間又會耦合在一塊兒,此處也是採用接口模式處理,過程和數據交互大體相同;
interface ContentService {
/** * 返回實例化的Fragment */
fun newInstanceFragment(): BaseCompatFragment?
}
// 內容模塊實現
class ContentServiceImpl : ContentService {
override fun newInstanceFragment(): BaseCompatFragment? {
return ContentBaseFragment.newInstance() //提供Fragment對象
}
}
複製代碼
ServiceFactory.getServiceFactory().serviceContent = ContentServiceImpl()
複製代碼
mFragments[SECOND] = ServiceFactory.getServiceFactory().serviceContent?.newInstanceFragment()
複製代碼
雖然經歷組件化將代碼解耦,但在開發中若是依賴的組件或模塊中的方法老是可見,萬一在開發中使用了其中的代碼,那程序程序又會耦合在一塊兒,如何能讓組件和模塊中的方法不可見呢?答案就在runtimeOnly依賴,他能夠在開發過程當中隔離代碼,在編譯時代碼可見
runtimeOnly project(':content') runtimeOnly project(':intelligence') 複製代碼
runtimeOnly依賴實現了代碼隔離,但對資源並無效果,使用中仍是可能會直接引用資源,爲了防止這種現象,爲每一個組件的資源加上特有的前綴
resourcePrefix "auto_"
複製代碼
此時該Module下的資源都必須以auto_開頭不然會警告;
因爲項目中使用到了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 //使用
複製代碼
解決上面的全部問題後,項目的組件化基本能夠實現,但具體的劃分粒度和細節,須要自身結合業務和經驗去處理,可能有些須要直接分離組件,也可能小的功能須要放在base組件中共享,並且每一個人針對每一個項目的處理方式也不一樣,只要理解組件化的思想和方式實現最終的需求便可;