本系列文章集合:從 MVP學習代碼封裝 (1) - 綜述
大家在做項目的時候絕對要面對一個需求,stateView - 缺省頁,也有叫狀態(tài)頁豌汇,我比較喜歡叫狀態(tài)頁借卧,這里我們今天要把 stateView 這個功能集成到我們的 MVP 基礎框架中。為啥,你傻啊瘾境,這 stateView 可是基礎功能歧杏,明顯要整合到基礎業(yè)務框架中的呀,難道你要傻傻的和筆者一樣迷守,要每個頁面都寫一遍嗎H蕖!兑凿!呵呵凯力,筆者在工作的第一年還真就干了這 SB 事,現(xiàn)在回頭想想也是感謝過去的自己冒傻氣礼华。當時產品每天對這個 statview 改來改去的咐鹤,真是要死的心都有啊,那滿滿的怨念現(xiàn)在都還記憶猶新啊圣絮。也是有了澤洋深刻的經歷祈惶,我對代碼封裝這塊才開始上心,真是不經歷不知道啊扮匠,這也才有了我的 MVP 這個系列的文章捧请。
我的 stateView
stateView 的實現(xiàn)方案很多,和 MVP 一樣棒搜,一個公司有自己的一套疹蛉,開源的庫也是有不少的,貌似這個 stateView 還真是個經久不衰的話題力麸,不時的總是可以在各種分享場合看到關于 stateView 的文章和庫氧吐,畢竟太典型了,而且實現(xiàn)上還真是很有味道啊末盔,先寫好不容易啊筑舅。
先來看看我寫的例子,我寫的效果很簡單陨舱,主要目的還是向大家表述 我寫 stateView 過程翠拣,而不是絢麗的效果:
我寫的例子在顯示上還是很靈活的:
- 在基類中設定 stateview 每個頁面的 layoutID
- 然后你可以在具體的某個 activity/fragment/viewGroup 上設定某個類型狀態(tài)頁對應的 layoutID
- 默認是在 activity/fragment/viewGroup 的根布局上顯示 stateview ,也可以設定 stateview 顯示在哪個布局上游盲。
上面說了下我簡單封裝的這個功能误墓,目前我看到的做 stateview 這個功能的實現(xiàn)思路有3種:
- 最簡單的, 在每個 activity/fragment/viewGroup 中的 xml 布局中 include 相關的狀態(tài)頁的 xml 進來益缎,這種方法最簡單谜慌,減少了我們很多 xml 布局中的代碼,但是 java 類中的代碼是沒有經過封裝的莺奔,還是有大量重復代碼
- 在 activity/fragment 所屬的 window 上 addView 欣范,這樣的思路就是開始從根本上封裝代碼了,這也應該是我們在寫代碼時首先應該想到怎么去封裝這個功能。這個在 window 上 addView 的方案恼琼,還是很有一些開源庫方案在用的妨蛹,但是我覺得很實際的, 就是這個思路不甚靈活晴竞,因為 stateview 的頁面只能顯示在 整個屏幕上蛙卤,那么自定義的 viewGroup 怎么辦,我們要是想 stateview 顯示在屏幕中的某一部分怎么辦呢噩死。我覺得靈活性和可維護性颤难,簡單易用性是代碼封裝的第一要務。
- 承接上個思路已维,還是 addView 行嗤,這次不是寫死在 window 上了, 而是由用戶決定衣摩,我們在 base 基類中可以默認給一種方案昂验,我想這也是大部分的需求了,然后在具體的 activity/fragment/viewGroup中艾扮,可以根據(jù)具體的業(yè)務需求指定不同的布局以容納 stateview
當然上面我都簡單介紹過我的實現(xiàn)了既琴,會跟明顯的我就是使用的第三種。這種方式我是沒看有第三方庫用泡嘴,可以全當是我自己想出來的甫恩,這里說下,筆者也是見識有限酌予,要是各位有不同意見請見諒磺箕,畢竟在沒見過相同實現(xiàn)的情況下,我自己寫出了就可以當做我自己的實現(xiàn)了不是嘛抛虫。
實現(xiàn)思路
記住核心的是:我們是在現(xiàn)有 MVP 框架上添加一個新功能 stateview
那么根據(jù)上一節(jié)我們抽象封裝聲明周期的做法松靡,使用組合的思路,給外部提供一個 stateview 的控制器建椰,而不是具體的邏輯代碼雕欺。那么首先我要思考下,MVP 中哪個角色應該持有這個 stateview 控制器棉姐,顯然應該是 V 層屠列,而不是 P 層,V 層關心的是具體顯示伞矩,那么 stateview 作為顯示的一部分笛洛,有 V 層來持有控制是合理的。
想明白這個功能加在誰的身上后乃坤,我們來思考下苛让,這個功能應該如何合理的對外表達沟蔑,顯然 stateview 是要給 view 添加各種意外狀態(tài)的頁面,那么對于具體的 view 使用者來說蝌诡,這些添加各種意外狀態(tài)的頁面的功能是有 view 提供的溉贿,而不關心 view 是如何實現(xiàn)的枫吧。好了浦旱,說到這里我們應該清楚了,這是一種代理模式的思路九杂,我們抽象出 stateview 的相關功能接口颁湖,然后 view 去實現(xiàn)接口,由 view 內部持有的具體的 stateview 實現(xiàn)類來實現(xiàn)相關方法例隆。特點是 view 會實現(xiàn) stateview 的功能接口甥捺,而不是拋出一個 stateview 對象,這是對外隱藏邏輯實現(xiàn)的一種做法镀层。我們當然可以對外提供 這個stateview 對象的方法镰禾,但是這只是為了照顧一些低度需求,而主要的對外使用上我們要按照代理模式來唱逢。
上面啰嗦了幾句吴侦,是因為我們在一開始在這點上猶豫了下, 思考這個 stateview 功能應該以何種形式對外服務坞古,是按照代理模式的思路來备韧,還是對外直接把這個 stateview 對象拋出來。想了想痪枫,結合 MVP 的封裝思路织堂,還是代理模式的思路更合適。
總體結構:
一共涉及到這幾個類:
- IStateView:對外功能接口
- DefaultStateView:外層控制器奶陈,對外提供給功能
- IStateViewProvide:內部提供 各種view 資源的接口易阳,視為一個內部子系統(tǒng)
- BaseStateViewProvide:這個子系統(tǒng)的 abs 基類,這里考慮要擴展吃粒,未來號維護抽象出一個功能基類出來
- DefaultStateViewProvide:具體的子系統(tǒng)實現(xiàn)類潦俺,這個是默認實現(xiàn),有其他實現(xiàn)可以繼承 DefaultStateViewProvide 声搁,或是 BaseStateViewProvide
- StateCodes:各種 view 對應的識別碼
看到這里黑竞,整個結構應該很清晰了,畢竟是個小系統(tǒng)疏旨,沒多少類很魂,但是很適合練手啊,想寫好這個小功能檐涝,各方面考慮到位也是很困難的遏匆。這幾個類分以下幾個角色:
- 外層功能接口:IStateView
- 頂層主控制器:DefaultStateView
- 子系統(tǒng):IStateViewProvide / BaseStateViewProvide / DefaultStateViewProvide
- 公共資源:StateCodes
不管多大型法挨,多小型的框架,功能系統(tǒng)幅聘,都是由上面這幾個角色組合而成的:
- 外層功能接口封裝了我們對外提供哪些方法功能
- 頂層主控制器是實現(xiàn)了這個外層功能接口凡纳,是提供給調用者使用的,要求使用簡單帝蒿,擴展方便荐糜,內部包含 N 多子系統(tǒng)提供具體功能,頂層主控制器值關心核心主邏輯實現(xiàn)葛超。
- 子系統(tǒng)是從主邏輯中抽象分離出的功能暴氏,封裝好了提供給頂層主控制器使用,這里我只是抽象出了 提供各種 view 對象的子系統(tǒng)出來绣张。
- 公共資源答渔,這個就不用說了。
寫這個 stateView 小功能侥涵,處處體現(xiàn)出了組合的思路啊沼撕,不同的子系統(tǒng)組合在一起就是一個大系統(tǒng)。大系統(tǒng)臃腫就可以拆分成不同的小系統(tǒng)芜飘。
下面我們開始具體代碼部分:
這個 stateview 也算是一個小小的框架了务豺,作為練手非常適合。從頭開始編寫一個框架燃箭,都是先開始寫主干結構冲呢,然后再去填充一個個具體功能實現(xiàn)。主干寫清楚了招狸,首先我們可以清楚無誤的審視我們的結構是否合理敬拓,需要不需要再大的調整,要是都寫完了再去打動裙戏,那就是大給自己找事了乘凸。另外的好處就是我們不會混亂了,結構都有了累榜,剩下的照著寫就好了营勤,會杜絕我們寫著寫著突然發(fā)現(xiàn)某個地方寫錯了,發(fā)生結構錯誤壹罚,說實話可以大大節(jié)省我們的重復工作葛作。
- 我們先來抽象 stateview 功能接口
stateview 是 view 對外展示功能的核心,view 實現(xiàn)哪個接口猖凛,調用者才能通過 view 去使用哪個接口的方法赂蠢,這里我們對外提供:顯示loading,網絡錯誤辨泳,數(shù)據(jù)異常虱岂,沒有數(shù)據(jù)玖院,顯示內容幾個功能
public interface IStateView {
void showContent();
void showDataError();
void showDataEmpty();
void shownNetError();
void showLoading();
}
- 把 stateview 功能提供給 V 層對象,當然是在 MVP 的框架下第岖。
首先 V 層的跟接口 IBaseView 繼承 IStateView 接口难菌,這樣 MVP 框架下的多有 view 對象就都有 stateview 的功能了
public interface IBaseView extends IStateView {
DefaultStateView getStateView();
}
然后我們在 V 層的 abs 抽象基類中實現(xiàn)相關的 IStateView 接口的方法,這里涉及到:V 層的 abs 抽象基類如何持有蔑滓,初始化 IStateView 接口具體實現(xiàn)類郊酒,如何添加顯示 stateview 功能的默認公共代碼。代碼只放了有關的代碼烫饼,其余的取掉了猎塞,DefaultStateView 是 IStateView 接口的具體實現(xiàn)類
public abstract class BaseActivity implements IBaseView {
// 持有 IStateView 對象引用
protected DefaultStateView mStateView;
// 返回IStateView 對象引用
@Override
public DefaultStateView getStateView() {
if (mStateView == null) {
initStateView();
}
return mStateView;
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 初始化時間節(jié)點
init();
}
// 初始化方法
public void init() {
initStateView();
}
// 初始化方法
public void initStateView() {
mStateView = createStateView();
mStateView.setRootView((ViewGroup) getWindow().getDecorView());
}
public DefaultStateView createStateView() {
return new DefaultStateView(this);
}
// 代理 stateview 功能實現(xiàn)
@Override
public void showContent() {
getStateView().showContent();
}
@Override
public void showDataError() {
getStateView().showDataError();
}
@Override
public void showDataEmpty() {
getStateView().showDataEmpty();
}
@Override
public void shownNetError() {
getStateView().shownNetError();
}
@Override
public void showLoading() {
getStateView().showLoading();
}
經過上面2部的編寫试读,我們就完成了 stateview 功能 在MVP 框架中的部分杠纵,下面就是 stateview 自身的實現(xiàn)了。
- 實現(xiàn) stateview 控制類
這個 stateview 控制類用于對外提供相關功能調用钩骇,多說點這個也可以叫門板類比藻,是外觀設計模式的概念,外觀設計模式把一個復雜的框架或是功能稱為:系統(tǒng)倘屹。這里我們的 stateview 功能也是可以稱之為一個系統(tǒng)银亲,不管這個系統(tǒng)內部實現(xiàn)有多么的復雜,對調用者來說我只要能簡單的調用就好了纽匙,才不關心你怎么實現(xiàn)的务蝠,那么這個門板類就是給調用者使用的,簡要求簡單易用烛缔,不要拖泥帶水馏段。
DefaultStateView 這個是我們的控制類,實現(xiàn) IStateView 接口
public class DefaultStateView implements IStateView {
public Context context;
private ViewGroup rootView;
private IStateViewProvide mStateViewProvide;
public DefaultStateView(Context context) {
this.context = context;
init();
}
private void init() {
initStateViewProvide(context);
}
private void initStateViewProvide(Context context) {
mStateViewProvide = new DefaultStateViewProvide(context);
}
public IStateViewProvide getStateViewProvide() {
return mStateViewProvide;
}
public void setContext(Context context) {
this.context = context;
invalidata(context);
}
@Override
public void showContent() {
if (rootView == null) {
return;
}
if (StateCodes.CONTENT.code != mStateViewProvide.getCurrentStateViewCode()) {
cleanCurrentStateView();
}
}
@Override
public void showDataError() {
if (rootView == null) {
return;
}
if (StateCodes.DATA_ERROR.code != mStateViewProvide.getCurrentStateViewCode()) {
addDataErrorStateView();
}
}
@Override
public void showDataEmpty() {
if (rootView == null) {
return;
}
if (StateCodes.DATA_EMPTY.code != mStateViewProvide.getCurrentStateViewCode()) {
addDataEmptyStateView();
}
}
@Override
public void shownNetError() {
if (rootView == null) {
return;
}
if (StateCodes.NET_ERROR.code != mStateViewProvide.getCurrentStateViewCode()) {
addNetErrorStateView();
}
}
@Override
public void showLoading() {
if (rootView == null) {
return;
}
if (StateCodes.LOADING.code != mStateViewProvide.getCurrentStateViewCode()) {
addloaingStateView();
}
}
public void cleanCurrentStateView() {
View currentStateView = mStateViewProvide.getCurrentStateView();
if (currentStateView == null || rootView == null) {
return;
}
rootView.removeView(currentStateView);
mStateViewProvide.setCurrentStateViewCode(StateCodes.CONTENT.code);
}
public void setRootView(ViewGroup rootView) {
this.rootView = rootView;
}
private void addDataErrorStateView() {
cleanCurrentStateView();
addStateViewByStateViewCode(StateCodes.DATA_ERROR.code);
}
private void addDataEmptyStateView() {
cleanCurrentStateView();
addStateViewByStateViewCode(StateCodes.DATA_EMPTY.code);
}
private void addNetErrorStateView() {
cleanCurrentStateView();
addStateViewByStateViewCode(StateCodes.NET_ERROR.code);
}
private void addloaingStateView() {
cleanCurrentStateView();
addStateViewByStateViewCode(StateCodes.LOADING.code);
}
private void addStateViewByStateViewCode(int stateViewCode) {
View view = mStateViewProvide.getStateViewByCode(stateViewCode);
if (view == null || rootView == null) {
return;
}
rootView.addView(view);
mStateViewProvide.setCurrentStateViewCode(stateViewCode);
}
private void invalidata(Context context) {
if (context == null) {
return;
}
mStateViewProvide = new DefaultStateViewProvide(context);
}
}
- 實現(xiàn) stateview 相關頁面提供器
看過第三部的践瓷,可以看到有一個關鍵功能類: mStateViewProvide院喜,這是提供相關 view 的功能類,這里覺得應該把 view 提供的功能從控制器類分離出來晕翠,再抽象出一個功能類來喷舀,因為外層控制器封裝的是整個系統(tǒng)的邏輯,而期中較為復雜的代碼片段可以再次封裝成為一個這個系統(tǒng)中一個子系統(tǒng)淋肾,這樣代碼結構可以更清晰硫麻,編寫頁簡單容易一些,不容易亂樊卓。這里 mStateViewProvide 這個對象就是對外提供 各種狀態(tài) view 的子系統(tǒng)對象拿愧。
考慮到這里以后會有擴展,所以靈活一些简识,封裝出一個 abs 基類赶掖,放一些基礎功能代碼
public abstract class BaseStateViewProvide implements IStateViewProvide {
protected Context context;
protected int curState = StateCodes.CONTENT.code;
protected Map<Integer, Integer> stateIDs;
protected Map<Integer, View> stateViews;
public BaseStateViewProvide(Context context) {
this.context = context;
init();
}
@Override
public void setContext(Context context) {
this.context = context;
}
public int getCurState() {
return curState;
}
public Map<Integer, Integer> getStateIDs() {
if (stateIDs == null) {
initMap();
}
return stateIDs;
}
public Map<Integer, View> getStateViews() {
if (stateViews == null) {
initMap();
}
return stateViews;
}
public View getStateView(int stateViewCode) {
if (stateViews == null) {
init();
}
if (stateViews == null || stateViews.size() == 0) {
return null;
}
return stateViews.get(stateViewCode);
}
public int getStateViewID(int stateViewCode) {
if (stateIDs == null) {
init();
}
if (stateIDs == null || stateIDs.size() == 0) {
return 0;
}
return stateIDs.get(stateViewCode);
}
public View inflateView(int stateViewCode) {
if (context == null) {
return null;
}
int stateViewCode2 = stateViewCode;
Integer integer = getStateIDs().get(stateViewCode);
View view = LayoutInflater.from(context).inflate(integer, null, false);
if (view == null) {
return null;
}
getStateViews().put(stateViewCode, view);
return view;
}
private void init() {
initMap();
initData();
}
private void initMap() {
if (stateIDs == null) {
stateIDs = new HashMap<>();
}
if (stateViews == null) {
stateViews = new HashMap<>();
}
}
protected abstract void initData();
protected void invalidateData() {
if (stateViews == null || stateIDs == null) {
initMap();
}
stateViews.clear();
stateIDs.clear();
initData();
}
protected void setStateVIewIDAndCleanStateView(int stateVIewCode,int stateVIewID) {
getStateIDs().remove(stateVIewCode);
getStateIDs().put(stateVIewCode, stateVIewID);
getStateViews().remove(stateVIewCode);
}
然后具體實現(xiàn)這個功能 DefaultStateViewProvide 感猛,核心就在于 重寫 initData() 這個方法,在這個方法里綁定各種 狀態(tài) view 的 layoutID奢赂,未來擴展時可以考慮繼承這個 DefaultStateViewProvide 類陪白,重寫 initData() ,也可以繼承 BaseStateViewProvide 這個 abs 基類提供更多的功能膳灶,這里涉及到重寫 外層控制器了咱士,要不也不會設計到寫個新的功能類了
public class DefaultStateViewProvide extends BaseStateViewProvide {
public DefaultStateViewProvide(Context context) {
super(context);
}
@Override
protected void initData() {
getStateIDs().put(StateCodes.LOADING.code, R.layout.layout_stateview_loading);
getStateIDs().put(StateCodes.DATA_ERROR.code, R.layout.layout_stateview_dataerror);
getStateIDs().put(StateCodes.DATA_EMPTY.code, R.layout.layout_stateview_dataempty);
getStateIDs().put(StateCodes.NET_ERROR.code, R.layout.layout_stateview_neterror);
}
public void setmDataErrorViewID(int mDataErrorViewID) {
setStateVIewIDAndCleanStateView(StateCodes.DATA_ERROR.code, mDataErrorViewID);
}
public void setmDataEmptyViewID(int mDataEmptyViewID) {
setStateVIewIDAndCleanStateView(StateCodes.DATA_EMPTY.code, mDataEmptyViewID);
}
public void setmNetErrorViewID(int mNetErrorViewID) {
setStateVIewIDAndCleanStateView(StateCodes.NET_ERROR.code, mNetErrorViewID);
}
public void setmloadingViewID(int mLoadingViewID) {
setStateVIewIDAndCleanStateView(StateCodes.LOADING.code, mLoadingViewID);
}
@Override
public View getCurrentStateView() {
return getStateViews().get(curState);
}
@Override
public int getCurrentStateViewCode() {
return curState;
}
@Override
public void setCurrentStateViewCode(int stateViewCode) {
curState = stateViewCode;
}
@Override
public View getStateViewByCode(int stateViewCode) {
View view = getStateViews().get(stateViewCode);
if (view == null) {
view = inflateView(stateViewCode);
}
return view;
}
}
總結
最后了,也沒啥說的轧钓,大伙沒事多看看設計模式序厉,設計模式要靈活應用,不必分得按照設計模式的例子來毕箍,只要設計模式的恩那個給我們提供思路就行弛房。
項目地址: BW-MVPDemo / step2_2 這部分。
其他資料
這個單元寫完有些時間了而柑,又看到一些新的文章文捶,這里整理一下
- 有個朋友在總結自己的 stateview 時有一些感想很贊
- 找到一個開源項目和我的思路基本吻合,卻別在于管理 stateview 地方不太一樣媒咳,我的是在頁面 base 層提供管理的粹排,這位兄弟的是在頁面層里,想了想涩澡,各有利弊顽耳,應該在頁面層之外也能提供靈活優(yōu)雅的使用,這點之后會考慮重構一下