得物App直播復(fù)雜頁面架構(gòu)實(shí)踐

原創(chuàng) 李振全 得物技術(shù)

1. 背景

當(dāng)前直播間業(yè)務(wù)迭代越來越頻繁,開發(fā)人員也越來越多,而幾乎百分之九十的需求都是在 直播觀眾頁,直播主播開播這兩個(gè)頁面上的功能開發(fā)和代碼累積。因此,頁面中代碼的膨脹速度相當(dāng)快。

直播代碼之前做了一次 Layer 層級(jí)的拆分,將整個(gè)直播間按照視圖的層級(jí)進(jìn)行了劃分,通過 ViewStub 對(duì)每個(gè)層級(jí)進(jìn)行漸進(jìn)式的加載,以提高頁面的加載速度,Layer 層級(jí)拆分后的組件圖如下:

image.png

但現(xiàn)在越來越不能支撐目前迭代的速度,因?yàn)橹安鸱值牧6仁且?strong>視圖層級(jí)為單位的,粒度比較粗,容易代碼膨脹。為了防止 Layer 層代碼邏輯過度膨脹大家也各顯神通,通過一個(gè)個(gè)幫助類,
xxxController/xxxHelper/xxxUtil/xxxManager/xxxAdapter/xxxHandler 等將各個(gè)業(yè)務(wù)邏輯封裝起來,代碼風(fēng)格各異,閱讀起來也比較費(fèi)勁。即使這樣各種抽取,也無法避免 Layer 邏輯的膨脹,且各幫助類沒有統(tǒng)一的API 和代碼規(guī)范, 很依賴外部調(diào)用的正確性,目前最大的 Layer視圖交互層代碼量已經(jīng)接近兩千行,消息面板及商卡層也有一千行左右。

布局 XML 復(fù)雜,預(yù)埋控件很多。直播間有不同種類的直播間(帶貨、娛樂),有各種 AB 策略,還有各種活動(dòng)入口,大部分組件實(shí)際是不展示的,可能一個(gè)直播間只需要展示其中一兩項(xiàng),但是目前很多都是預(yù)埋到 XML 中代碼動(dòng)態(tài)控制其顯隱,這其實(shí)就是無意義的性能消耗。

Layer 層級(jí)之間互相引用,職責(zé)不單一,比如:語音連麥邏輯是在一個(gè)單獨(dú)的LiveRoomVoiceLinkLayer語音連麥層, 連麥按鈕在 FunctionLayer 的底部布局中,需求是當(dāng)主播接受連麥的時(shí)候,這個(gè)按鈕不可點(diǎn)擊,當(dāng)掛斷連麥的時(shí)候,這個(gè)按鈕可以點(diǎn)擊。

image.png

代碼是這樣:


image.png

也就是在 語音連麥層 調(diào)用了宿主 Fragment,然后通過宿主Fragment獲取到 FunctionLayer 也就是視圖交互層的 View,去進(jìn)行控制的。這就造成了層級(jí)之間的相互調(diào)用,且修改了本不屬于自己層級(jí)的視圖。這樣會(huì)有什么問題呢?比如說一個(gè)不清楚這塊邏輯的同學(xué)需要對(duì)連麥按鈕的點(diǎn)擊狀態(tài)進(jìn)行修改,他在 FunctionLayer 找到了這個(gè)按鈕,理所當(dāng)然的在 FunctionLayer 進(jìn)行修改,但是可能收到預(yù)期外的效果,因?yàn)榇藭r(shí)可能這個(gè)按鈕被 LiveRoomVoiceLinkLayer 給改回去了,本屬于 FunctionLayer 的視圖被其他地方修改了。

視圖層級(jí)架構(gòu)由簡(jiǎn)單的視圖層級(jí)關(guān)系變成了互相調(diào)用的關(guān)系:


image.png

在這個(gè)背景下,我們一個(gè)初步的想法是將直播頁面進(jìn)行組件化拆分。組件內(nèi)部處理組件各自的業(yè)務(wù),粒度更小,邏輯更內(nèi)聚。直播頁只需注冊(cè)這些組件,然后根據(jù)數(shù)據(jù)驅(qū)動(dòng)的思想,當(dāng)數(shù)據(jù)發(fā)生改變時(shí),因?yàn)榻M件內(nèi)部 observe 了自己關(guān)心的數(shù)據(jù),就可以自動(dòng)更新組件。這樣還有個(gè)好處就是可以動(dòng)態(tài)注冊(cè)組件,也就是只有當(dāng)真正需要展示此組件的時(shí)候再去注冊(cè)組件模塊,而不必所有組件都在進(jìn)入直播間的一瞬間去初始化,減少性能的消耗。

也就是期望是這樣一個(gè)新的組件架構(gòu):

image.png

直播間、商詳都是非常復(fù)雜的一個(gè)頁面,而我們的交易商詳已經(jīng)有了組件化拆分的成功示例,直播作為后來者其實(shí)做起來的難度并不是很大,但是卻有可預(yù)見的較大的收益。

目標(biāo):因?yàn)槲覀兇舜胃脑鞂儆诖a重構(gòu),所以核心目標(biāo)是讓程序在功能不變的前提下代碼更清晰,拓展性更好,更解耦,改善代碼膨脹問題; 次要目標(biāo)是在做完此項(xiàng)重構(gòu)后能夠提升代碼的運(yùn)行性能,可以方便的動(dòng)態(tài)注冊(cè)和刪除組件,通過數(shù)據(jù)驅(qū)動(dòng)動(dòng)態(tài) inflate 添加布局,刪除預(yù)埋在 XML 中的布局,做到按需加載業(yè)務(wù)模塊,提高頁面啟動(dòng)速度。

2. 基礎(chǔ)頁面組件設(shè)計(jì)

我們通過抽象一個(gè)輕量化的頁面內(nèi)組件單元——IComponent 來解決上面背景中提到的幾個(gè)問題。

(1)針對(duì)各幫助類沒有統(tǒng)一 API 問題 ,基礎(chǔ)組件需要統(tǒng)一 API 回調(diào)和代碼規(guī)范。

類似 Activity 生命周期模板方法,組件需要默認(rèn)提供組件自身獨(dú)立的生命周期和宿主如 Activity/Fragment/父Component 生命周期回調(diào)。

之前的 Layer 是在宿主中主動(dòng)調(diào)用 lifecycle.addObserver():

image.png

新的組件可以簡(jiǎn)化這步操作,默認(rèn)實(shí)現(xiàn)對(duì)父組件的生命周期觀察,同時(shí),自身生命周期跟隨父組件生命周期,父組件銷毀,子組件也會(huì)跟著銷毀,類似于 ViewGroup 和 View 之間的關(guān)系。這也符合業(yè)務(wù)上的定義,比如我們要銷毀某一個(gè)大的組件我們只需解綁這個(gè)大組件即可,無需關(guān)心其內(nèi)部的子組件的解綁。

如何實(shí)現(xiàn)呢?

image.png

這里我們定義了 IComponent 接口實(shí)現(xiàn) LifecycleOwner 和 DefaultLifecycleObserver, 作為和宿主 Fragment/Activity/Component 生命周期的綁定,對(duì)外暴露系統(tǒng)同名也和系統(tǒng)生命周期一致的回調(diào)(onCreate/onStart/onResume/onPause/onStop/onDestroy),并且提供組件自身的 onAttach/onDetach 生命周期, onAttach 代表綁定上了宿主,onDetach 代表從宿主解綁;開發(fā)者只需要在特定的生命周期編寫業(yè)務(wù)邏輯即可,比如 onAttach 進(jìn)行一些初始化 View 和監(jiān)聽的操作, onDetach 進(jìn)行一些釋放資源的操作,onResume/onPause 做一些曝光埋點(diǎn)的上報(bào)等等,使業(yè)務(wù)組件可以專注自身業(yè)務(wù)的邏輯實(shí)現(xiàn)。

(2)針對(duì)代碼膨脹問題,組件需要支持任意層級(jí)嵌套,細(xì)化組件的粒度。

組件可以有他的子組件,子組件也可以有子組件的子組件,組件之間的組合方式就是一個(gè)樹形結(jié)構(gòu)。

這里我們抽象了 IComponentRegister 接口用來提供組件注冊(cè)和管理的功能,實(shí)現(xiàn)此接口需要提供一個(gè) Map 記錄注冊(cè)的組件,以及對(duì)應(yīng)的注冊(cè)反注冊(cè)方法。

image.png

父組件的生命周期是在 register 的時(shí)候綁定,unregister 的時(shí)候解綁。目前實(shí)現(xiàn)IComponentRegister接口的有 BaseLiveActivity/BaseLiveFragment/BaseComponent, 他們都支持組件的注冊(cè)和反注冊(cè)。

image.png

(3)針對(duì) Layer 層級(jí)之間互相引用,職責(zé)不單一問題,新組件需要做到同級(jí)組件之間不強(qiáng)依賴。

實(shí)現(xiàn) IComponentRegister 接口的宿主提供的 map 是 protected 修飾的,所以組件不能直接獲取到同級(jí)的組件。

那原本組件之間的耦合怎么處理呢?

組件之間的耦合分為兩種:

一種是組件 A 依賴組件 B 的一個(gè)狀態(tài),比如點(diǎn)贊模塊時(shí)可能需要知道關(guān)注模塊的關(guān)注狀態(tài);對(duì)于這種我的解決方案是組件 A 不直接依賴組件 B 而是依賴一個(gè) LiveData 數(shù)據(jù)狀態(tài),關(guān)注的狀態(tài)通過 LiveData 存在ViewModel中,組件 A 依賴的就變成 ViewModel 即可。

一種是組件 A 依賴組件 B 的 View 視圖,比如引導(dǎo)組件在顯示評(píng)論引導(dǎo)的時(shí)候需要評(píng)論控件作為定位錨點(diǎn),因?yàn)樵u(píng)論控件屬于底部操作區(qū),而底部操作區(qū)是需要根據(jù)直播類型以及 AB 動(dòng)態(tài)去創(chuàng)建的,所以在初始化的時(shí)候可能評(píng)論控件并沒有初始化完成,所以在注冊(cè)引導(dǎo)模塊的組件時(shí)通過構(gòu)造函數(shù)無法獲取到評(píng)論控件。

這種依賴我的解決方案是組件 A 不直接依賴組件 B 而是通過根布局 view或者AB組件對(duì)應(yīng)的父布局 findViewById 獲取 組件 B 的 View 視圖,如下:

image.png

這樣其實(shí)并不算是完全解耦,只是將顯式的依賴其他組件變成了依賴布局中的一個(gè)控件,并且注意這種方式獲取的View只能讀,不能寫,算是目前解耦的臨時(shí)方案。也是本次改造做的不完美的一個(gè)地方,但考慮到如果要徹底解決這個(gè)問題,成本較高,開發(fā)也會(huì)更不便捷,這里最終取舍還是開發(fā)效率優(yōu)先。大家如果有更好的處理方式,歡迎討論。

(4)針對(duì)布局 XML 越來越復(fù)雜預(yù)埋控件很多的問題,新組件方便的需要支持 ViewStub 改造。

2.1 抽象 BaseComponent

Android 應(yīng)用架構(gòu)指南說到,最重要的原則就是分離關(guān)注點(diǎn):

image.png

那么我們抽象出的 BaseComponent 要做的就是這么一件事,將頁面邏輯按照業(yè)務(wù)進(jìn)行劃分為一個(gè)個(gè)的頁面內(nèi)組件,將關(guān)注點(diǎn)內(nèi)聚到自己業(yè)務(wù)的獨(dú)立組件中。

image.png

BaseComponent 實(shí)現(xiàn) IComponent 和 IComponentRegister 作為組件的最小單元;IComponent 用來提供最基本的組件的綁定解綁生命周期,IComponentRegister 接口用來提供組件注冊(cè)和管理的功能,以及和宿主的生命周期綁定。組件以業(yè)務(wù)邏輯為核心,可以對(duì)應(yīng)1或多個(gè) UI 元素或者 0 個(gè) UI 元素,多個(gè)UI 元素從業(yè)務(wù)邏輯層面屬于同一個(gè)模塊,0 個(gè)就是純邏輯層面的組件而已。

2.2 抽象 BaseLiveComponent

因?yàn)橛脩舳擞猩舷禄袚Q直播間的交互,為了兼容歷史邏輯,所以在 BaseComponent 之上又抽象了一個(gè) BaseLiveComponent,BaseLiveComponent 繼承自 BaseComponent 之外又實(shí)現(xiàn)了 ILiveLifecycle(直播特定生命周期), LayoutContainer(kotlin 自動(dòng)findViewById 插件)。

image.png

ILiveLifecycle 有三個(gè)生命周期,分別是 onSelected 選中,unSelected 不選中,對(duì)應(yīng) ViewPager 的 onPageSelected 和滑出,這兩個(gè)生命周期還是比較好理解的,destroy 又是什么呢?

Destroy 從字面意思看是銷毀的時(shí)候做一些資源的釋放,但實(shí)際并不是宿主 onDestroy生命周期 而是 onPause時(shí)且isFinishing=true,這是因?yàn)?onStop/onDestroy 回調(diào)是根據(jù) IdleHandler 來的,如果主線程消息隊(duì)列消息比較多,那么 onStop/onDestroy 就會(huì)延遲回調(diào)(詳細(xì)可參考
https://juejin.cn/post/6936440588635996173),資源釋放onStop/onDestroy這里就會(huì)釋放不及時(shí)。于是我們給直播間的 ILiveLifecycle 加了一個(gè)特定的 destroy 方法,用來及時(shí)釋放資源。

上面我們說到 BaseComponent 有兩個(gè)生命周期,onAttach 代表綁定上了宿主,onDetach 代表解綁了宿主,那釋放資源現(xiàn)在就有了三個(gè)地方,宿主的 onDestroy(),ILiveLifecycle 的 destroy(), BaseComponent 的 onDetach()。

onDetach destroy onDestroy 傻傻分不清

onDestroy 是什么?
宿主的生命周期回調(diào)(Fragment/Activity)。

onDetach 是什么?
Component 被反注冊(cè)時(shí)回調(diào)。

destroy 是什么?
直播間特定生命周期,因?yàn)?onDestroy 生命周期的延遲回調(diào),我們?cè)?onPause 判斷 isFinish 執(zhí)行 destroy 進(jìn)行及時(shí)的資源釋放。

明確了三個(gè)方法的含義,為了簡(jiǎn)化組件的使用,我們將 destroy 和 onDetach 進(jìn)行了合并,因?yàn)閮烧弑举|(zhì)目的都是做資源釋放的。為了減少對(duì)歷史代碼的影響,這里 BaseLiveComponent 實(shí)現(xiàn) destroy 方法,用final 修飾即可,這樣繼承了 BaseLiveComponent 的組件就無法實(shí)現(xiàn) destroy 方法只能選擇 onDetach 方法了。

image.png

另外 BaseLiveComponent 多了一個(gè) CustomLiveLifecycleOwner 這個(gè)是干什么的呢?CustomLiveLifecycleOwner 是 BaseLiveComponent 的一個(gè)內(nèi)部類:

image.png

主要用來處理直播間上下滑的場(chǎng)景問題。

直播間 Activity 內(nèi)有一個(gè) ViewPager2, ViewPager2 上下滑的是一個(gè)個(gè)獨(dú)立的直播間 Fragment, 所以直播間 Fragment 就有了 onSelected/unSelected 的自定義生命周期,現(xiàn)在有些情況下我們需要監(jiān)聽 Activity 域的 ViewModel 的 LiveData 狀態(tài),那么我們期望的就是在直播間滑出 unSelected 的時(shí)候不響應(yīng)此監(jiān)聽,我們可以根據(jù) isSelected 屬性判斷,也可以直接使用 customLiveLifecycleOwner 作為 observe 傳入的 LifecycleOwner 參數(shù)。

image.png

頁面滑出的時(shí)候會(huì)調(diào)用到 customLiveLifecycleOwner 的 unSelected(), 這樣customLiveLifecycleOwner 的 lifecycle就是 ON_STOP狀態(tài), ON_STOP 不會(huì)響應(yīng)對(duì)應(yīng)的 LiveData 監(jiān)聽,也就符合了我們的期望。onSelected, destroy 同理。

2.3 組件之間通信

根據(jù)我們的需要和架構(gòu)現(xiàn)狀,這個(gè)并沒有什么糾結(jié)的,因?yàn)槲覀冎暗募軜?gòu)模式就是 Jetpack MVVM 模式,所以這里還是繼續(xù)沿用 ViewModel LiveData 的方式進(jìn)行通信。這樣之前的代碼基本就不用動(dòng),直接挪到 Component 即可。組件內(nèi)部自己管理自己的狀態(tài),組件只需關(guān)注自身功能實(shí)現(xiàn)而不必關(guān)系與其他組件的交互。這里需要注意的兩點(diǎn),一個(gè)是LiveData數(shù)據(jù)倒灌問題,這個(gè)網(wǎng)上很多資料我就不再贅述了,另外一個(gè)是如何讓 LiveData 保持消息同步的一致性,什么意思呢?

比如我們?cè)?ViewModel 定義了一個(gè)心跳接口返回的 MutableLiveData 數(shù)據(jù):

image.png

那么外面只要拿到此 ViewModel 就可以更新這個(gè) MutableLiveData 的數(shù)據(jù),這就是消息同步的不一致。如何做到一致呢?其實(shí)也非常簡(jiǎn)單,只需要定義成這樣:

image.png

私有化 MutableLiveData,暴露出一個(gè) pulic 的 LiveData,為什么要這么做呢?

這是因?yàn)?LiveData 對(duì)于寫操作是 protected 的,對(duì)于讀操作是 public 的,也就限制了外部拿到 ViewModel 的時(shí)候只能讀,不能寫,這樣就限制了我們只能在 ViewModel 內(nèi)部進(jìn)行寫操作,這就內(nèi)聚了邏輯確保了消息同步的一致性。

image.png

詳情可以參考“重學(xué)安卓:架構(gòu)組件 “一致性問題” 全面解析”【1】

3. 改造

在完成基礎(chǔ)組件的代碼設(shè)計(jì)后,我們來看下如何進(jìn)行代碼的具體改造。

3.1 基礎(chǔ)使用和注意點(diǎn)

(1)定義一個(gè)業(yè)務(wù) Component, 選擇繼承 BaseComponent or BaseLiveComponent。

BaseLiveComponent 和 BaseComponent 的區(qū)別在于,BaseLiveComponent 有直播間上下滑特殊生命周期處理(onSelected, unSelected, destroy)

比如直播間用戶端因?yàn)槭怯猩舷禄换サ?,所以可以選擇繼承 BaseLiveComponent;主播端的組件因?yàn)闆]有上下滑的交互,所以可以選擇繼承 BaseComponent;如果是想主播端和用戶端共用的組件并且不涉及上下滑的交互處理,那么可以選擇繼承 BaseComponent。

(2)按需實(shí)現(xiàn) onAttach onDetach 方法,以及需要在宿主生命周期執(zhí)行的業(yè)務(wù)邏輯。
在 onAttach 綁定生命周期宿主后進(jìn)行初始化的操作,如注冊(cè) LiveData 監(jiān)聽,初始化 View 狀態(tài),設(shè)置點(diǎn)擊事件等,因?yàn)榇藭r(shí) Component 對(duì)象已經(jīng)創(chuàng)建完成且綁定了宿主的生命周期。

這里需要注意的是:宿主生命周期方法如 onCreate 可能是跟綁定的 Fragment onCreate 回調(diào)時(shí)機(jī)有一定延遲的,宿主生命周期方法回調(diào)取決于你registerComponent 的時(shí)機(jī)。比如我們可能是 View 創(chuàng)建完畢 onCreatedView 的時(shí)候才會(huì) registerComponent,或者更晚的時(shí)機(jī),此時(shí) Fragment onCreate 已經(jīng)執(zhí)行完畢,所以實(shí)際上 Component 組件回調(diào)的 onCreate 真正時(shí)機(jī)是你創(chuàng)建 Component 并綁定宿主生命周期的時(shí)候。當(dāng)然如果你是在 Fragment 的 onCreate 就去注冊(cè) Component 組件,那么onCreate 回調(diào)時(shí)機(jī)是和 Fragment 差不多的。

(3)在宿主中通過registerComponent 注冊(cè)子組件。

registerComponent 可以自動(dòng)跟隨宿主的生命周期。宿主可以是 BaseLiveActivity, BaseLiveFragment, BaseComponent, 或者其他實(shí)現(xiàn)了 IComponentRegister 接口的類。(推薦只在復(fù)雜的 Activity/Fragment 使用)

以優(yōu)惠券組件為例(這里省略了大部分業(yè)務(wù)邏輯代碼,著重突出組件主流程代碼):

//1\. 創(chuàng)建 CouponComponent 繼承 BaseLiveComponent 
class CouponComponent( 
    override val containerView: View, 
    private val itemViewModel: LiveItemViewModel, 
    private val fragment: LiveRoomLayerFragment 
) : BaseLiveComponent(containerView) { 
    private val couponViewModel by fragment.viewModels<CouponActivityViewModel>() 

    //2\. onAttach 初始化 View, 注冊(cè) LiveData 監(jiān)聽 
    override fun onAttach(lifecycleOwner: LifecycleOwner) { 
        super.onAttach(lifecycleOwner) 
        initView() 
        registerObservers() 
    } 

    private fun initView() { 
        couponNewIcon?.setOnClickListener { 
            val couponListFragment = LiveProductCouponListFragment.newInstance(LiveProductCouponListFragment.ADAPTER_ROOM) 
            couponListFragment.show(fragment.childFragmentManager) 
        } 
    } 

    private fun registerObservers() { 
        //心跳 
        itemViewModel.notifySyncModel.observe(this, { 
            notifySyncModel(it, itemViewModel.roomId) 
        }) 
        //獲取優(yōu)惠券信息 487 
        couponViewModel.couponPopupRequest.observe(this, onSuccess = { m, _, _ -> 
            showCouponDialog(m) 
        }) 
        。。。。
    } 

    // 2\. 頁面滑出邏輯如隱藏彈窗,重置狀態(tài)等 
    override fun unSelected() { 
        super.unSelected() 
        dissmissCouponDialog() 
    } 

    // 3\. 如果要做一些資源釋放寫在 onDetach 
    override fun onDetach(lifecycleOwner: LifecycleOwner) { 
        super.onDetach(lifecycleOwner) 
    } 

    // 4\. 可選,是否開啟 EventBus 開關(guān) 
    override fun enableEventBus(): Boolean { 
        return true 
    } 

    @Subscribe(threadMode = ThreadMode.MAIN) 
    fun onAutoPopTypeEvent(event: LiveAutoPopTypeEvent) { 
        if (event.autoPopType == AutoPopTypeConstant.TYPE_COUPON) { 
            couponActivityInclude.syncAutoCoupon() 
        } 
    } 
}

3.2 提高頁面打開速度

image.png

直播間有相當(dāng)多的這種掛件視圖,目前我們大多數(shù)都是將布局預(yù)埋到 XML,然后動(dòng)態(tài)控制其顯示隱藏,但實(shí)際上用戶側(cè)真正需要展示的掛件根據(jù)直播間,根據(jù)用戶都是不一樣的,有的人不是新人,那么這個(gè)新人任務(wù)的視圖就沒必要加載,有的直播間沒有抽獎(jiǎng),沒有限時(shí)直降活動(dòng)也一樣不需要加載對(duì)應(yīng)的掛件視圖。

經(jīng)過組件化拆分后,每一個(gè)掛件實(shí)際上都相當(dāng)于一個(gè)頁面內(nèi)小組件,被注冊(cè)到直播間中,組件依賴 的是一個(gè) View,那么只要我們將 View 改成 ViewStub 并在 ViewStub inflate 完成后再注冊(cè)到直播間就可以了,改造起來也就十分輕松了。

以抽獎(jiǎng)入口和限時(shí)直降入口為例:

第一步: 先將其組件預(yù)埋的 XML 布局替換成 ViewStub。

image.png

第二步:設(shè)置ViewStub inflate 監(jiān)聽,inflate完成時(shí)注冊(cè)組件。

image.png

第三步:在需要加載組件的時(shí)候動(dòng)態(tài) inflate ViewStub 組件。

image.png

改造后代碼大概是這樣:

/** 
* 直播間交互層級(jí) 
*/ 
class LiveRoomFunctionLayer( 
    override val containerView: View, 
    private val layerFragment: LiveRoomLayerFragment 
) : BaseLiveComponent(containerView) { 

    override fun onAttach(lifecycleOwner: LifecycleOwner) { 
        super.onAttach(lifecycleOwner) 
        initView() 
        initViewStubComponent() 
        registerObserver() 
        initChildComponent() 
        initViewStubObserver() 
    } 
    /** 
    * 初始化動(dòng)態(tài)加載的組件 
    */ 
    private fun initViewStubComponent() { 
        vsLotteryEntrance?.setOnInflateListener { _, inflated -> 
            registerComponent(LotteryEntranceComponent(inflated, lotteryViewModel, layerFragment)) 
        } 
        vsSecKillEntrance?.setOnInflateListener { _, inflated -> 
            registerComponent(SecKillComponent(inflated, liveInfoViewModel, layerFragment.childFragmentManager)) 
        } 
            ...... 
    } 

    private fun initViewStubObserver() { 
        lotteryViewModel.notifyAutoLotteryInfo.observe(this, Observer { 
            it?.apply { vsLotteryEntrance?.inflate() } 
        }) 
        liveInfoViewModel.notifyLiveDiscountInfo.observe(this, Observer { 
            it?.apply { vsSecKillEntrance?.inflate() } 
        }) 
            .... 
    } 
    /** 
    * 初始化非動(dòng)態(tài)加載組件 
    */ 
    private fun initChildComponent() { 
        registerComponent(BottomViewComponent(containerView, layerFragment)) 
        registerComponent(EnergyComponent(energyContainerView, layerFragment)) 
        registerComponent(ShareOrReportComponent(layerFragment)) 
        registerComponent(FansGroupComponent(containerView, layerFragment)) 
        registerComponent(CouponComponent(containerView, liveInfoViewModel, layerFragment)) 
        registerComponent(XXXComponent(depenView, dependData)) 
            ...... 
    }

這樣就完成了直播間掛件真正需要的時(shí)候再去加載,改造成本很低,且代碼可讀性也很好。

小Tip:這里是先收到 LiveData 狀態(tài)判斷需要加載 ViewStub 之后再去注冊(cè)子組件的,存在一個(gè)先后關(guān)系,那會(huì)不會(huì)導(dǎo)致后注冊(cè)的子組件內(nèi)部收不到 LiveData 狀態(tài)更新呢?答案是不會(huì)的,因?yàn)槲覀冞@里更新數(shù)據(jù)狀態(tài)使用的 MutableLiveData, MutableLiveData 可以簡(jiǎn)單的理解為粘性的事件,也就是后注冊(cè)監(jiān)聽,如果發(fā)現(xiàn)這個(gè) MutableLiveData 是有值的就會(huì)回調(diào) observe 監(jiān)聽。比如,抽獎(jiǎng)組件內(nèi)部控制 View 的顯隱邏輯都不用動(dòng)。

image.png

4. 收益

(1)統(tǒng)一代碼風(fēng)格,提升了代碼可讀性;通過使用統(tǒng)一的自定義的 Component ,提供明確的組件生命周期回調(diào),讓大家寫一個(gè)業(yè)務(wù)組件,按照模板就可以快速實(shí)現(xiàn),無需再借助很多特定的幫助類,且代碼風(fēng)格統(tǒng)一,閱讀起來也會(huì)更容易。

(2)有效改善了代碼膨脹問題;通過方便的注冊(cè)子組件的方式,代碼可以內(nèi)聚到子組件中去,組件以業(yè)務(wù)邏輯劃分,整個(gè)直播間就由一個(gè)個(gè)或大或小的組件積木搭建起來。這也符合職責(zé)單一原則,父組件負(fù)責(zé)組合子組件,子組件負(fù)責(zé)內(nèi)部具體業(yè)務(wù),當(dāng)一個(gè)組件慢慢迭代越來越復(fù)雜后,就可以考慮是否可以拆分子組件,因?yàn)樽咏M件的拆分設(shè)計(jì)的非常方便,基本只需挪一下代碼位置,注冊(cè)一下就好了,所以也不怕出現(xiàn)組件代碼膨脹閱讀困難的問題了。

(3)提高了頁面性能;采用響應(yīng)式編程思想,使用 ViewStub 動(dòng)態(tài)加載組件視圖和注冊(cè)組件,避免 View 集中加載的性能問題,以及可以直接從源頭避免不該響應(yīng)到的事件響應(yīng)。因?yàn)闆]有被注冊(cè)的組件,其內(nèi)部的業(yè)務(wù)邏輯也都是相當(dāng)于不存在的。

(4)通過私有化組件集合,避免業(yè)務(wù)組件間的耦合依賴;可以注冊(cè)子組件的地方都會(huì)有一個(gè) map 來維護(hù)子組件,可以新增和刪除子組件,但是沒有提供獲取的方法。也就是說,避免了子組件A被其他子組件B所引用,每個(gè)組件只處理自己的業(yè)務(wù),減少業(yè)務(wù)間不必要的耦合。

5. 思考&總結(jié)

5.1 思考

在歷史代碼遷移過程中要有所妥協(xié),有些歷史代碼寫的不好的地方很想順手改掉,克制一下,因?yàn)檫@樣做極其容易造成線上問題。我們一次改造就專注于一件事,比如這次就主要關(guān)注組件之間的劃分,原本的邏輯只要注入對(duì)應(yīng)的視圖依賴以及數(shù)據(jù)依賴放到對(duì)應(yīng)的生命周期即可,無需改動(dòng)任何業(yè)務(wù)邏輯。比較復(fù)雜一點(diǎn)的就是改造過程中發(fā)現(xiàn)的業(yè)務(wù)之間的耦合部分如何解耦,這個(gè)因?yàn)槊總€(gè)業(yè)務(wù)都不一樣,這里就需要具體問題具體分析。

不要一下步子跨的太大,循序漸進(jìn)的進(jìn)行改造,組件是隨著迭代動(dòng)態(tài)增加和刪除的,一下子全部拆分并不現(xiàn)實(shí),目前直播間的組件化經(jīng)歷了三個(gè)版本持續(xù)進(jìn)行改造,基礎(chǔ)組件框架已經(jīng)搭建完畢,已拆出了二十多個(gè)核心組件,后續(xù)計(jì)劃根據(jù)業(yè)務(wù)迭代的具體業(yè)務(wù),再進(jìn)行代碼上的拆分或聚合改造,這樣也不會(huì)給測(cè)試照成太大壓力。

同時(shí)要考慮到組員之間的接受成本,積極和大家進(jìn)行討論和同步,大家的認(rèn)同才是代碼架構(gòu)改造后面能否落地的關(guān)鍵,這樣才能減少改造過程中導(dǎo)致的問題,過渡也更為平滑。

5.2 總結(jié)

架構(gòu)沒有萬能的,只有更適合的。在直播間復(fù)雜頁面的場(chǎng)景下,為了改善日益膨脹的業(yè)務(wù)代碼累積,提高代碼可讀性,于是設(shè)計(jì)了這樣一套頁面組件化的方案。每個(gè)人心中或許都有自己對(duì)組件設(shè)計(jì),代碼拆分的想法,以上是我的一些思考,簡(jiǎn)單總結(jié)下有如下幾點(diǎn):

(1)更細(xì)致的業(yè)務(wù)組件拆分,分離業(yè)務(wù)關(guān)注點(diǎn),組件支持父子關(guān)系,符合單一職責(zé)原則;

(2)組件提供統(tǒng)一生命周期回調(diào)和模板方法,提高代碼可讀性和穩(wěn)定性;

(3)組件之間的通信采用響應(yīng)式編程的方式,讓組件監(jiān)聽外部狀態(tài)的變化,當(dāng)外界狀態(tài)變化時(shí),組件內(nèi)部維護(hù)好自己的狀態(tài)即可,實(shí)現(xiàn)了依賴反轉(zhuǎn);

(4)在 BaseComponent 之上抽象了一個(gè) BaseLiveComponent,兼容直播間特殊的上下滑交互,對(duì)外屏蔽特殊的處理,減少使用者的心智負(fù)擔(dān)。

如果你對(duì)頁面組件化有什么獨(dú)特的見解,歡迎評(píng)論區(qū)和我交流~

參考鏈接:

【1】
https://xiaozhuanlan.com/topic/9340256871

*文/李振全

關(guān)注得物技術(shù),每周一三五晚18:30更新技術(shù)干貨
要是覺得文章對(duì)你有幫助的話,歡迎評(píng)論轉(zhuǎn)發(fā)點(diǎn)贊~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容