這是一個(gè)有關(guān)安卓MVC框架模式的短系列,目的是思索和分析安卓中MVC模式更為真實(shí)的一面。
系列:
在上一篇中,主要從一個(gè)比較傳統(tǒng)但又精致的角度重新審視了一下安卓中的MVC模式。首先回顧一下上篇中最后有關(guān)MVC的觀點(diǎn),核心是分層,重點(diǎn)是職責(zé)如何單一和清晰化:
- 視圖V,具備展示職責(zé),職責(zé)的劃分是通過(guò)與控制者的改變無(wú)關(guān)這條原則來(lái)進(jìn)行的;比如在安卓中,以LinearLayout為例,不管使用者怎么改變,它要么縱向布局,要么橫向布局,其本身就是這樣,而與使用的場(chǎng)景無(wú)關(guān)。整體來(lái)說(shuō),V層,就是要好好的擔(dān)起展示的職責(zé),不要把屬于展示的職責(zé),交給其它層去做。
- 數(shù)據(jù)M,具備數(shù)據(jù)存取和操作職責(zé),職責(zé)的劃分是通過(guò)是否要與V發(fā)生聯(lián)系來(lái)進(jìn)行的。比如對(duì)于C來(lái)說(shuō),M應(yīng)該就是一個(gè)最終且最有目的性的需求品,C不該關(guān)心這個(gè)目標(biāo)數(shù)據(jù)怎么來(lái)的。這使得一般的校驗(yàn)和緩存等操作,應(yīng)該存在于M層。
- 控制器C,或者叫做調(diào)度器,主要用來(lái)委托操作和調(diào)度,它不應(yīng)該著眼于業(yè)務(wù)邏輯或者視圖邏輯;而要將業(yè)務(wù)邏輯和視圖邏輯,都還給對(duì)應(yīng)的V層和M層。視圖業(yè)務(wù)邏輯,可視情況委托給業(yè)務(wù)工具類(lèi)(Helper)。對(duì)于調(diào)度職責(zé)來(lái)說(shuō),要構(gòu)建一個(gè)調(diào)度體系,上級(jí)調(diào)度管理下層調(diào)度,下層調(diào)度管理下下層調(diào)度,整體就是采用分而治之的思想,實(shí)現(xiàn)頁(yè)面級(jí)別的模塊化。
理論難免枯燥,本篇就以一個(gè)完整的簡(jiǎn)單demo來(lái)聊聊頁(yè)面的模塊化,本文主要說(shuō)明怎么分模塊的問(wèn)題,很多細(xì)節(jié)問(wèn)題沒(méi)有指出,這些細(xì)節(jié)問(wèn)題很多并不是demo中方案帶來(lái)的,而是項(xiàng)目實(shí)踐中都會(huì)遇到的。
demo的倉(cāng)庫(kù)地址
下面是demo項(xiàng)目的大體介紹。
-
首先是demo的頁(yè)面原型圖,見(jiàn)附圖所示。
頁(yè)面粗略原型圖 需求分析:這個(gè)頁(yè)面是個(gè)整體可滾動(dòng)的視圖,單從原型圖來(lái)看在實(shí)際項(xiàng)目中應(yīng)該至少有一個(gè)網(wǎng)絡(luò)請(qǐng)求接口,甚至4個(gè);頂部UI需要填充多個(gè)數(shù)據(jù);WebView應(yīng)該有其對(duì)應(yīng)的配置甚至js交互;縱向list可能還能上拉加載等。滑動(dòng)沖突的問(wèn)題不作考慮,因?yàn)闊o(wú)論什么布局都要考慮。
方案一,ScrollView嵌套LinearLayout,內(nèi)部縱向嵌套4種布局,分別對(duì)應(yīng)原型圖。這種方案應(yīng)該是80%以上的童鞋都會(huì)采用的方案;但是本文并不推薦采用這種方式。這里先列三條原因:1. 除了List里面的UI,所有的UI都將會(huì)出現(xiàn)在ScrollView所在的Controller中,少說(shuō)也要超過(guò)10個(gè)ui要find和設(shè)置數(shù)據(jù)吧,容易造成C的臃腫;2. 這種頁(yè)面肯定有網(wǎng)絡(luò)請(qǐng)求,假設(shè)以比較多的一種方式,4個(gè)接口來(lái)說(shuō),所有的數(shù)據(jù)請(qǐng)求以及給View設(shè)置都出現(xiàn)在C中,也會(huì)臃腫;3. 增加和刪除布局,會(huì)比較麻煩,因?yàn)镃中代碼臃腫耦合較多。
方案二,ScrollView嵌套LinearLayout,內(nèi)部縱向嵌套4種布局,在C中分別替換成Fragment。這種方案相對(duì)方案一來(lái)說(shuō),有了一些進(jìn)步,至少一些View的find和數(shù)據(jù)請(qǐng)求被分散了。但本文仍不建議這樣,原因有三:1. Fragment的生命周期真的有點(diǎn)難以把控,尤其是當(dāng)上述布局出現(xiàn)在二級(jí)嵌套的Fragment中時(shí);2. Fragment的替換本身有一定的復(fù)雜度;3. 擴(kuò)展性有不足,若橫向List和縱向List交替實(shí)現(xiàn)多次,這種替換方式顯得被動(dòng)。
方案三,當(dāng)然就是本文推薦的方式:RecyclerView作為大C中的唯一的UI存在,那四種布局,分別對(duì)應(yīng)四種item,即對(duì)應(yīng)四種holder。這么做的優(yōu)勢(shì)有:1. 大C幾乎只處理一個(gè)View的find和設(shè)置,網(wǎng)絡(luò)請(qǐng)求也只處理部分接口,C更多的作用是調(diào)度;2.RecyclerView的adapter負(fù)責(zé)分發(fā)布局,職責(zé)比較清晰;3. 對(duì)應(yīng)的幾個(gè)Holder都相當(dāng)于小C,分別處理View的find和set,也能處理自身的網(wǎng)絡(luò)請(qǐng)求;4. 布局增刪,復(fù)用都很容易。
代碼實(shí)現(xiàn),還是上代碼比較容易理解,本demo省略了網(wǎng)絡(luò)部分。
- 大C的主要代碼
public class MainActivity extends AppCompatActivity {
private RecyclerView mGlobalList;
private GlobalAdapter mGlobalAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mGlobalList = (RecyclerView) findViewById(R.id.list_main_acty);
mGlobalList.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
mGlobalAdapter = new GlobalAdapter(this, Provider.create());
mGlobalList.setAdapter(mGlobalAdapter);
}
}
- adapter的主要代碼
public class GlobalAdapter extends RecyclerView.Adapter {
private Activity mActivity;
private Context mContext;
private List<BaseBean> mDataList;
public static final int TOP = 900;
public static final int WEB = 901;
public static final int GALLERY = 902;
public static final int SHOW = 903;
public GlobalAdapter(Activity aActivity, List<BaseBean> aDataList) {
mActivity = aActivity;
mContext = aActivity.getApplicationContext();
mDataList = aDataList;
if (mDataList == null) {
mDataList = new ArrayList<>();
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case TOP:
return new TopViewHolder(LayoutInflater.from(mContext).inflate(R.layout.holder_top, parent, false));
case WEB:
return new WebHolder(LayoutInflater.from(mContext).inflate(R.layout.holder_web, parent, false));
case GALLERY:
return new GalleryHolder(LayoutInflater.from(mContext).inflate(R.layout.holder_gallery, parent, false));
case SHOW:
return new ShowHolder(LayoutInflater.from(mContext).inflate(R.layout.holder_show, parent, false));
default:
break;
}
return null;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
((BaseViewHolder) holder).bindData(mActivity, mDataList, position);
}
@Override
public int getItemCount() {
return mDataList.size();
}
@Override
public int getItemViewType(int position) {
return mDataList.get(position).getItemType();
}
}
重點(diǎn)來(lái)了,數(shù)據(jù)和布局的分發(fā)是關(guān)鍵,畢竟這四種布局差別很大,數(shù)據(jù)也沒(méi)什么直接關(guān)系。但是本系列第一篇最后也提到,利用組合模式的思想,可實(shí)現(xiàn)數(shù)據(jù)的組合。還是上代碼好理解。
先定義一個(gè)基礎(chǔ)數(shù)據(jù)
public class BaseBean {
private int itemType;
public int getItemType() {
return itemType;
}
public void setItemType(int aItemType) {
itemType = aItemType;
}
}
- 頂部ui的數(shù)據(jù)比較好解釋?zhuān)瑱?quán)且貼出;
public class TopBean extends BaseBean {
private String name;
private String imgHead;
private int resImgHead;
private int countNum;
private int commentNum;
private String description;
.......省略
}
- 重點(diǎn)是子列表型數(shù)據(jù)的處理,以橫向列表數(shù)據(jù)為例,這里看ContainerBean和GalleryBean;這里的兩個(gè)bean類(lèi)就使用了組合思想,讓ContainerBean可當(dāng)做一個(gè)元素,也可被用作列表元素,如此帶來(lái)的額外的好處是adapter也可被內(nèi)部list復(fù)用。
public class ContainerBean extends BaseBean {
private List<BaseBean> dataList;
public List<BaseBean> getDataList() {
return dataList;
}
public void setDataList(List<BaseBean> aDataList) {
dataList = aDataList;
}
}
public class GalleryBean extends BaseBean {
private String imgUrl;
private int resId;
....省略
}
- Holder的處理也是關(guān)鍵,為了復(fù)用和減少很多代碼,一個(gè)父Holder是很重要的。里面有三個(gè)抽象方法,都有其作用。其中,數(shù)據(jù)接收或者網(wǎng)絡(luò)請(qǐng)求便可發(fā)生在bindData方法中。
public abstract class BaseViewHolder extends RecyclerView.ViewHolder implements IBindData, View.OnAttachStateChangeListener {
protected Activity mActivity;
public BaseViewHolder(View itemView) {
super(itemView);
itemView.addOnAttachStateChangeListener(this);
}
@Override
public abstract void bindData(Activity aActivity, List<BaseBean> dataList, int position);
@Override
public void onViewAttachedToWindow(View v) {
onAttached();
}
@Override
public void onViewDetachedFromWindow(View v) {
onDetached();
}
public abstract void onAttached();
public abstract void onDetached();
}
- 最后,模擬一下數(shù)據(jù),搭建一下這個(gè)列表。
public class Provider {
public static List<BaseBean> create() {
List<BaseBean> baseBeanList = new ArrayList<>();
//top
BaseBean top = new TopBean();
top.setItemType(GlobalAdapter.TOP);
baseBeanList.add(top);
//web
WebBean web = new WebBean();
web.setItemType(GlobalAdapter.WEB);
web.setUrl("https://m.baidu.com/");
baseBeanList.add(web);
//ga
List<BaseBean> innerGallery = new ArrayList<>();
innerGallery.add(new GalleryBean());
innerGallery.add(new GalleryBean());
.......
ContainerBean containerBean1 = new ContainerBean();
containerBean1.setDataList(innerGallery);
containerBean1.setItemType(GlobalAdapter.GALLERY);
baseBeanList.add(containerBean1);
//show
List<BaseBean> shows = new ArrayList<>();
shows.add(new ShowBean());
shows.add(new ShowBean());
.....
ContainerBean containerBean2 = new ContainerBean();
containerBean2.setDataList(shows);
containerBean2.setItemType(GlobalAdapter.SHOW);
baseBeanList.add(containerBean2);
return baseBeanList;
}
}
以上就是將頁(yè)面模塊的代碼實(shí)現(xiàn),整體還是比較簡(jiǎn)單的,通過(guò)這些設(shè)置,方案一中唯一的C將會(huì)變成5個(gè)C,頂級(jí)C是MainActivity,它和adapter共同來(lái)實(shí)現(xiàn)布局的調(diào)度和分發(fā);子布局分別對(duì)應(yīng)一個(gè)小C-holder,每個(gè)holder可實(shí)現(xiàn)其自己的邏輯。增加布局也很容易,只需增加一個(gè)新type數(shù)據(jù),增加一個(gè)holder即可;另外,布局復(fù)用也很方便,比如橫向List和縱向List交替復(fù)用,相當(dāng)于數(shù)據(jù)中出現(xiàn)了多個(gè)ContainnerBean,仍在List<BaseBean>和RecyclerView的適配中?;静挥脛?dòng)什么代碼。
寫(xiě)在本篇最后,本文主要目的是,通過(guò)一步步分析demo示例,揭示MVC中的C如何變得更小和清晰。demo粗糙,但想法不粗,希望能給讀者帶來(lái)啟發(fā)。在接下來(lái)的一篇中,將主要針對(duì)M層的優(yōu)化思想來(lái)場(chǎng)實(shí)戰(zhàn),以使得筆者的觀點(diǎn)更容易理解。
篇幅較長(zhǎng),碼字不易。
歡迎交流,共同進(jìn)步!
