前言
RecyclerView作為Google替代ListView的一個元件,其強大的擴充性和效能,現在已經成為無數App核心頁面的主體框架。RecyclerView的開發模式一般來說都是多Type型別的ViewHolder——後面就稱為樓層(感覺很形象)。但是使用多了,許多問題就暴露出來了,經常考慮有這麼幾個問題:
-
- 如何更便捷的使用Adapter和ViewHolder的開發模式?
-
- 如何和他人的樓層做到樓層的複用?
-
- 如何做到全域性樓層的打通?
-
- 樓層本身如何做到邏輯閉合,做到MVP的元件化模式?
功能特性
- 基於編譯期註解,不影響效能
- 使用簡單,樓層耦合度低
- 程式碼侵入性低
- 支援全域性樓層打通,多人樓層打通
- 樓層支援點對點MVP模式
- 事件中心模式,樓層只是事件的傳遞者。
- 生命週期監聽,支援邏輯的生命週期感知。
- 豐富的API,支援多方面擴充。
- 提供元件化工程使用方案
- 不用每次再寫Adapter了~
專案地址
歡迎Star?~ 歡迎提issue討論~
使用方式
這裡就介紹一下基於自己對於RecyclerView的理解,開發的一款基於AOP的,適用於多樓層模式的RecyclerView的開發框架。
核心註解
@Documented()
// 表示是基於編譯時註解的
@Retention(RetentionPolicy.CLASS)
// 表示可以作用於成員變數,類、介面
@Target(ElementType.TYPE)
public @interface ComponentType {
//ComponentId
int value() default -1;
//LayoutId,當為ViewHolder型別需要
int layout() default -1;
//元件化專案時,註解父View,通過LayoutInflater建立佈局
Class view() default Object.class;
//是否利用反射建立,預設開啟的(複雜的,效能相關的,數量大的當然建議關閉咯)
boolean autoCreate() default true;
//樓層繫結的類,通過類來尋找樓層的可用範圍
Class attach() default Object.class;
}
複製程式碼
一.單樣式列表
1.定義樓層(支援三種模式)
- 繼承Component型別
@ComponentType(
value = ComponentId.SIMPLE,
layout = R.layout.single_text
)
public class SimpleVH extends Component {
public SimpleVH(Context context, View itemView) {
super(context, itemView);
}
@Override
public void onBind(int pos, Object item) {
}
@Override
public void onUnBind() {
}
}
複製程式碼
- 繼承原生ViewHolder型別
@ComponentType(
value = PersonId.VIEWHOLDER,
layout = R.layout.person_item_layout
)
public class PersonVH extends RecyclerView.ViewHolder implements IComponentBind<PersonModel> {
private TextView tvName;
public PersonVH(View itemView) {
super(itemView);
tvName = itemView.findViewById(R.id.tv_name);
}
@Override
public void onBind(int pos, PersonModel item) {
tvName.setText(item.name);
}
@Override
public void onUnBind() {
}
}
複製程式碼
- 自定義View型別
@ComponentType(PersonId.CUSTOM)
public class CustomView extends LinearLayout implements IComponentBind<PersonModel> {
public CustomView(Context context) {
super(context);
LayoutInflater.from(context).inflate(R.layout.cutom_view_vh, this, true);
setBackgroundColor(Color.BLACK);
}
@Override
public void onBind(int pos, PersonModel item) {
}
@Override
public void onUnBind() {
}
}
複製程式碼
很清晰,不用再每次在複雜的if else
中尋找自己樓層對應的佈局檔案。(熟悉的人應該都懂)
注意:
- value:樓層的唯一標示,int型
- layout:樓層的佈局檔案
- 繼承ViewHolder和自定義View型別需要實現
IComponentBind
介面即可
對於R檔案不是常量在元件化時遇到的問題的解決方案 Wiki
這裡沒有選用butterknife將R檔案複製一份成R2的方式,我個人感覺不是特別優雅,最終我選擇的是在註解中增加一種View型別的註解,可以在註解中註解父View的Class,然後在建構函式通過LayoutInflater加入佈局檔案。
@ComponentType(
value = ComponetId.BANNER,
view = FrameLayout.class
)
public BannerVH(Context context, View itemView) {
super(context, itemView);
fgContainer = (FrameLayout) itemView;
//再利用LayoutInflater
LayoutInflater.from(context).inflate()
}
複製程式碼
2.定義Model
@BindType(ComponentId.SIMPLE)
public class SimpleModel {
}
複製程式碼
BindType:當是單樣式時,model直接註解對應的樓層的唯一標示,int型
3.繫結RecyclerView
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.common_layout);
mRcy = findViewById(R.id.rcy);
mRcy.setLayoutManager(new LinearLayoutManager(this));
new ToolKitBuilder<>(this, mData).build().bind(mRcy);
}
複製程式碼
使用對應的API,利用build()方法構建SlotsContext實體最後利用bind()
方法繫結ReyclerView.
二.多樓層模式
1.定義ViewHolder(同前一步) 2.多樣式判斷邏輯(兩種方式)
2.1 Model實現HandlerType介面處理邏輯
public class CommonModel implements HandlerType {
public int pos;
public String tips;
public String eventId;
@Override
public int handlerType() {
if (pos > 8) {
pos = pos % 8;
}
switch (pos) {
case 1:
return ComponentId.VRCY;
case 3:
return ComponentId.DIVIDER;
case 4:
return ComponentId.WEBVIEW;
case 5:
return ComponentId.TEXT_IMG;
case 6:
return ComponentId.IMAGE_TWO_VH;
case 7:
return ComponentId.IMAGE_VH;
case 8:
return ComponentId.USER_INFO_LAYOUT;
}
return ComponentId.VRCY;
}
}
複製程式碼
返回定義的ItemViewType,這裡封裝在Model內部,是由於平時我們總是將java中的Model當作一個JavaBean,而導致我們賦予Model的職責過於輕,所以就會出現更多的其實和Model緊密相關的邏輯放到了Activity,Presenter或者別的地方,但是其實當我們將Model當作資料層來看待,其實可以將許多與Model緊密相關的邏輯放到Model中,這樣我們其實單模組的邏輯內聚度就很高,便於我們理解。 (這裡思路其實來源於IOS開發中的胖Model的概念,大家可以Goolge一下)
好處:當我們需要確定樓層之間和Model的關係,直接按住ctrl,進入Model類,一下就可以找到相關邏輯。
2.2 實現IModerBinder介面自定義處理類
一款好的框架肯定是對修改關閉,對擴充開放的,當我們認為放到Model中處理過於粗暴,或者Model中已經有過多的邏輯了,我們也可以將邏輯抽出來,實現IModerBinder介面。
public interface IModerBinder<T> {
int getItemType(int pos, T t);
}
複製程式碼
對應的利用ToolKitBuilder.setModerBinder(IModerBinder<T> moderBinder)
構建即可。例如:
.setModerBinder(new ModelBinder<PersonModel>() {
@Override
protected int bindItemType(int pos, PersonModel obj) {
//處理Type的相關邏輯
return type;
}
})
複製程式碼
個人模式
當涉及到大型專案時,多人協作往往是一個問題,當所有人都維護一套ComponentId,合併程式碼時解決衝突往往是很大的問題,並且不可能所有的樓層都是全域性打通的型別,所以這裡提供一種個人開發模式。
用法
- 1.使用attach註解,繫結對應class
@ComponentType(
value = PersonId.VIEWHOLDER,
layout = R.layout.person_item_layout,
//class型別,對應到對映表的key
attach = PersonModel.class
)
public class PersonVH extends RecyclerView.ViewHolder implements IComponentBind<PersonModel> {
private TextView tvName;
public PersonVH(View itemView) {
super(itemView);
tvName = itemView.findViewById(R.id.tv_name);
}
@Override
public void onBind(int pos, PersonModel item) {
//tvName.findViewById(R.id.tv_name);
tvName.setText(item.name);
}
@Override
public void onUnBind() {
}
}
複製程式碼
- 2.呼叫SlotContext.attachRule繫結對應的Class
SlotContext slotContext =
new ToolKitBuilder<PersonModel>(this)
//註冊繫結的型別,對應獲取對映表
.attachRule(PersonModel.class).build();
複製程式碼
進階使用
專案利用Build模式構建SlotContext實體,SlotContext原理基於Android中的Context思想,作為一個全域性代理的上下文物件,通過SlotContext,我們可以獲取對應的類,進而實現對應類的獲取和通訊。
避免反射建立
框架本身利用反射進行建立,內部利用LruCache
對反射對構造器進行快取,優化反射效能。如果想要避免反射對建立,也是可以自定義建立過程。
@ComponentType(
value = PersonId.INNER,
view = TextView.class,
//註解不使用反射
autoCreate = false
)
public static class InnerVH extends RecyclerView.ViewHolder implements IComponentBind<PersonModel> {
....
}
複製程式碼
可以將不需要反射建立對ViewHolder的autoCreate=false
,然後通過ToolKitBuilder. setComponentFactory()
自定義建立過程。
具體方式->Wiki
事件中心
事件中心其實本質就是一個繼承於View.OnClickListener
的類,所有和ViewHolder本身無關的事件,統一傳遞給事件中心,再由事件中心處理,對應於一條準則:
ViewHolder只是一個專注於展示UI的殼,只做事件的傳遞者,不做事件的處理者。
使用方式:
@ComponentType(
value = ComponetId.SINGLE_TEXT,
layout = R.layout.single_text
)
public class TextVH extends Component<Text> implements InjectCallback {
private TextView tv;
private View.OnClickListener onClickListener;
public TextVH(Context context, View itemView) {
super(context, itemView);
tv = (TextView) itemView;
}
@Override
public void onBind(int pos, Text item) {
tv.setText(item.title);
//此處所有的資料和事件型別通過setTag傳出
tv.setTag(item.eventId);
tv.setOnClickListener(onClickListener);
}
@Override
public void onUnBind() {
}
@Override
public void injectCallback(View.OnClickListener onClickListener) {
this.onClickListener = onClickListener;
}
}
複製程式碼
仿照依賴注入的思想,只不過程式碼侵入性沒有那麼強,當然只能在onBind的時候才能繫結,建構函式的時候,事件中心物件還沒有注入進來。
-
- ViewHolder實現InjectCallback介面,在onBind生命週期就可以拿到事件中心物件。
-
- 通過View.setTag,將事件型別(int型等,唯一性)和相關需要的資料傳出。
事件中心的思想就是:ViewHolder單純的只傳遞事件,完全由資料驅動事件,View不感知事件型別,也就是說,這個ViewHolder的事件是可變的!
MVP的拆分
關於MVP是什麼這裡就不多講了,這裡講一講MVP的拆分,常規的MVP我們經常做的就是一個P完成所有的邏輯,但是這時帶來的問題就時P層過於大,這時我的理解就是對P進行拆分,具體拆分的粒度要根據不同的業務場景來區分(這個就比較考驗開發者對於設計模式的理解)。而ViewHolder自身可以完成一套MVP體系,想一想,當一個特殊的樓層,涉及複雜的業務邏輯,這時完全將這個樓層拆分成MVP模式,這時其他頁面需要使用的時候,只需要new對應的MVP即可。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
slotContext = new ToolKitBuilder<>(this, mData).build();
//1.註冊對應的邏輯類
slotContext.registerLogic(new CommonLogic(slotContext));
...
}
@ComponentType(value = ComponentId.TEXT_IMG)
//2.註解對應的邏輯類
@ILogic(CommonLogic.class)
//3.實現IPresenterBind介面
public class TextImgLayout extends LinearLayout implements IComponentBind<CommonModel>,IPresenterBind<CommonLogic> {
private View root;
private TextView tvInfo;
private CommonLogic logic;
...
@Override
public void onBind(int pos, CommonModel item) {
tvInfo.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (logic != null) {
//對應的P,處理業務邏輯
logic.pageTransfer();
}
}
});
}
...
@Override
public void injectPresenter(CommonLogic commonLogic) {
this.logic = commonLogic;
}
}
複製程式碼
對應的需要三步:
-
slotContext.registerLogic(IPresenter presener)
,這裡IPresenter只是一個空介面,用於表明這是一個邏輯層的類。
-
- 在ViewHolder利用@ILogic註解對應的P的Class
-
- ViewHolder實現IPresenterBind介面,注入註冊給SlotContext對應的Presenter.
生命週期感知
無論是Presenter還是任何其他類,當脫離的Activity,對於生命週期的感知時非常重要的,所以SlotContext提供的有兩個API
pushLife(ILifeCycle lifeCycle)
pushGC(IGC gc)
複製程式碼
需要感知生命週期,或者僅僅感知OnDestroy的類,只需實現相應的介面,並利用api註冊觀察者即可。
MIX模式,多樓層打通
對於多樓層打通,我們需要利用ToolKitBuilder實現IMixStrategy策略。
public interface IMixStrategy<T> {
//通過type得到真正的對映表中的ComponentId
int getComponentId(int type);
//通過Type確定對應的對映表
Class<?> attachClass(int type);
//傳入ViewHolder的Bind中的實體類
Object getBindItem(int pos, T t);
}
複製程式碼
具體方案->Wiki
ToolKitBuilder的建構函式
public ToolKitBuilder(Context context, List<T> data)
public ToolKitBuilder(Context context)
複製程式碼
ToolKitBuilder的API
方法名 | 描述 | 備註 |
---|---|---|
setData(List data) | 設定繫結的資料集 | 空物件,對應的構造的size=0 |
setModerBinder(IModerBinder moderBinder) | 處理多樣式時Model對應的Type | 處理優先順序優先於HandlerType和註解BindType |
setEventCenter(View.OnClickListener onClickListener) | 設定事件中心 | ViewHolder的事件繫結後都會回撥到這個事件中心 |
setComponentFactory(CustomFactory componentFactory) | 設定自定義建立ViewHolder的工廠 | 可以自定義建立三種型別 |
setMixStrategy(IMixStrategy mixStrategy) | 設定混合模式處理策略 | 多人樓層打通 |
attachRule(Class<?> clazz) | 註冊樓層對映表 | 個人模式和混合模式 |
SlotContext build() | 構建出SlotContext物件 |
SlotContext的建構函式
public SlotContext(Context context, List<T> data)
public SlotContext(ToolKitBuilder<T> builder)
複製程式碼
SlotContext的API
方法名 | 描述 | 備註 |
---|---|---|
Context getContext() | 獲取Context物件 | |
setData(List data) | 繫結資料集 | 這裡不會重新整理資料,僅僅是設定 |
notifyDataSetChanged() | 重新整理資料 | 只提供了全域性重新整理的方式,區域性重新整理可以通過獲取Adapter使用 |
attachRule(Class<?> clazz) | 註冊樓層對映表 | 個人模式和混合模式 |
registerLogic(IPresent logic) | 註冊Presenter邏輯 | 可註冊多個,需要實現IPresenter空介面 |
obtainLogic(Class<?> clazz) | 獲取對應註冊的Presenter例項 | 以class作為key |
bind(RecyclerView rcy) | 繫結Adapter | 會重新建立Adapter並繫結 |
RecyclerView.Adapter getAdapter() | 獲取Adapter | |
pushLife(ILifeCycle lifeCycle) | 註冊任何物件監聽生命週期 | 實現ILifeCycler介面 |
pushGC(IGC gc) | 監聽Destroy生命週期 |
更多擴充
更多使用方式詳見Wiki
專案原始碼解析
Python自動生成10000個java類使用APT註解後引發的問題
專案地址:EMvp
歡迎Star?
歡迎大家提issues提意見~