基於AOP的一種RecyclerView複雜樓層開發框架,支援元件化,全域性樓層打通,MVP等高擴充性功能

Drowning Coder發表於2018-11-14

前言

RecyclerView作為Google替代ListView的一個元件,其強大的擴充性和效能,現在已經成為無數App核心頁面的主體框架。RecyclerView的開發模式一般來說都是多Type型別的ViewHolder——後面就稱為樓層(感覺很形象)。但是使用多了,許多問題就暴露出來了,經常考慮有這麼幾個問題:

    1. 如何更便捷的使用Adapter和ViewHolder的開發模式?
    1. 如何和他人的樓層做到樓層的複用?
    1. 如何做到全域性樓層的打通?
    1. 樓層本身如何做到邏輯閉合,做到MVP的元件化模式?

功能特性

  • 基於編譯期註解,不影響效能
  • 使用簡單,樓層耦合度低
  • 程式碼侵入性低
  • 支援全域性樓層打通,多人樓層打通
  • 樓層支援點對點MVP模式
  • 事件中心模式,樓層只是事件的傳遞者。
  • 生命週期監聽,支援邏輯的生命週期感知。
  • 豐富的API,支援多方面擴充。
  • 提供元件化工程使用方案
  • 不用每次再寫Adapter了~

專案地址

EMvp

歡迎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中尋找自己樓層對應的佈局檔案。(熟悉的人應該都懂) 注意:

  1. value:樓層的唯一標示,int型
  2. layout:樓層的佈局檔案
  3. 繼承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的時候才能繫結,建構函式的時候,事件中心物件還沒有注入進來。

    1. ViewHolder實現InjectCallback介面,在onBind生命週期就可以拿到事件中心物件。
    1. 通過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;
    }
}
複製程式碼

對應的需要三步:

    1. slotContext.registerLogic(IPresenter presener),這裡IPresenter只是一個空介面,用於表明這是一個邏輯層的類。
    1. 在ViewHolder利用@ILogic註解對應的P的Class
    1. 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提意見~

相關文章