Android元件化方案實踐與思考
Demo地址:https://github.com/751496032/ComponentDemo
效果圖:
背景
Android從誕生到現在,不知不覺的走過十多個年頭了,也產生了很多App,隨著專案的推進不斷的迭代,而App也從最初的單一功能演變成多工功能,各種業務的錯綜複雜,開發人員也不斷的增加,如果架構不做調整優化,會給開發帶來很大的困難:
- 各種業務程式碼耦合性及高,程式碼臃腫會越來越高,不利於團隊間協同開發,維護成本也高;
- 降低開發效率,工程的編譯執行時間及長,在單一工程下,每修改一小處都要執行整個專案,導致非常耗時。
- ……
基於程式碼耦合性問題,App的設計架構也不斷的演變,從最初的MVC,到現在主流的MVP、MVVM,這些模式也確實起到程式碼解耦的效果,但還是很大侷限性的;各業務間的耦合、執行效率問題都還是存在的;於是元件化思想就誕生了,元件化有如下優勢:
- 業務間程式碼互不干擾,解耦性好,程式碼複用性也高;
- 各個元件能單獨生成apk,可以單獨除錯,降低了編譯執行時長。
元件化思想
元件化就是把單一工程的app分成多個Module,每個Module就相當於一個元件,而這些元件是不需要相互依賴的,可根據開發需求,自由將各個元件進行Application
、Library
模式切換進行除錯開發。
上面是元件化基礎結構圖,從上向下分為三部分,分別是app空殼、功能元件(業務元件)、基礎元件;
- App空殼只有一個元件就是App元件,需要依賴於各個業務元件,最終上線的就是App統籌所有的業務元件打包生成的;
- 功能元件又稱之業務元件,各個元件間並沒有依賴關係,除了Login元件外,把Login元件單獨分開的原因是,我認為基本上所有的業務元件都需要登入行為才可以操作,不排除少數業務元件是不需要的,於是乾脆把所有的業務元件全部依賴於Login元件,在這裡Login元件其實是一個共享元件;
- 基礎元件,這個很好理解就是我們封裝的基礎庫,比如網路、路由、推送、圖片等等
元件化需解決的問題
- 模式切換,如何使每個Module在
Application
、Library
間自由切換; - 依賴關係,如何處理每個Module間、工具類庫的依賴關係,這個沒有唯一模型,可根據專案需求也定,但一點可以肯定的是,同一層次的元件模組不能存在相互依賴的關係,不然就失去了元件化的意義了;
- 資源衝突,如何處理App空殼中所依賴的Module間資源重名衝突;
- 元件通訊,如何處理業務元件間通訊問題。
- ……
上面這幾個問題是元件化實現過程中的主要問題,解決了上述問題,元件化方案實施基本沒有多大的問題,其他的一些問題可根據自身需求而定。
實現步驟
1、在專案根目錄下的gradle.properties
配置全域性引數,方便管理各個Module的常用全域性引數,比如版本號、常量等等
isModuleRun=false
compile_sdk_version=26
min_sdk_version=17
target_sdk_version=26
version_code=1
version_name=1.0
constraint_layout_version=1.1.3
support_version=26.1.0
leakcanary_version=1.6.1
arouter_version=1.3.1
arouter_annotation_version=1.2.0
eventbus_version=3.1.1
……
2、模式切換
在專案根目錄下的gradle.properties
設定一個boolean的變數isModuleRun
,這個變數的作用就是控制業務元件Application
、Library
模式切換,當isModuleRun=true
,元件處於Application
可單獨編譯執行,反之則為Library
是一個依賴庫,在模式切換過程同時還需處理每個Module的AndroidMainfest
檔案的衝突,如下:
在每個業務Module下的build.gradle
下編寫切換判斷的程式碼處理模式切換
因isModuleRun
的值不同,Module的AndroidMainfest
檔案內容也會有所不同,當Module處於Application
下,此時是獨立應用,需要配置applicationId
,以及應用的啟動頁設定,而在Library
下則不需要這些,因為我們針對不同模式下引用不同AndroidMainfest
。
首頁在Module的main
目錄下建立一個module_run
目錄單獨存放Application
所需的AndroidMainfest
檔案,接著在Module下的build.gradle
引入:
Lib下的AndroidMainfest檔案內容
Application下的AndroidMainfest檔案內容
到這裡基本解決了模式切換的問題
3、資源衝突
從App空殼到基礎元件,中間依賴很多其他元件,難免會有資源衝突的問題,在這情況下,建議在定義一個資源命名規範,大家統一遵守這個規範,能很好的避免資源衝突的問題,比如可以以Module名稱作為字首進行規範:
4、元件通訊
元件間通訊我們使用開源元件通訊框架,比如阿里的ARouter,能很好的處理各元件間的跳轉,並且同層次的元件間不會任何的依賴關係,實現瞭解耦的效果。使用如下:
在各元件下的build.gradle
新增依賴和配置
android {
defaultConfig {
...
//注意:這裡每個業務元件都需要配置
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
}
dependencies {
//這裡在Base基礎元件新增依賴即可,其他元件無需新增
api "com.alibaba:arouter-api:${arouter_version}"
//註解依賴需要在各個元件中新增依賴
annotationProcessor "com.alibaba:arouter-compiler:${arouter_annotation_version}"
...
}
在BaseAppliction下初始化
private void initARouter() {
if (BuildConfig.DEBUG) { // 這兩行必須寫在init之前,否則這些配置在init過程中將無效
ARouter.openLog(); // 列印日誌
ARouter.openDebug(); // 開啟除錯模式(如果在InstantRun模式下執行,
必須開啟除錯模式!線上版本需要關閉,否則有安全風險)
}
ARouter.init(this); // 儘可能早,推薦在Application中初始化
}
簡單的使用,比如獲取Fragment例項、啟動Activity、攔截跳轉頁面
/**
* 路由管理類
* 命名規則:/模組名/特殊描述/目標頁面名稱(特殊描述可選)
* 需要登入的頁面操作,帶login_after欄位
*/
public final class ARouterManager {
public static final String LOGIN_AFTER="login_after";
public static final String HomeFragment = "/home/HomeFragment";
public static final String CartFragment="/cart/CartFragment";
public static final String MeFragment="/me/CartFragment";
public static final String LoginActivity="/login/LoginActivity";
public static final String GoodsDetailActivity="/home/GoodsDetailActivity";
public static final String ShareActivity="/login/login_after/ShareActivity";
}
-------------------------------------------------------------------------------------
//Fragment路由路徑定義
@Route(path = ARouterManager.HomeFragment) //定義路由路徑
public class HomeFragment extends BaseFragment implements View.OnClickListener {
}
// Fragment例項獲取
Fragment fragmet = (Fragment) ARouter.getInstance().build(ARouterManager.HomeFragment).navigation()
-------------------------------------------------------------------------------------
//Activty
@Route(path = ARouterManager.GoodsDetailActivity)
@SuppressWarnings("all")
public class GoodsDetailActivity extends BaseActivity {
}
//啟動
ARouter.getInstance().build(ARouterManager.GoodsDetailActivity).navigation();
頁面跳轉攔截器,比如某些頁面操作必須登入,我們可以先獲取當前是否有登入,然後根據頁面路由路徑進行判斷攔截頁面跳轉
/**
* 頁面跳轉攔截器
* 應用場景:如某些頁面需要登入才可操作,可通過攔截器來統一處理跳轉頁面
*/
@Interceptor(priority = 7)
public class ARouterInterceptor implements IInterceptor {
Context mContext;
/**
* The operation of this interceptor.
*
* @param postcard meta
* @param callback cb
*/
@Override
public void process(final Postcard postcard, final InterceptorCallback callback) {
boolean isLogin = SpUtils.getBoolean(mContext, SpUtils.LOGIN_KEY);
String path = postcard.getPath();
if (!isLogin&&path.contains(ARouterManager.LOGIN_AFTER)){
//未登入
ARouter.getInstance().build(ARouterManager.LoginActivity).navigation();
callback.onInterrupt(null);
}else {
callback.onContinue(postcard);
}
}
/**
* Do your init work in this method, it well be call when processor has been load.
* 在路由初始化時會載入攔截器
* @param context ctx
*/
@Override
public void init(Context context) {
mContext = context;
Log.e("testService", ARouterInterceptor.class.getName() + " has init.");
}
}
上面是ARouter的一些簡單用法,詳細可以檢視官方文件。
總結
元件化並沒有一個放之四海皆準的通用方案,在我認為,只要實現各個業務模組、基礎模組間解耦就是一個好方案,最起碼相對之前單一工程來說,已經改善了很多了,效率肯定會有提升,只有根據自己專案實際情況,進行不斷改造找到適合自己專案的設計方案。如果在現有的專案中進行元件化拆分,建議先把基礎元件庫進行剝離,緊接著再抽離一些共享資料元件(比如登入、分享元件等等),最後才對核心業務元件下刀拆分,在拆分的過程中千萬別指望一口氣全部拆分完,否則一不小心就會出現專案滿堂紅的情況,要做到邊拆分邊備份,避免程式碼丟失的危險。