電影詳細信息頁面基于基類的層,這些層與各種服務(如特定的網(wǎng)絡加載)相耦合。因為詳細頁面有想看、評分、贊等可編輯狀態(tài)。,也是和greendao數(shù)據(jù)庫耦合的(這個數(shù)據(jù)庫之前也是和網(wǎng)絡加載耦合的,但是被改裝+rxjava代替了,所以這個耦合被代替了,感謝上帝)。這個頁面需要和其他頁面交互(比如跳轉(zhuǎn)、評分同步等。),所以也與其他頁面的類相耦合。另外還有utils,view,model等等。如果你想拉出電影細節(jié)頁面,所有這些耦合都應該被剝離。需要解決的具體問題如下:
4.2準備工作-4.2.1工作量評估
首先說一下脫鉤的準備工作。因為這些任務是解耦和拆分的基礎(chǔ)。有兩件事要做,如下所示:
首先,我不喜歡玩五星。確實這部分工作量比較大~ ~ ~
-4.2.2公共資源、模型、設施等的分割。-4.2.2.1耦合示例
第一點是公共資源、模式、公用事業(yè)等的分割。這些東西雖然不需要考慮太多,但是很復雜。這個地方模塊化的時候花了很多時間。很大一部分原因是之前的貓眼版本歷史代碼不規(guī)范,對代碼耦合不夠敏感。舉幾個例子:
我們之前的utils基本都寫在一個類MovieUtils里面了。這個類就像大染缸。什么都向里面放。在傳入的參數(shù)方面也不夠規(guī)范,甚至MaoyanBaseFragment這種業(yè)務代碼都作為參數(shù)傳入。導致這個東西及其難拆。utils的方法不傳context。前人寫的時候圖省事,在項目中統(tǒng)一加了一個靜態(tài)的context,導致幾乎所有的utils都沒有傳入context,這樣的后果是這些工具方法直接以來宿主app。之前寫的common view 不夠獨立。既然想寫common view,那么就盡量讓這個view能夠獨立,不要耦合其他第三方庫,盡量使用android 官方庫。- - 4.2.2.2 資源拆分經(jīng)驗 資源的拆分其實很繁瑣。尤其是如果字符串、顏色、dimens等資源分布在代碼的每個角落,一個一個的拆解,非常麻煩。其實不用這么做。因為安卓在構(gòu)建的時候會合并收縮資源。最后,res/values下的所有文件(應該注意styles.xml)將只放入intermediate/RES/merged/../valos.xml,無用的文件將被自動刪除。最后我們可以用lint自動刪除。所以不要在這個地方待太久。就像我剛才說的,styles.xml要注意。那么需要注意什么呢?這個東西是這樣寫的:
當我們寫屬性名時,我們必須添加前綴限定符。如果不添加,當你的lib打包成aar被其他應用使用時,會出現(xiàn)屬性名重復的沖突。為什么?因為名稱BezelImageView沒有出現(xiàn)在intermediate/res/merged/../value.xml根本不要以為是屬性的限定符!
-4.2.3集成型與組合型(可選)
我們討論了資源的分割,比如utils,然后我們討論了第二點,基類的處理。我們看到電影細節(jié)頁面是建立在一堆基類之上的。每一層的基類都做了一些事情。(當時是為了頁面的快速發(fā)展而寫的。)如果我們想要分離電影細節(jié)頁面,我們需要將這些基類打包到一個aar中,并將它們放入基本庫中,供所有頁面使用。但是我們之前的基類是耦合了很多貓眼的東西,比如下拉刷新,頁面狀態(tài)等等,如果我需要寫一個頁面,我需要繼承很多片段。當然這種改變也是可以移植的。但是,對于未來的代碼迭代(修改和添加服務)來說,肯定不是好事。因為靈活性差。比如評論app需要某個頁面的一部分,而不是整個頁面,那么改變原來的就不太方便了。我希望這些頁面都是視圖,而不是片段。而且不是這種傳承方式,而是一種結(jié)合方式。也就是說,如果我想要一個帶有下拉刷新的列表視圖,那么我可以直接構(gòu)建這樣一個視圖,如果我需要任何配置,可以設置它,并且可以使用它。您可以將這個視圖放入任何視圖中,將其分割并與其他視圖組合。即:
moviercpagefulltorefreshstatusblock是一個視圖,可用于組合任何頁面上的視圖。
-4.2.3.1組件的插件和插件設計
其實我的做法更大膽或者說“懶”。希望在我的MovieR page purll store refresh status block成功構(gòu)建后,可以在頁面上運行顯示,自動加載數(shù)據(jù)。就像小時候玩積木一樣,組件和組件都是即插即用的。至于這個塊如何加載數(shù)據(jù),對用戶來說并不重要。用戶只需要得到這個塊,然后在構(gòu)建的時候設置自己需要什么。放到頁面上就可以運行了。你可以以此為例:
我們可以看到這一頁。我只是建立了兩個視圖,并把它們放在這個頁面上。我不在乎數(shù)據(jù)加載。數(shù)據(jù)加載在這個塊內(nèi)完成。然后,如前所述,這個頁面可以被應用程序激活并運行。外掛和傻瓜式思維,可能是我“懶”了吧~ ~
這個架構(gòu)怎么實現(xiàn)?接下來,我們粗略的看一下這個框架的總體實現(xiàn)思路(具體來說,我們看一下我寫的這篇關(guān)于android官方mvp框架優(yōu)化的文章:lifecycle-mvp,像前端一樣組合寫頁面)。其實這個框架也是mvp框架的思路,但是也解決了業(yè)務場景的一些問題,比如生命周期、可移植性、通信成本、易用性等。既然要講實現(xiàn)思路,那就從頭給自己總結(jié)一下,可能對讀者有幫助。先說一下mvp框架的含義:
-4.2.3.2 MVP框架含義
一般來說,mvp框架適合安卓場景。m代表模型,提供數(shù)據(jù);v是視圖,提供視圖相關(guān)的方法供演示者調(diào)用;p是presenter,它提供了一種邏輯方法來觸發(fā)頁面中的動作。
-4 . 2 . 3 . 3的官方mvp框架的缺點
網(wǎng)上有很多mvp框架,官方也推薦mvp框架。一般的區(qū)別是,合同用于承載視圖和演示者的接口定義。用片段實現(xiàn)視圖接口。然而官方用片段實現(xiàn)觀有其無奈之處。為什么無奈?對于視圖層的接口,用fragment來實現(xiàn),主要是因為fragment有生命周期。但是碎片太蠢太重。想象一下,我有一個頁面,里面有四五條內(nèi)容。為了以后移動、移除、移植每一段內(nèi)容,希望每一段內(nèi)容都做成mvp形式,塊與塊之間不耦合。那么官方的mvp框架就不適用了。因為你不能在一頁上寫五個片段。不建議在android活動中寫那么多片段,片段的典型使用場景是ViewPager。
-4.2.3.4通常是靈活的
為了靈活起見,五段內(nèi)容的視圖層不再用片段實現(xiàn),而只是普通視圖,每個視圖監(jiān)聽事件的響應仍然在view中進行(調(diào)用自己的presenter方法)。對于整個頁面的初始化加載或者下拉刷新加載,這五條內(nèi)容共享一個片段,這五條內(nèi)容對應的presenter方法加載在這個片段的onStart()和下拉刷新的監(jiān)聽回調(diào)中。然后在片段的onCreateView()中填充5段內(nèi)容的視圖。這五項內(nèi)容之間可能需要通信和數(shù)據(jù)交換,這是通過演示者分段進行的。
-4.2.3.5有生命周期的MVP:生命周期-MVP
這樣做絕對沒有問題,以上做法在我們的項目中也是存在的。但是通過幾個版本的迭代,我發(fā)現(xiàn)了一些問題:主持人太亂,分散。片段需要容納所有演示者,并在onStart()時加載()數(shù)據(jù)。每個視圖還需要有自己的演示者。并且視圖和演示者需要相互設置()。您還需要在activty或fragment的onDetroy()方法中管理演示者??偟膩碚f,人們感到困惑。尤其是你的組件需要別人使用,或者組件的使用需要其他應用,別人會得到你的組件。你要關(guān)心兩件事,即觀點和主持人。他必須知道這兩件事情中的方法,他需要把它們關(guān)聯(lián)起來,在activity/fragment的生命周期中調(diào)用一些方法。嗯。這個過程肯定有很大的溝通成本~
這就是為什么我想到前面提到的構(gòu)建方法來實例化組件,然后使用尋呼機來組合組件。特點是(具體來說,你可以看看安卓官方的mvp框架優(yōu)化:生命周期-mvp,像前端一樣組合寫頁面):
使用lifecycle-component這個組件提供生命周期。presenter被view層內(nèi)部持有,不向外暴露。build創(chuàng)建view實例時,提供TypeFactory,用于業(yè)務的擴展。業(yè)務代碼分層。用這種mvp的變種框架改寫項目的原代/寫新業(yè)務,就可以使頁面更容易移植、拓展,頁面內(nèi)的模塊也可以移動改變。當然,這種框架是建立在我們的業(yè)務基礎(chǔ)之上,框架還是需要因項目而已,沒有最好,只有更適合~4.3 接口的抽離 模塊化的準備工作前面已經(jīng)介紹過了,接下來我們需要做什么?根據(jù)前面介紹的原項目的耦合結(jié)構(gòu),我們知道我們之前的項目直接依賴于各種服務的具體實現(xiàn)。我們接下來要做的是用接口剝離這些具體的服務實現(xiàn):
-4.3.1使用servieloader解耦-對服務實現(xiàn)類的非顯式調(diào)用
-4.3.1.1官方服務加載程序
從圖中可以看出,我們的實現(xiàn)類被相應的接口所取代。但就這一步本身而言,并不太難:找到之前調(diào)用服務的地方,然后用接口調(diào)用。無非是一些服務用的比較多,換起來比較麻煩。但現(xiàn)在我們需要考慮一個問題:如何才能提供服務?首先想到的是我們留下一個參數(shù)傳入。但是這種方式會導致以后使用lib的時候通信開銷太大:你需要告訴別人我需要傳入的參數(shù)在哪里,傳入什么類型的參數(shù)。否則,您的lib將不起作用。我不希望別人在使用你的lib時檢查你的代碼是什么,如何傳遞參數(shù)。我希望lib在別人使用的時候盡可能的透明。你不需要知道lib里面寫了什么,你只需要在外面配置一個txt文本就可以運行l(wèi)ib了!那么我們該怎么辦呢?
其實java很早就提供了這個類似的功能:把提供者配置文件放在資源目錄META-INF/services中,當app運行時,遇到service loader . load(xxxinterface . class)時,會在META-INF/services的配置文件中查找這個接口對應的實現(xiàn)類的完整路徑名,然后使用反射生成一個不帶參數(shù)的實例。
我們的一般用法也是基于java提供的這個函數(shù):
-4.3.1.2官方服務加載器的改造
-4.3.1.2.1官方服務裝載機的缺陷
從前面的解釋來看,java提供的官方serviceloader至少有三個方面需要改進。
serviceloader沒有緩存功能。因為對于服務來說,大部分我們都需要使用單例模式,而不會頻繁的生成新的實例。serviceloader使用無參的構(gòu)造方法進行構(gòu)建實例。這點不用多說,肯定需要改進。誰的服務構(gòu)建的時候不需要傳入?yún)?shù)呢?serviceloader沒有預檢查等問題。因為在運行時,需要在配置文件中去尋找接口對應的實現(xiàn)類名。那么肯定會遇到接口名寫錯了,類名寫錯了,配置方式寫錯了,找不到接口實現(xiàn)類等,這些錯誤在編譯器是發(fā)現(xiàn)不了的。同時,使用serviceloader是一種非顯式的調(diào)用服務實現(xiàn)類方式,如果不在proguard中保護這些實現(xiàn)類,那么肯定會被shrink掉。除了proguard問題外,配置文件寫在資源目錄META-INF/services下對于一些手機(三星)也有兼容問題。最后,考慮servic配置文件手動注冊的缺點,serviceloader需要提供自動注冊功能。第一點在處理以上三種情況時很容易解決。只提供一個緩存,我就不多說了。
- 4.3.1.2.2 serviceloader構(gòu)造示例
其次,我們解決了這個問題:我們讓所有使用serviceloader加載服務的接口都實現(xiàn)了Iprovider接口。Iprovider接口提供了一個init(上下文上下文)方法。這樣,所有的服務實現(xiàn)類都需要實現(xiàn)init(Context)方法,并在原構(gòu)造方法中做初始化邏輯。因此,當我們調(diào)用serviceloader來加載服務時,它類似于這樣:
imageloaderiimageloader = movieserviceloader . getservice(context,imageloader . class);
在MovieServiceLoader中,生成的實例將調(diào)用init(上下文)方法一次。就這樣,我們解決了第二個問題。這里可能有朋友有一些疑問(比如美團平臺童鞋討論過這件事):為什么只傳上下文參數(shù)。服務實現(xiàn)類需要其他參數(shù)怎么辦?就我們的服務和而言,我認為我們只需要在上下文中傳遞,基本上我們可以通過上下文獲得安卓的大部分參數(shù)。而對于服務,既然是服務,那么說它不會依賴于你的項目的某個具體的組成部分也是合理的。所以我覺得傳入上下文就夠了,不要傳入格式不定的對象參數(shù):
MovieServiceLoader.getService(對象...params,imageloader . class);
這樣當然可以解決所有問題。然而,這種設計思想違背了接口和實現(xiàn)之間的隔離概念。例如,我想使用圖像加載服務,所以我只需要調(diào)用它
imageLoader = movieserviceloader . getservice(context,imageLoader . class);
沒事的。別讓我知道你的具體服務是畢加索還是格萊德。我也不想知道。如果用第二種方案,我需要知道你具體的服務需要什么參數(shù),然后傳進去嗎?感覺好不友好。使用Iprovider的另一個好處是,我們只需要在MovieServiceLoader倉庫的proguard中添加它
夠了。在其他地方,當使用或創(chuàng)建新的服務接口時,不需要考慮proguard。
- 4.3.1.2.3服務加載器預處理-漸變插件
第三個要解決的問題是serviceloader的預檢驗。解決方法是編寫一個gradle插件。插件的一般流程是:
我們在build的某個階段拿到所有編譯后的class文件(夾)和jar包。使用javassit確定哪些類被@autoService修飾,配置文件中如果不存在,在其添加。查看serviceConfig配置文件里面的格式是不是正確。通過javassit來確定serviceConfig配置文件里面的類是不是在項目中存在,接口類是不是實現(xiàn)了Iprovider接口。- - - 4.3.1.2.4 需要用到的知識:build流程,javassit,groovy 這里不想說太多,但是考慮到這三點,很多讀者可能并不熟悉。直接去網(wǎng)上谷歌,光是這些東西,你可能需要學點東西。然后我寫下我的一些經(jīng)歷(為了針對性,我就不詳細展開了)。讀者可以參考參考,有些可以事半功倍。
因為需要得到編譯后的類文件和jar包,所以需要知道build的一般流程,每個任務的輸入輸出是什么,是以文件夾的形式還是jar包的形式。比如你獲取所有類的時候,可以在組裝XXX的時候從dex任務的輸入文件夾/jar包中獲取所有類(dex任務任務已經(jīng)完成)。同理,也可以使用javac task的輸入。但是不允許javac任務輸出,因為javac任務輸出的中間/類文件夾只包含項目中的類文件,不包含aar對應的中間/分解-aar文件中的類文件。轉(zhuǎn)換也是一種實現(xiàn)。給出了轉(zhuǎn)換的輸入輸出文件路徑,輸入類都是類。
除了構(gòu)建過程,您還可以使用groovy來編寫插件邏輯。然而,如果你真的不想使用groovy,你也可以使用java,Java是相互兼容的,但是groovy的許多特性,比如循環(huán),是不能使用的。這里有一點經(jīng)驗:寫goovy,ide不能很好的提示錯誤,比如你用了一個變量或者一個方法,如果方法錯了,變量就是未定義的。不會給你找不到的暗示。所以,還是寫信給比較好。java,然后轉(zhuǎn)到。太棒了。
最后,你需要了解一些javassit的知識,這是一個處理類文件的工具。它非常強大,類似于java,它的大部分使用將基于ctClass。所以這個類應該比較熟悉。這里有一點經(jīng)驗:有時候需要CTClass>:對于類轉(zhuǎn)換,記得用靜態(tài)變量來存儲這個類對象,否則會報告classloader多次加載同一個路徑的異常。
好了,用serviceloader解耦的原理、改進和好處都說完了。
-4.3.2服務加載器解耦與路由模式解耦
網(wǎng)上大部分關(guān)于模塊化的博客都是使用路由解耦的。什么是路由模式的解耦?
-4.3.2.1路由模式的解耦描述
讓我們看看一般的路由框架
那么這個路由框架是如何工作的呢?這里的操作是一個服務,而提供者是一個映射集,它保存了lib中的所有鍵值對(操作名:操作實例)。在宿主應用程序中注冊每個庫的提供者。這樣,當模塊A請求moduleB的服務時,它傳遞(代碼來源):
即通過提供提供者名稱、動作名稱、參數(shù)名稱和值,在注冊的映射中找到對應的動作實例,然后調(diào)用其對應的方法。核心是使用字符串匹配對應的實例進行解耦。
-4.3.2.1.1去耦路由模式的優(yōu)勢
這種方法最大的優(yōu)點是,在創(chuàng)建新的服務時,不需要編寫接口,所有的接口都用字符串標記進行匹配,兩個模型之間不需要耦合任何東西,甚至接口聲明也不需要耦合。如果一個lib中有很多服務需要外界調(diào)用,調(diào)用的次數(shù)不多,或者如果我只對服務進行解耦,那么這種路由方式非常好,因為不需要寫接口。
-4.3.2.1.2討論路由在服務解耦中的不適用性
但是為什么不選擇這種脫鉤方式呢?因為這樣,對于安卓的整體服務脫鉤,我還是提出了以下關(guān)注點(僅代表我自己的觀點,可能比較粗糙,不是說別人的項目不夠優(yōu)秀):
對于大面積的解耦,肯定大部分是app界別的服務進行解耦。特點是大量使用,這時候我寫幾個接口,下沉到base庫,無傷大雅。這樣我在使用的時候,serviceloader好處就突出來了:使用服務的時候,我不需要關(guān)心實現(xiàn)類的類名,包名是什么,需要傳入什么參數(shù),調(diào)用的方法的名字是什么。如果使用路由方式接口,我需要關(guān)心的事情就多了,如果我需要關(guān)心這么多東西,它就不應該叫服務了。如果另一個lib在你不知情的情況下改了名字怎么辦?并且在代碼移植到其他app或獨立運行時,配置方式也不夠友好。serviceloader只需要寫個配置txt文件放在apk中即可,并且每一個lib的服務寫到自己的serviceCinfig即可,不需要宿主app關(guān)心。使用路由方式,即使action可以自動注冊,也需要在application處理一些注冊的事情。路由這種服務框架和serviceloader,本質(zhì)來說,并不能進行真正意義上的模塊間的通信。說的通俗點,路由框架能做的是:b lib可以在不依賴a lib項目的情況下,b可以new 出來a中一個類的實例(或提前new好),然后調(diào)用那個實例的方法。這并非通信,只是能夠調(diào)用其他倉庫的方法。而通信指的是監(jiān)聽狀態(tài),回調(diào)。serviceloader同樣也做不到真正意義上的通信。模塊間通信只能通過非顯式的監(jiān)聽機制才能進行,比如eventbus,廣播,contentprovider等來進行。為什么要說這一點呢?因為我看到很多模塊化的博客都在說使用路由框架進行模塊間通信。但就前面提到的這種路由框架,確實做不到真正意義上的模塊間通信。好了,serviceloader解耦vs route解耦在這里。
4.4關(guān)于解耦的其他工作-4.4.1工作評估
前面的大部分工作是關(guān)于通過使用serviceloader來分離服務。那么除此之外還需要做什么呢?這里我就概括一下,然后逐一說明:
-4.4.2服務實施的撤銷
注意第一點的后半部分:如果想讓所有模塊獨立打包運行,需要把所有服務實現(xiàn)都拉出來。如果不想獨立運行,只想解耦,那么可以留在宿主app。雖然說這樣的話很容易。但是沒有一個服務實現(xiàn)要花很多時間去實現(xiàn),因為一個服務可能會耦合很多東西,不小心很難拆解。讀者應該記住一個數(shù)字。但是如果你能把它拉走,試著把它拉走,而不僅僅是lib的獨立運作。對于之后的服務更換也是大有裨益的。比如網(wǎng)絡加載庫,之前用的是retrofi+okhttp,后來升級為改裝+長連接。替換只是更改服務配置文件中的一句話。如果打算抽離,要注意接口的定義,不要耦合某個特定庫的類,綜合考慮,合理設計。例如INet庫,接口定義為:
publiciinterceinetserviceextendsiprovider {
& ltT>。t create(FinalClass & lt;T>。服務,字符串獲取數(shù)據(jù)策略,字符串緩存時間);
}
雖然改裝是一個很棒的庫,但是接口沒有耦合這個庫。也許有一天會被取代。
-4.4.3撤銷數(shù)據(jù)庫
第二點說起來很痛苦。拉走數(shù)據(jù)庫真的很麻煩。不知道是哪個版本開始的。貓眼再加上綠島。這個數(shù)據(jù)庫本身很優(yōu)秀,但是太大了裝不下!如果我想給別人一個lib,我必須把這個lib和一個大型的第三方數(shù)據(jù)庫連接起來嗎??。∫驗橹皼]有考慮模塊化,基本上所有的網(wǎng)絡數(shù)據(jù)和敏感數(shù)據(jù)都被grrendao保存了。所以每次在解耦的時候看到刀會話,都覺得自己像只老虎。網(wǎng)絡數(shù)據(jù)存儲在文件中,對業(yè)務代碼透明。敏感數(shù)據(jù)存儲在數(shù)據(jù)庫中,但通過接口隔離,建議數(shù)據(jù)庫使用官方數(shù)據(jù)庫sqlite或room。
-4.4.4告別黃油刀
第三點意味著,如果你想獨立地模塊化業(yè)務代碼,你必須像butterknife框架一樣告別視圖注入功能。安卓ADT14啟動后,庫的R資源不再是最終的,所以不能在庫中使用R.id.xx,需要改用findViewById();也不能用switch(R.id.xx),需要用if...否則。
第四點是第一點的后續(xù)工作。工作不多。
-4.4.5頁面跳轉(zhuǎn)-4.4.5.1頁面跳轉(zhuǎn)需要做什么
頁面跳轉(zhuǎn)在app中也是很重要的一件事,因為它是一個模塊化的門戶,涉及到頁面和其他應用的溝通,我的版本和頁面。雖然看似簡單,但如果設計不合理,模塊化門戶的代碼優(yōu)雅性、崩潰量、頁面退化、操作協(xié)作等方面都會受到影響。
對于頁面之間的跳轉(zhuǎn),我們的一般做法是:
如果這個類頁面沒有隱式跳轉(zhuǎn)功能: 那么直接在其他頁面首先 獲取intent(getContext(),TargetActivity.class(,然后intent添加參數(shù)。 最后starActivity(getContext(),intent)。 在目標activty的onCreate()里面getIntent().getString(xx_key,defaultValue)等獲取參數(shù); 如果xx_key對應的value不合法或者解析錯誤,比如movieId=0,或者等于“”。那么應該跳轉(zhuǎn)到一個其他頁面或者跳轉(zhuǎn)失敗。如果這個頁面配置了隱式跳轉(zhuǎn)功能: 那么在其他頁面你首先得創(chuàng)建一個createXxxActivityIntent()的utils方法,在里面?zhèn)魅肼涞仨摰膒ath,參數(shù)key,參數(shù)value。 在manifest中聲明。 在目標activty的onCreate()里面getIntent().getData().parseBoolean(xx_key,defaultValue)…等獲取參數(shù) 如果xx_key對應的value不合法或者解析錯誤,比如movieId=0,或者等于“”。那么應該跳轉(zhuǎn)到一個其他頁面或者跳轉(zhuǎn)失敗。- - 4.4.5.2 android原生頁面跳轉(zhuǎn)存在的問題 來說說這個原生頁面跳轉(zhuǎn)的問題吧~
在獲取參數(shù)的時候,需要寫一大推的intent.get(xx),如果這個頁面既含有隱式跳轉(zhuǎn),又含有顯示跳轉(zhuǎn),那么肯定上面那個過程都需要,這樣在onCreate()里面就會非常的亂。要進行if else如果想進行隱式跳轉(zhuǎn),那么都需要在manifest進行注冊intent-filter。一是麻煩,二是我需要在另外一個地方去配置某一個activity的東西,管理不方便。需要另外寫一個utils獲取隱式intent。沒有降級策略,如果運營配錯了,那么只能到錯誤頁面,而無法進行一個補救措施,比如進入i版頁面。開發(fā)人員或者后臺配置錯誤參數(shù)的時候,我們需要寫兜底邏輯。每一個頁面解析都需要寫一段相同的邏輯。如果一個頁面需要登錄用戶才可以打開的權(quán)限,那么我們經(jīng)常會寫if(isLogin()){//跳轉(zhuǎn)頁面}else{//跳轉(zhuǎn)到登錄頁面} ,每次操作都要寫這些個相同的邏輯。如果你覺得這方面沒有那么多要求,對于頁面之間的跳轉(zhuǎn),為了不耦合其他模塊的類,所有頁面都可以采用隱式跳轉(zhuǎn)機制。這已經(jīng)基本滿足情況了。但我還是想說一下阿里推出的開源框架Arouter。它具有攔截功能,使跳轉(zhuǎn)失敗可以降級(如呈現(xiàn)I版頁面),使頁面具有登錄用戶可以打開的權(quán)限;獲取參數(shù)的統(tǒng)一方式等。挺好的?;窘鉀Q了以上問題。具體就不展開了。具體來說,你可以看到開源的最佳實踐:“Android平臺頁面路由框架ARouter”
4.5模塊/頁面之間的通信-4.5.0使用視圖模型在頁面之間共享數(shù)據(jù)
這一段是新增加的。我覺得這里比較合適。ViewModel是谷歌引入的生命周期組件中的一個新類。官方文件聲明使用ViewModel可以解決頁面旋轉(zhuǎn)等配置變化時的數(shù)據(jù)保存問題。想了想,覺得也能起到解耦頁面內(nèi)數(shù)據(jù)共享的問題。
舉個我之前遇到的例子:pm讓我做一個頁面完成后的嵌入點。嵌入點需要頁面的movieId信息,但是需要嵌入點的塊中沒有movieId。而且我的街區(qū)級別很深。如果我想得到movieId,我需要把它從活動頁面級轉(zhuǎn)移到我的塊,這就不可避免地導致了中間層的耦合和方法的創(chuàng)建。當時覺得真的是大事。當時,需要像eventbus這樣的事件監(jiān)控形式。我只需要把數(shù)據(jù)放在總線上,然后就可以很容易地在這個頁面的任何地方獲得它。總結(jié)一下:說白了,當需要在塊/片段頁面之間使用對方的數(shù)據(jù)/視圖時,不需要在它們之間進行硬性引用,只需要活動的上下文參數(shù)就可以獲取對方的數(shù)據(jù)/視圖,從而交換數(shù)據(jù),訪問視圖。但是頁面的上下文是系統(tǒng)類型,容易獲取,沒有耦合。
具體使用請參考我之前寫的一篇文章,用ViewModel分享頁面中的數(shù)據(jù):ActivityDataBus
-4.5.1為什么要去掉eventbus,使用廣播
如果到了這個階段,一個頁面已經(jīng)被拉出來,剩下的就是與其他模塊和其他頁面的交互。
如前所述,serviceloader和routing方法都不能做這些事情。首先想到的是使用eventbus來做這些事情。使用Eventbus的前提是需要定義一些事件事件。例如:
但是如果把業(yè)務代碼模塊化,就有一個尷尬的問題:Event在哪里?因為很多庫需要監(jiān)聽這個Event,所以只能將Event沉入基本庫。結(jié)果基本庫越來越大,分不開了。在這一點上,《微信安卓模塊化架構(gòu)重構(gòu)實踐》也提到了這件事,并且創(chuàng)造性地使用了一種叫做的方式。api”來解決這個問題。原理是將公共接口沉入基礎(chǔ)庫,供其他模塊在編譯時使用,而這段代碼的維護仍在非基礎(chǔ)庫中。這個基礎(chǔ)庫就不擴展了,代碼維護的責任制更清晰了,挺好的??上ё罱鼪]多少時間寫這個gradle插件了。不知道哪位讀者有時間和興趣來實現(xiàn)這個插件。意義還是很大的,基礎(chǔ)庫的代碼不會越來越膨脹。Eventbus不僅擴展了基礎(chǔ)庫,還存在一個無法在應用之間進行通信的問題。我們用廣播代替eventbus。android引入的LocalBroadcast實現(xiàn)機制只是一個循環(huán)處理器,并維護一個全局映射。性能類似于事件總線,使用字符串而不是事件模型來匹配事件。如果我們使用一個接口來包裝BroadcastManager,那么我們可以在應用程序內(nèi)部使用域內(nèi)廣播,對于模塊化的lib,我們可以使用域外廣播來在應用程序之間進行通信。
-4.5.2不要亂播
如果在項目中大量使用eventbus,會看到一個類中有很多onEventMainThread()方法,寫起來很爽,讀起來很痛苦。如果項目中有許多地方可以發(fā)送此事件,則有許多地方可以接收此事件。當你拆分代碼的時候,你不敢輕舉妄動,怕哪些事件沒有收到。廣播類似于eventbus。如果一個項目中有很多相同事件的發(fā)送和接收,項目的可讀性和可維護性就會變得相當差。這種情況在敏感數(shù)據(jù)的同步中尤為突出:
實際上,對于敏感數(shù)據(jù)的同步,不需要發(fā)送廣播或eventbus進行同步。同步可以通過在數(shù)據(jù)庫的幫助下本地化所需的數(shù)據(jù)來實現(xiàn)??偟南敕ㄊ牵覀儚木W(wǎng)絡上獲得的所有數(shù)據(jù)都與數(shù)據(jù)庫同步。當用敏感數(shù)據(jù)填充視圖時,所有使用的數(shù)據(jù)都來自數(shù)據(jù)庫。當頁面返回時,如果頁面沒有觸發(fā)填充敏感數(shù)據(jù)視圖的邏輯,則在onResume()手動調(diào)用,即:
那么模塊/頁面之間的通信一般就完成了,這里沒有太多的工作要做:
4.6 lib獨立運行
-4.6.1為什么需要與主機app流程溝通
此時,業(yè)務模塊可以作為庫放在宿主應用程序中,也可以作為應用程序獨立運行。作為圖書館,很好理解。就像文章前面的問答模塊一樣,給宿主app添加幾個activty的shells,然后在lib中添加頁面,再在manifest中注冊,也就是:
當然,如果需要做一些actionBar交互,需要在主機activty中編寫相應的邏輯。整個app的框架圖如下:
當需要調(diào)試業(yè)務lib時,我們需要讓lib獨立運行,如前一篇文章中的問答業(yè)務模塊演示所示。這時候,就有問題了。我們的lib獨立運行時,賬號數(shù)據(jù)從哪里來,如何獲取與app的地理位置和城市相關(guān)的數(shù)據(jù)?讀者可能會說這些不是服務?服務,不應該像網(wǎng)絡加載和圖片加載一樣用serviceloader加載嗎?原則上是這樣的,但是賬戶等一些信息的服務實現(xiàn)類并不是那么容易從主機app中拉出的,因為服務實現(xiàn)類需要在應用中進行初始化,還有很多其他的事情需要考慮。所以真實的賬戶信息不是那么容易通過之前的方式獲取的,那怎么辦?最簡單的方法就是創(chuàng)建假數(shù)據(jù),比如創(chuàng)建自己賬號的信息,作為服務實現(xiàn)類使用。但這種情況下,賬號信息只能屬于一個人,修改賬號信息不可行,賬號無法注銷。所以要想新辦法。
-4.6.2與主機應用程序流程的溝通流程
最后發(fā)現(xiàn),如果我們自主運行的lib能夠監(jiān)控主機app的賬號、位置、城市、登錄類型、設備等信息并進行同步,那么自主運行的lib中的所有信息都是真實動態(tài)的。主機app注銷時,lib中沒有登錄狀態(tài)。具體操作是:
在宿主app中,我們提供一些contentProvider,各方法提供的內(nèi)容就是宿主app真實的的賬戶等數(shù)據(jù)。當對宿主app賬戶等信息改變時,通知contentProvider的監(jiān)聽者,比如:publicationeventmainthread(LoginEvent LoginEvent){
getContext()。getContentResolver()。notifyChange(Uri . parse(" content://com . Mao Yan . Android . RuneNV/login session "),null);
}
在獨立app中,其扮演contentProvider的監(jiān)聽者:mcontentresolver . registercontentobserver(Uri . parse(" content://com . Mao Yan . Android . runen v/device session "),false,newContentObserver(null) {
@覆蓋
publicationchange(BooleanSelfChange){
super . OnChange(SelfChange);
reloadeenvironment();
}
});
這樣一來,lib中的account等數(shù)據(jù)與宿主app的數(shù)據(jù)是一致的。我們使用服務接口包層,使使用模式與之前的服務使用模式一致。
總體示意圖如下:
主機app注銷時,lib中沒有登錄狀態(tài)。讓我們看一下演示: