隨着業務的積累,產品的迭代,咱們寫的工程會愈來愈大,也愈來愈臃腫,更加難以維護,那有沒有一種方法,可以使得每一個人專門負責本身的業務模塊,使用的時候把每一個人作的模塊直接拼裝組合起來就行,這樣代碼也更加靈活,相互之間的耦合性也更低,重用性也可以更大。那麼模塊化的概念就來了。html
簡單來講, 模塊化就是將一個程序按照其功能作拆分,分紅相互獨立的模塊,以便於每一個模塊只包含與其功能相關的內容。模塊咱們相對熟悉,好比登陸功能能夠是一個模塊, 搜索功能能夠是一個模塊, 汽車的發送機也但是一個模塊。java
固然從我的的理解上,模塊化只是一種思想,就是大化小,分開治理,在實際項目中如何具體實施,目前有兩種方案,一個是組件化,一個是插件化
在網上找到了一張很形象的圖android
組件化方案就是:由若干獨立的子模塊,組合成一個總體,下降模塊間的耦合,這些子模塊在補足必定的條件下,均可獨立運行。主模塊也不會由於缺乏任意子模塊而沒法運行。組件之間能夠靈活的組建。git
相似於積木,拼裝組合,易維護github
插件化方案就是:一個程序的輔助或者擴展功能模塊,對程序來講插件無關緊要,但它能給予程序必定的額外功能。web
打個比方,就像如今的應用程序,更多的須要依賴一些第三方庫,好比地圖sdk、分享sdK、支付sdk等等,致使安裝包變得愈來愈大,單是依賴這些sdk,安裝包可能就會額外的增長10-20M的大小;api
當須要新增功能時,不得不從新更新整個安裝包。再熟讀一下上面的定義,就知道它的用途和做用了,那就是有些附加功能,須要時,可靈活的添加,動態的加載。插件化主要是解決的是減小應用程序大小、免安裝擴展功能,當須要使用到相應的功能時再去加載相應的模塊
安全
區別根據他們使用的用途,就很好理解了:組件化在運行時不具有動態添加或修改組件的功能,可是插件化是能夠的
markdown
提及組件化的實踐方案,只有一首小詩形容, 走遍了各類論壇,看遍了地老天荒,原來最適合的方案啊,就在身旁app
總而言之一句話:各類方案都有,也不缺少不少寫的不錯的,可是秉持着商用開發爲主,接下來介紹一個最合適的,那就是阿里巴巴出的一套ARouter,它簡單易用、它支持多模塊項目、它定製性較強、它支持攔截邏輯等諸多優勢,接下來會寫阿里這套框架的使用方便往後開發。若是有興趣的小夥伴,能夠等我下一篇博客,介紹它的實踐原理。
就是一個電商,有3個組件,一個是首頁,一個是購物車,一個是我的中心,3個獨立的模塊
由於每個模塊都是要可以單獨調試的,因此咱們先定義每一個模塊的開關,設置這個模塊是否要進行單獨調試運行
buildscript { ext.kotlin_version = '1.3.31' ext { isRunHome = true // true是Home模塊可單獨運行 isRunPersonalcenter = true isRunShopingcar = true } 複製代碼
if(isRunHome.toBoolean()){ // 1.根據以前設定的isRunHome,判斷是否須要獨立運行 apply plugin: 'com.android.application' }else { apply plugin: 'com.android.library' } android { android { compileSdkVersion 29 buildToolsVersion "29.0.0" defaultConfig { if(isRunHome.toBoolean()){ // 2.這裏也設置一下,可運行的話,添加applicationId applicationId "com.bj.home" } 複製代碼
dependencies { if(!isRunHome.toBoolean()){ // 1.若是要獨立運行,那麼主工程不加載它 implementation project(path: ':home') } implementation project(path: ':personalcenter') implementation project(path: ':shoppingcar') 複製代碼
編譯一下就是這樣
4.固然還差一步,設置AndroidManifest.xml文件,由於通常來講,一個APP只有一個啓動頁,在組件單獨調試時也須要一個啓動頁,因此咱們須要設置兩個文件。就這樣
AndroidManifest文件和ApplicationId 同樣都是能夠在 build.gradle 文件中進行配置的,因此咱們一樣經過動態配置組件工程類型時定義的 boolean變量的值來動態修改。須要咱們修改子模塊(如home)的build.gradle文件。
android { ... sourceSets { main { // 1.單獨調試與集成調試時使用不一樣的 AndroidManifest.xml 文件 // 咱們還能夠根據不一樣工程配置不一樣的 Java 源代碼、不一樣的 resource 資源文件等的 if(isRunHome.toBoolean()) { manifest.srcFile 'src/main/manifest/AndroidManifest.xml' } else{ manifest.srcFile 'src/main/AndroidManifest.xml' } } } } 複製代碼
大功告成,使用時只須要修改根目錄build.gradle文件中的那3個變量,就能夠一鍵開啓該模塊的單獨運行模式了,親測有效,好了,咱們已經完成了,模塊獨立化了,子模塊可單獨運行了,可是,怎麼通信,傳遞數據呀?組件與組件之間都是不能夠直接使用類的相互引用來進行數據傳遞的!
解決辦法就是集成集成阿里的路由框架ARouter,一個用於幫助 Android App 進行組件化改造的框架 —— 支持模塊間的路由、通訊、解耦 來咱們集成一下
1.在各個模塊中添加了對 ARouter 的依賴,固然本身新建一個base模塊,依賴添加到base裏,其餘模塊引用它也能夠。
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [moduleName: project.getName()]
}
}
}
複製代碼
dependencies { compile 'com.alibaba:arouter-api:1.2.1.1' annotationProcessor 'com.alibaba:arouter-compiler:1.1.2.1' ... } 複製代碼
好了,配置完成
咱們在自定義的MyApplication中,初始化它
@Override public void onCreate() { super.onCreate(); // 這兩行必須寫在init以前,不然這些配置在init過程當中將無效 if(isDebug()) { // 打印日誌 ARouter.openLog(); // 開啓調試模式(若是在InstantRun模式下運行,必須開啓調試模式!線上版本須要關閉,不然有安全風險) ARouter.openDebug(); } // 初始化ARouter ARouter.init(this); } private boolean isDebug() { return BuildConfig.DEBUG; } 複製代碼
1.在目標Activity添加註解 Route (home : HomeAty)
/** * 首頁模塊 * * 其中 path 是跳轉的路徑,這裏的路徑須要注意的是至少須要有兩級,/xx/xx * */ @Route(path = "/home/HomeAty") public class HomeAty extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.aty_home); } } 複製代碼
2 頁面跳轉(app : MainActivity)
@Override public void onClick(View view) { switch (view.getId()){ // 跳轉Activity頁面 case R.id.btn_go_home: ARouter.getInstance().build("/home/HomeAty").navigation(); break; } } 複製代碼
@Override public void onClick(View view) { switch (view.getId()){ ... // 跳轉Activity頁面, 而且返回數據 case R.id.btn_go_aty_forresult: ARouter.getInstance().build("/home/HomeResultAty").navigation(this, 897); break; } } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if(requestCode == 897 && resultCode == 999){ String msg = data.getStringExtra("msg"); tv_msg.setText(msg); } } 複製代碼
@Route(path = "/home/HomeResultAty") public class HomeResultAty extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.aty_home_result); findViewById(R.id.btn_goback).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent in = new Intent(); in.putExtra("msg", "從home模塊返回的數據"); setResult(999, in); finish(); } }); } } 複製代碼
Fragment mFragment = (Fragment) ARouter.getInstance().build("/home/HomeFragment").navigation(); getSupportFragmentManager().beginTransaction().replace(R.id.fl, mFragment).commit(); 複製代碼
2.固然fragment也要加註解(home : HomeFrag)
@Route(path = "/home/HomeFragment") public class HomeFrag extends Fragment {...} 複製代碼
// 攜參數跳轉 case R.id.btn_go_home_byArgs: ARouter.getInstance().build("/home/arg") .withString("msg", "5") .withDouble("msg2", 6.0) .navigation(); break; 複製代碼
2.目標Activity(home: HomeByArgAty)
@Route(path = "/home/arg") public class HomeByArgAty extends Activity { @Autowired(name = "msg") String arg1; @Autowired String arg2; private TextView tv_msg; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.aty_home_arg); // 若是使用Autowired註解,須要加入底下的代碼 // 固然也能夠用 getIntent().getStringExtra("") ARouter.getInstance().inject(this); tv_msg = findViewById(R.id.tv_msg); tv_msg.setText("從主工程傳遞過來的參數:"+arg1); } } 複製代碼
---------------------------------進階用法------------------------------
ARouter也添加了攔截器模式,攔截器有不少用處,好比路由到目標頁面時,檢查用戶是否登陸,檢查用戶權限是否知足,若是不知足,則路由到相應的登陸界面或者相應的路由界面。ARouter的攔截器比較奇葩,只須要實現IInterceptor接口,並使用@Interceptor註解便可,並不須要註冊就能使用。固然這也有了它的壞處,就是每一次路由以後,都會通過攔截器進行攔截,顯然這樣程序的運行效率就會下降。Interceptor能夠定義多個,好比定義登陸檢查攔截器,權限檢查攔截器等等,攔截器的優先級使用priority定義,優先級越大,越先執行。攔截器內部使用callback.onContiune()/callback.onInterrupt(),前者表示攔截器任務完成,繼續路由;後者表示終止路由。例子:
1.實現IInterceptor接口,自定義攔截器,檢測全部跳轉中,只要uri爲空就攔截,也能夠在這請求中再加內容
@Interceptor(priority = 4) public class LoginInterceptor implements IInterceptor { @Override public void process(Postcard postcard, InterceptorCallback callback) { String uri = postcard.getExtras().getString("uri"); if(TextUtils.isEmpty(uri)){ Log.i("lybj", "uri爲空,中斷路由"); callback.onInterrupt(null); }else { Log.i("lybj", "攔截器執行,uri不爲空,繼續執行吧"); postcard.withString("msg", "能夠隨意加內容"); callback.onContinue(postcard); } } @Override public void init(Context context) { } } 複製代碼
// 攔截器測試 case R.id.btn_test_interceptor: ARouter.getInstance().build("/home/web") .withString("uri", "file:///android_asset/schame-test.html") .navigation(); break; 複製代碼
3.目標界面
@Route(path = "/home/web") public class WebAty extends Activity { private WebView wv_web; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.aty_web); Log.i("lybj", getIntent().getStringExtra("msg")); String uri = getIntent().getStringExtra("uri"); wv_web = findViewById(R.id.wv_web); wv_web.loadUrl(uri); } } 複製代碼
這個很坑,翻了翻文檔,說是要在所引用的全部的model的build.gradle裏面都要加上下面的代碼
defaultConfig{
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [moduleName: project.getName()]
}
}
}
複製代碼
或者
defaultConfig{
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
複製代碼
AROUTER_MODULE_NAME和moduleName根據不一樣的版本,選擇不一樣的名字,上面的代碼要保證全部模塊都要添加,爲的是作區分
做者就作了個蠢事,兩個model的layout名字同樣,主工程加載的時候,老是出問題,因此儘量的保證每一個model的資源名加前綴。