Android組件化探索與實踐

什麼是組件化

不用去糾結組件和模塊語義上的區別,若是模塊間不存在強依賴且模塊間能夠任意組合,咱們就說這些模塊是組件化的。android

組件化的好處

  1. 實現組件化自己就是一個解耦的過程,同時也在不斷對你的項目代碼進行提煉。對於已有的老項目,實現組件化剛開始是很難受的,可是一旦組件的框架初步完成,對於後期開發效率是會有很大提高的。
  2. 組件間間相互獨立,能夠減小團隊間的溝通成本。
  3. 每個組件的代碼量不會特別巨大,團隊的新人也能快速接手項目。

如何實現組件化

這是本文所主要講述的內容,本篇文章同時適用於新老項目,文中會逐漸帶領你們實現以下目標:git

  • 各個組件不存在強依賴
  • 組件間支持通訊
  • 缺乏某些組件不能對項目主體產生破壞性影響

組件化-理論篇

理論篇不會講述實際項目,先從技術上實現上面的三個目標。github

組件間不存在強依賴

組件間不存在強依賴從理論上來講其實很簡單,我不引用你任何東西,你也不要引用我任何東西就好了。但在實際項目中,須要清楚明白那些業務模塊應該定義爲組件,另外在已有項目中,拆分代碼也須要大量的工做。多線程

組件間如何通訊

組件間經過接口通訊。爲每個組件定義一個或者多個接口,簡單起見,咱們假定只爲每個組件定義接口(多個接口是相似的)。併發

便於理解,仍是要舉實例。假設當前存在兩個組件UserManagement(用戶管理)和OrderCenter(訂單中心),咱們爲組件接口定義的模塊的名爲ComponentInterface。UserManagement和OrderCenter都依賴於ComponentInterface。爲了有個直觀的感覺,仍是放張圖:hexo

工程目錄1

在ComponentInterface模塊中新建爲組件UserManagement的定義接口:app

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的組件實現呢?這個問題能夠經過反射來解決,只是須要知足組件的接口和組件接口的實現的路徑和名稱知足必定的約束條件。ide

咱們定義組件接口和其實現的路徑和名稱的約束條件以下:組件化

  1. 組件的接口和組件接口的實現必須定義在同一個包名下。
  2. 組件接口的實現的類名能夠經過組件的接口的類名推導出來。好比每個接口的實現的類名都是在該接口的名稱後面接上「Impl」。

那麼如今,咱們的工程目錄大概就像這個樣子:

工程目錄2

接下來,在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

漸進式組件化

老項目要徹底組件化是會有較長一個週期要走,一般也太可能專門拿出幾個月讓你來實現組件化,因此要實現漸進式組件化,才能真正將組件化應用到實際項目中。

實際項目中,因爲自己開發任務就很重,因此不要太指望可以有足夠的時間讓你將某個模塊徹底組件化。我這邊的作法是:

  1. 給App主模塊也定義一個組件接口
  2. 平常開發中能夠慢慢將某個模塊組件化,沒有徹底組件化也不要緊,能夠在App組件接口中爲那些還未徹底組件化的功能定義一系列接口
  3. 這樣,耦合在App模塊中的還沒有徹底組件化的代碼就能夠在該組件中進行調用了
  4. 後期有時間完整該組件的組件化的工做後把App組件接口中相關方法刪掉就能夠了

這樣的組件化開發方式幾乎不會對平常開發工做形成太大的影響,隨着平常開發工做的進行,項目組件化的程度也在慢慢提高。

組件如何單獨運行

組件單獨運行也是咱們開發人員比較強烈的一個需求。主要存在如下方面的緣由:

  1. 單獨運行組件須要的編譯、打包、安裝時間會大大下降,能夠節約不少等待時間
  2. 組件可以單獨運行也表示咱們不用等待其餘組件完成才能開始測試。實際項目協做中,咱們能夠預先定義好組件間的通訊接口,這樣經過組件接口實現注入,就能夠開始組件的測試,徹底不須要等待其依賴的組件完成後才能開始測試。

不少文章都在使用將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上主流的頁面導航方式有三種:

  1. 不一樣的頁面對應不一樣Activity類型
  2. 在Activity中使用Fragment導航,在Activity中同時
  3. 使用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/F...

歡迎關注個人博客:http://zhoumingyao.cn

相關文章
相關標籤/搜索