什麼是組件化 不用去糾結組件和模塊語義上的區別,如果模塊間不存在強依賴且模塊間可以任意組合,我們就說這些模塊是組件化的。 組件化的好處 1. 實現組件化本身就是一個解耦的過程,同時也在不斷對你的項目代碼進行提煉。對於已有的老項目,實現組件化剛開始是很難受的,但是一旦組件的框架初步完成,對於後期開發效 ...
什麼是組件化
不用去糾結組件和模塊語義上的區別,如果模塊間不存在強依賴且模塊間可以任意組合,我們就說這些模塊是組件化的。
組件化的好處
實現組件化本身就是一個解耦的過程,同時也在不斷對你的項目代碼進行提煉。對於已有的老項目,實現組件化剛開始是很難受的,但是一旦組件的框架初步完成,對於後期開發效率是會有很大提升的。
組件間間相互獨立,可以減少團隊間的溝通成本。
每一個組件的代碼量不會特別巨大,團隊的新人也能快速接手項目。
如何實現組件化
這是本文所主要講述的內容,本篇文章同時適用於新老項目,文中會逐漸帶領大家實現如下目標:
- 各個組件不存在強依賴
- 組件間支持通信
- 缺少某些組件不能對項目主體產生破壞性影響
組件化-理論篇
理論篇不會講述實際項目,先從技術上實現上面的三個目標。
組件間不存在強依賴
組件間不存在強依賴從理論上來說其實很簡單,我不引用你任何東西,你也不要引用我任何東西就行了。但在實際項目中,需要清楚明白那些業務模塊應該定義為組件,另外在已有項目中,拆分代碼也需要大量的工作。
組件間如何通信
組件間通過介面通信。為每一個組件定義一個或者多個介面,簡單起見,我們假定只為每一個組件定義介面(多個介面是類似的)。
便於理解,還是要舉實例。假設當前存在兩個組件UserManagement(用戶管理)和OrderCenter(訂單中心),我們為組件介面定義的模塊的名為ComponentInterface。UserManagement和OrderCenter都依賴於ComponentInterface。為了有個直觀的感受,還是放張圖:
在ComponentInterface模塊中新建為組件UserManagement的定義介面:
public interface UserManagementInterface
{
//獲取用戶ID
String getUserId();
}
UserManagement實現ComponentBInterface
:
public class UserManagementInterfaceImpl implements UserManagementInterface
{
@Override
public String getUserId()
{
return "UID_XXX";
}
}
現在假定OrderCenter組件需要從UserManagement獲取用戶ID以便載入該用戶的訂單列表。那麼問題來了,OrderCenter怎麼才能調用到UserManagement的組件實現呢?這個問題可以通過反射來解決,只是需要滿足組件的介面和組件介面的實現的路徑和名稱滿足一定的約束條件。
我們定義組件介面和其實現的路徑和名稱的約束條件如下:
組件的介面和組件介面的實現必須定義在同一個包名下。
組件介面的實現的類名可以通過組件的介面的類名推導出來。比如每一個介面的實現的類名都是在該介面的名稱後面接上“Impl”。
那麼現在,我們的工程目錄大概就像這個樣子:
接下來,在OrderCenter組件中就可以通過反射獲取到UserManagement組件介面的實現了,我們定義一個ComponentManager
類:
public class ComponentManager
{
public static <T> T of(Class<T> tInterface)
{
String interfaceName = tInterface.getCanonicalName();
String implName = interfaceName + "Impl";
try
{
T impl = (T) Class.forName(implName).newInstance();
return impl;
}
catch (Exception ex)
{
ex.printStackTrace();
return null;
}
}
}
然後在OrderCenter就可以通過ComponentManager
來獲取UserManagement的組件介面實現了:
String userId = ComponentManager.of(UserManagementInterface.class).getUserId();
至此,組件間通信的問題就算解決了,而且組件之間還是不存在強依賴。
缺少某些組件不能對項目主體產生破壞性影響
假設打包後的項目不存在UserManagement
組件,上面獲取userId的代碼會有什麼問題?ComponentManager.of(UserManagementInterface.class)
這裡的返回必然為null,我們的代碼就會產生空指針異常。
那麼如何解決這個問題呢?像下麵這樣嗎:
UserManagementInterface userManagementInterface = ComponentManager.of(UserManagementInterface.class);
if (userManagementInterface != null)
{
userId = userManagementInterface.getUserId();
}
從程式運行的角度來看,上面的代碼沒有什麼問題。但從碼農的角度來看,上面代碼寫起來必然不是很舒爽,整個項目中會充斥著這樣的非空判斷。
我們期望,在某個組件不存在時,通過ComponentManager.of
獲取的組件介面實現可以具備一個預設值。在Java中,我們可以通過動態代理在運行時動態生成一個介面的實現。
我們修改ComponentManager
的代碼:
public class ComponentManager
{
public synchronized static <T> T of(Class<T> tInterface)
{
String interfaceName = tInterface.getCanonicalName();
String implName = interfaceName + "Impl";
try
{
T impl = (T) Class.forName(implName).newInstance();
return impl;
}
catch (Exception ex)
{
ex.printStackTrace();
ClassLoader classLoader = ComponentManager2.class.getClassLoader();
T fakeImpl = (T) Proxy.newProxyInstance(classLoader, new Class[]{tInterface}, new DefaultInvocationHandler());
return fakeImpl;
}
}
private static class DefaultInvocationHandler implements InvocationHandler
{
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{
Class<?> returnClass = method.getReturnType();
if (!returnClass.isPrimitive())
{
return null;
}
String returnClassName = returnClass.getCanonicalName();
if (returnClassName.contentEquals(boolean.class.getCanonicalName()))
{
return false;
}
if (returnClassName.contentEquals(byte.class.getCanonicalName()))
{
return (byte)0;
}
if (returnClassName.contentEquals(char.class.getCanonicalName()))
{
return (char)0;
}
if (returnClassName.contentEquals(short.class.getCanonicalName()))
{
return (short)0;
}
if (returnClassName.contentEquals(int.class.getCanonicalName()))
{
return (int)0;
}
if (returnClassName.contentEquals(long.class.getCanonicalName()))
{
return (long)0;
}
if (returnClassName.contentEquals(float.class.getCanonicalName()))
{
return (float)0;
}
return (double)0;
}
}
}
我們判斷了介面方法的返回值,如果返回值為引用類型則直接返回null,否則返回值類型的預設值(boolean返回false,其他返回0)。
通過這樣的修改,外部獲取到的組件介面的實現就一定是非空的,也就是無論組件存在與否,都不會影響到項目主體,而且外部也並不需要關心組件是否存在。
組件化-實踐篇
理論篇從技術的角度介紹瞭如何實現組件化,不過對於實際項目,我們使用組件化還會遇到諸多問題,下麵將從實踐的角度來幫助大家更快的實現項目組件化。
ComponentManager優化
技術篇中,我們每次獲取組件介面的實現時都會反射一次,這顯然是不合理的。我們可以使用Map將組件和組件介面的實現的關係保存下來。
另外還需要考慮多線程併發的問題。
在實際項目中,有時為了方便測試,會期望能夠主動為某個組件介面指定一個假的實現,我們可以增加一個註入組件介面實現的方法。
組件化的項目結構
將項目中的基礎類庫提取出來是組件化應該要做的第一件事情。基礎類庫不應摻雜過多的業務邏輯,基礎類庫要考慮不僅能夠應用與當前產品,也可以應用於其他產品。
每一個組件化工程都應該存在至少一個以上的Common庫,Common庫可以依賴下麵的基礎庫。Common庫中可以放置一些通用的資源(如返回按鈕圖標、全局的字體大小、全局的字體樣式等)以及對一些業務邏輯的封裝(如BaseActivity、HttpClient)
最上面就是組件層了,組件可以依賴Common庫,也可以依賴基礎庫。最後將各個組件組合起來,就一個完整的App。
組件的代碼如何隔離
由於組件之間是不能相互直接依賴,所以組件間也不存在代碼隔離的問題。問題主要出現在App殼上,App殼依賴了所有的組件,如果採用implementation
依賴方式,在App殼中還是能夠訪問組件中的代碼的,我們可以採用runtimeOnly
這種依賴方式。
組件的資源如何隔離
由於當前沒有更好的方式對各個組件的資源進行隔離(runtimeOnly
也不能隔離),所以我們通過命名的約定來避免某個組件引用不屬於本組件的資源。
組件中的資源,如字元串、圖標、菜單等的名稱應該以組件的名稱開頭,如:
usermanagement_login
ordercenter_delete
漸進式組件化
老項目要完全組件化是會有較長一個周期要走,通常也太可能專門拿出幾個月讓你來實現組件化,所以要實現漸進式組件化,才能真正將組件化應用到實際項目中。
實際項目中,由於本身開發任務就很重,所以不要太期望能夠有足夠的時間讓你將某個模塊完全組件化。我這邊的做法是:
給App主模塊也定義一個組件介面
日常開發中可以慢慢將某個模塊組件化,沒有完全組件化也沒關係,可以在App組件介面中為那些還未完全組件化的功能定義一系列介面
這樣,耦合在App模塊中的尚未完全組件化的代碼就可以在該組件中進行調用了
後期有時間完整該組件的組件化的工作後把App組件介面中相關方法刪掉就可以了
這樣的組件化開發方式幾乎不會對日常開發工作造成太大的影響,隨著日常開發工作的進行,項目組件化的程度也在慢慢提升。
組件如何單獨運行
組件單獨運行也是我們開發人員比較強烈的一個需求。主要存在以下方面的原因:
單獨運行組件需要的編譯、打包、安裝時間會大大降低,可以節約很多等待時間
組件能夠單獨運行也表示我們不用等待其他組件完成才能開始測試。實際項目協作中,我們可以預先定義好組件間的通信介面,這樣通過組件介面實現註入,就可以開始組件的測試,完全不需要等待其依賴的組件完成後才能開始測試。
很多文章都在使用將plugin由com.android.library
修改為com.android.application
,讓組件由一個庫變成一個應用程式使得組件能夠單獨運行。這確實是一個辦法,不過對於大部分組件,只修改plugin的類型是完全不夠的。很多組件都需要一些特定的參數才能運行起來,比如訂單列表這個功能肯定是需要用戶ID才能展示出來的。所以我們還是要想辦法如何在組件獨立運行時能夠給組件傳遞參數。
我採用了一種略微不同的方法來運行組件。
我創建了一個Application類型的Module:ComponentTest
來運行組件。在build.gradle中為每一個組件創建一個productFlavor
,示例如下:
productFlavors {
userManager {
applicationIdSuffix ".userManager"
manifestPlaceholders = [appName : "用戶管理"]
}
}
<manifest>
<application
android:label="${appName}">
</application>
</manifest>
在完成這樣的配置之後,每一個組件都具備自己獨特的applicationId,也就是手機上可以同時安裝不同的組件應用程式。
然後通過每個productFlavor
特有的依賴方式將組件實現依賴進來,例如:
userManagerRuntimeOnly project(':userManager')
然後我們就可以在src
目錄創建一個和productFlavor
同名的目錄。在這個目錄下麵可以書寫每個組件自己的測試代碼。當然我們還可以在src/main
下麵書寫一些各個組件都可能使用到的通用代碼,src/main
的內容在其他productFlavor
目錄下是可以訪問的。
在實際項目中,我會給每個組件程式寫一個MainActivity,MainActivity裡面很簡單,就是一排按鈕,每一個按鈕對應著組件介面中的一個方法。這樣開發時很方便測試,開發完成時至少也能夠保證組件基本可用,不太會出現別人一調用你的組件就出錯的情況。
最後,運行某個組件時,需要在AS的Build Variants
中選擇該組件定義的productFlavor
。
頁面跳轉
可以為每一個頁面跳轉定義一個介面方法:
public interface UserManagementInterface
{
//跳轉到用戶信息頁面
String startToUserInfoPage(Context context);
}
然後在startToUserInfoPage
的實現中實現具體的跳轉邏輯。
現在android上主流的頁面導航方式有三種:
不同的頁面對應不同Activity類型
在Activity中使用Fragment導航,在Activity中同時
使用Activity導航,和第一種不同的是Activity只充當Fragment的容器
針對第一種導航方式,在直接使用Intent跳轉就可以,當然使用當前流行的ARouter也行。
針對第二種導航方式,把把FragmentManager放到Common中可能是比較好的辦法。如果有更好的辦法,感謝分享。
我個人比較喜歡第三種導航方式,在項目中也是用的這種導航方式。第三種導航方式同時具備第一種和第二種導航方式的優點,當然它也有比較大的缺點。金無足赤,人無完人,選擇合適的就好。
首先創建一個Activity用做Fragment的容器,比如就叫TheActivity。(命名規範中肯定不推薦用The,但是實際上項目中就這麼個Activity,用The也不會造成什麼理解困難)
TheActivity的啟動參數至少要包含要包含的Fragment的名稱(有了名稱就可以通過反射創建Fragment),還要包含Fragment自身需要的參數。
核心代碼很簡單就像下麵這樣:
Fragment fragment = createFragment();//使用反射創建Fragment
getSupportFragmentManager().beginTransaction()
.replace(fragmentContainerId, fragment)
.commit();
有些東西核心思想很簡單,但是實際項目中使用會暴漏很多問題。
比如需要在Activity中解析Intent參數,有多少個跳轉你幾乎就要寫多少個解析方法,然後在Fragment中還要再解析一次。
人天性就不會喜歡做這種重覆又毫無營養的事情,我抽空做了一個基於註解和AnnotationProcessor的方案,可以簡化參數的傳遞和解析工作。感興趣的同學可以移步:https://github.com/a3349384/FragmentLauncher
最後歡迎關註我的博客:http://zhoumingyao.cn/