Android組件化探索與實踐

来源:https://www.cnblogs.com/DoNetCoder/archive/2019/02/17/10389912.html
-Advertisement-
Play Games

什麼是組件化 不用去糾結組件和模塊語義上的區別,如果模塊間不存在強依賴且模塊間可以任意組合,我們就說這些模塊是組件化的。 組件化的好處 1. 實現組件化本身就是一個解耦的過程,同時也在不斷對你的項目代碼進行提煉。對於已有的老項目,實現組件化剛開始是很難受的,但是一旦組件的框架初步完成,對於後期開發效 ...


什麼是組件化

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

組件化的好處

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

最後歡迎關註我的博客:http://zhoumingyao.cn/


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 2019.2.17日:最終安裝成功,完美解決! 2019.2.16日:最終安裝未成功,但是過程中排除 了幾個bug,前進了幾步,僅供參考。 寫在最前面,yocto安裝是有系統要求的,Deepin 15.8不在此Freescale SDK安裝的官方適配範圍內,樓主只是一時興起,強行安裝。 首先在VMw ...
  • 最開始從51入門、之後MSP、ARM、FriendARM等等和使用keil(MDK)、iar等工具、之後Arduino、Raspberry Pi的人想說:”說’Arduino是玩具,和Arduino極其簡單‘的人,基本是沒有搞過Arduino的人,或者是接觸Arduino淺嘗輒止的人。“Arduin... ...
  • 狂神聲明 : 文章均為自己的學習筆記 , 轉載一定註明出處 ; 編輯不易 , 防君子不防小人~共勉 ! Nginx學習筆記 課程目標 理解反向代理,負載均衡的概念 . 掌握Nginx在Linux下的安裝 , 配置 掌握在項目中使用Nginx反向代理實現負載均衡 瞭解Nginx負載均衡的常用配置方案 ...
  • 前言 目前市面上的PC電腦主要運行著四大類系統,它們分別是微軟的Windows、蘋果的MacOS、Linux的發行版以及Unix類系統。其中Linux和Unix都是開源的,因此市面出現的眾多基於Linux內核和Unix內核的發行版系統,其中Linux類系統數量最多、用戶基數大,廣泛使用於各行各業。相 ...
  • 1. 概述 1. 不同的操作系統, 對於 換行, 有不同的表示 2. 不算是很重要, 但有時候也會製造些小麻煩 2. 換行 1. lf 1. 名稱 1. linefeed 2. 轉義字元 1. \n 2. cr 1. 名稱 1. carriage return 2. 轉義字元 1. \r 3. 操作 ...
  • 背景 ​ 回顧人們在開始工作之前,都會問自己這樣一個問題:給你一臺16G記憶體的Innodb專用資料庫伺服器,如何配置才能讓其穩定、高效地給典型的Web應用提供服務? 硬體 記憶體:記憶體對於Innodb資料庫至關重要!16 32G記憶體 CPU:雖然CPU配置依賴於應用,但一般來說,就CPU來說,2 Du ...
  • 第一步:下載Tomcat8壓縮包 進入 http://tomcat.apache.org/download-80.cgi 下載tar.gz壓縮包 第二步:用ftp工具把壓縮包上傳到/home/data/下 第三步:解壓以及新建目錄 [root@localhost ~]# ls /home/data/ ...
  • --查詢資料庫邏輯文件名 USE 資料庫名 SELECT FILE_NAME(1) --查詢資料庫邏輯文件名(日誌) USE 資料庫名 SELECT FILE_NAME(2) --附加資料庫 sp_attach_db '資料庫名','資料庫全路徑','資料庫日誌全路徑' GO USE 資料庫名 -- ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...