Android元件化探索與實踐

程式碼超人發表於2019-03-02

什麼是元件化

不用去糾結元件和模組語義上的區別,如果模組間不存在強依賴且模組間可以任意組合,我們就說這些模組是元件化的。

元件化的好處

  1. 實現元件化本身就是一個解耦的過程,同時也在不斷對你的專案程式碼進行提煉。對於已有的老專案,實現元件化剛開始是很難受的,但是一旦元件的框架初步完成,對於後期開發效率是會有很大提升的。

  2. 元件間間相互獨立,可以減少團隊間的溝通成本。

  3. 每一個元件的程式碼量不會特別巨大,團隊的新人也能快速接手專案。

如何實現元件化

這是本文所主要講述的內容,本篇文章同時適用於新老專案,文中會逐漸帶領大家實現如下目標:

  • 各個元件不存在強依賴
  • 元件間支援通訊
  • 缺少某些元件不能對專案主體產生破壞性影響

元件化-理論篇

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

元件間不存在強依賴

元件間不存在強依賴從理論上來說其實很簡單,我不引用你任何東西,你也不要引用我任何東西就行了。但在實際專案中,需要清楚明白那些業務模組應該定義為元件,另外在已有專案中,拆分程式碼也需要大量的工作。

元件間如何通訊

元件間通過介面通訊。為每一個元件定義一個或者多個介面,簡單起見,我們假定只為每一個元件定義介面(多個介面是類似的)。

便於理解,還是要舉例項。假設當前存在兩個元件UserManagement(使用者管理)和OrderCenter(訂單中心),我們為元件介面定義的模組的名為ComponentInterface。UserManagement和OrderCenter都依賴於ComponentInterface。為了有個直觀的感受,還是放張圖:

工程目錄1

在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的元件實現呢?這個問題可以通過反射來解決,只是需要滿足元件的介面和元件介面的實現的路徑和名稱滿足一定的約束條件。

我們定義元件介面和其實現的路徑和名稱的約束條件如下:

  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的方案,可以簡化引數的傳遞和解析工作。感興趣的同學可以移步:github.com/a3349384/Fr…

最後歡迎關注我的部落格:zhoumingyao.cn/

相關文章