不用去糾結組件和模塊語義上的區別,若是模塊間不存在強依賴且模塊間能夠任意組合,咱們就說這些模塊是組件化的。android
實現組件化自己就是一個解耦的過程,同時也在不斷對你的項目代碼進行提煉。對於已有的老項目,實現組件化剛開始是很難受的,可是一旦組件的框架初步完成,對於後期開發效率是會有很大提高的。git
組件間間相互獨立,能夠減小團隊間的溝通成本。github
每個組件的代碼量不會特別巨大,團隊的新人也能快速接手項目。bash
這是本文所主要講述的內容,本篇文章同時適用於新老項目,文中會逐漸帶領你們實現以下目標:多線程
理論篇不會講述實際項目,先從技術上實現上面的三個目標。併發
組件間不存在強依賴從理論上來講其實很簡單,我不引用你任何東西,你也不要引用我任何東西就好了。但在實際項目中,須要清楚明白那些業務模塊應該定義爲組件,另外在已有項目中,拆分代碼也須要大量的工做。hexo
組件間經過接口通訊。爲每個組件定義一個或者多個接口,簡單起見,咱們假定只爲每個組件定義接口(多個接口是相似的)。app
便於理解,仍是要舉實例。假設當前存在兩個組件UserManagement(用戶管理)和OrderCenter(訂單中心),咱們爲組件接口定義的模塊的名爲ComponentInterface。UserManagement和OrderCenter都依賴於ComponentInterface。爲了有個直觀的感覺,仍是放張圖:框架
在ComponentInterface模塊中新建爲組件UserManagement的定義接口:ide
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)。
經過這樣的修改,外部獲取到的組件接口的實現就必定是非空的,也就是不管組件存在與否,都不會影響到項目主體,並且外部也並不須要關心組件是否存在。
理論篇從技術的角度介紹瞭如何實現組件化,不過對於實際項目,咱們使用組件化還會遇到諸多問題,下面將從實踐的角度來幫助你們更快的實現項目組件化。
技術篇中,咱們每次獲取組件接口的實現時都會反射一次,這顯然是不合理的。咱們可使用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的方案,能夠簡化參數的傳遞和解析工做。感興趣的同窗能夠移步:github.com/a3349384/Fr…
最後歡迎關注個人博客:zhoumingyao.cn/