Aug 5

对RecyclerView进行结耦封装后的上层封装与其它

Lrdcq , 2016/08/05 14:42 , 程序 , 閱讀(2637) , Via 本站原創
在上文中我们已经将RecyclerView拆分称为了数个基本结构,其中仔细观察的话,最重要且最复杂的部分就是dataSource和adapter的相互结耦和交互了。而这两个元素拆分来看,dataSource已经是功能完善的现成的对象了,可以取来直接使用,而adapter则需要复杂的绑定操作才能完成配置。因此,我们首要要进行封装的说简单点就是一个adapterBuilder了。

AdapterBuilder

这个builder核心内容就是我们上文设计的特定的adapter的构造辅助类,整理一下,在这一层我们需要配置的东西包括:

1. itemAdapter,这是adapter的主要构成部分,我们需要分类型的为adapter设置itemAdapter。当然,具体的api是可以直接传入一个itemAdapter,或者用过布局id把builder转换为itemBuilder。
2. header/footer,这算是itemAdapter的特殊应用,因为它们两的item类型是已经预设好的并且在item内部作了特殊逻辑处理,因此需要单列出来。考虑到header/footer常用的使用场景,api提供的接口支持入参为view或者布局id。
3. typeHandler,和数据无关的类型转换逻辑可以直接通过adapter进行配置,当然,优先级是dataSource优先配置,对其没进行设置的数据再丢给adapter再判断一次。这样可以非常方便的从不同的维度设置item类型逻辑。

这样adapter基本层面的配置就均覆盖了。

ItemBuilder

针对每一个itemAdapter,我们就有了这个ItemBuilder。ItemBuilder需要构建的包括两方面,一个是item创建时进行界面初始化,hold住界面引用,同时还要描述holder的属性和样式;另一个是当item绑定时,把数据绑定到特定界面上去。因此,这个builder进行了如下的设计:

1. 构造器。每当构造一个itemAdapter,我们肯定是需要和一个item的布局和一个固定的数据类型进行绑定的,因此我们在初始化的时候就必须传入一个布局的id,并且在泛型类中定义好需要绑定的数据类型。
2. holder表现描述。在itemAdapter构造holder后,可为holder添加HolderBehavior。在这里因为只存在striped和selectable两种描述,直接作为配置的方法串在一起就可以了。
3. 数据绑定。这是ItemBuilder的核心部分了。我们通过view的id可以一个当个绑定操作的key-value绑定,我们显然可以清晰的知道我们有哪些界面需要holder,和哪些界面和数据需要对应。最基础的接口是这样两个:
public interface OnBinderData<OBJ, V extends View> {
    void bind(V view, OBJ data);
}
public interface OnBinderDataBack<OBJ, C> {
    C bind(OBJ data);
}

而ItemBuilder提供的绑定方法,从最基础的view抽象绑定,到针对特定类型的绑定,类似于这样:
public <VV extends View> XGItemBuilder<DATA> bind(int id, OnBinderData<DATA, VV> call) {
    binders.add(Pair.create(id, call));
    return this;
}
public XGItemBuilder<DATA> bindTextView(int id, OnBinderData<DATA, TextView> call) {
    return bind(id ,call);
}
public XGItemBuilder<DATA> bindText(int id, OnBinderDataBack<DATA, String> call) {
    return bindTextView(id, (view, data) -> view.setText(call.bind(data)));
}

我们为常用的界面元素对象编写了特定的bind方法,除了绑定元素,当然还包括绑定listener,绑定view状态(比如enable,focus,select等)。除了针对特定view绑定,还可以不传id,即是对根view(viewHolder.itemView)进行绑定。这些绑定操作能够覆盖绝大部分没有复杂交互的item的绑定操作。最后生成itemAdapter时,当然是结合之前完成的BindViewHolder进行的。最终生成itemAdapter的代码如下:
//计算出绑定的元素出现过的id
ArrayList<Integer> id = new ArrayList<>();
for (Pair<Integer, OnBinderData> pair : binders)
    if (pair.first != VIEW_ID_HOLDER_ROOT && !id.contains(pair.first))
        id.add(pair.first);
        
//把这个ArrayList转变为int[],并且在new BindViewHolder时塞进去
final int[] ids = new int[id.size()];
for (int i = 0; i < ids.length; i++)
    ids[i] = id.get(i);
    
//用BindViewHolder和泛型DATA完成itemAdapter构建
adapter = new XGAdapter.ItemAdapter<BindViewHolder, DATA>() {
    @Override
    public void onBindViewHolder(BindViewHolder holder, DATA data) {
        //遍历binders列表完成数据绑定
        for (Pair<Integer, OnBinderData> pair : binders)
            pair.second.bind(pair.first == VIEW_ID_HOLDER_ROOT ? holder.itemView : holder.get(pair.first), data);
    }

    @Override
    public BindViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new BindViewHolder(getLayoutInflater().inflate(layout, parent, false), ids);
    }
};

当然,实际的builder由于有两种生成形式(new buider和AdapterBuilder转换),因此返回值也有可能是一个ItemAdapter实例或者会到AdapterBuilder对象中。

其它元素暂时不需要builder

1. 由于设计中使用我们自己写的recycerView是一个可选项,因此recycerView层次是不需要builder的,还是推进大家按照一般的方法配置即可,反正正常使用条件下需要配的东西并不多。
2. dataSource虽然重要,但是并没有什么好配置的,要么重写一个,要么选择一个恰当的dataSource,而且一般情况下SimpleDataSource完全可以解决问题,因此也不需要builder来构建了。

最后,一个最简单的列表的构建代码大概如下:
recyclerView = (RecyclerView) findViewById(R.id.list_act_rv);
recyclerView.setLayoutManager(new LinearLayoutManager(this));

adapter = XGAdapter.build()
    .itemAdapter(XGAdapter
        .<Data>item(R.layout.item_list_with_redtext)
        .bindText(R.id.list_item_tv, data -> data.text)
        .build())
    .build();
recyclerView.setAdapter(adapter);
    
adapter.setDataSource(dataSource = new SimpleDataSource<>(adapter));

dataSource.setData(list);

可以看到很清晰的4步

1. 获取RecyclerView,进行基本配置(主要是必须的LayoutManager和可选的ItemDecoration)
2. 使用builder构造出adapter并配置到RecyclerView中
3. 初始化DataSource并于其和adapter相互绑定
4. 往中塞数据。

这样一个完整的配置链已经足以清晰明了的完成列表展示逻辑的描述,且可以方便的和实际业务逻辑和代码进行耦和。

在举一个例子,之前在安卓中写起来非常麻烦的section样式还带pinned带header带footer的列表写起来也不过如此了:
recyclerView = (XGRecyclerView) findViewById(R.id.list_act_rv);
recyclerView.addItemDecoration(new PinnedSectionItemDecoration());

//添加header
TextView ntv = new TextView(this);
//...

//添加footer
TextView ntv2 = new TextView(this);
//...

//添加adapter
adapter = XGAdapter.with(recyclerView.adapter())
    .header(ntv)
    .footer(ntv2)
    .itemAdapter(XGAdapter
        .<Data>item(R.layout.item_list_with_redtext)
        .bindText(R.id.list_item_tv, data -> data.title)
        .build(),SectionDataSource.TYPE_SECTION)
    .itemAdapter(XGAdapter
        .<SubData>item(R.layout.item_list_with_yellowtext)
        .bindText(R.id.list_item_tv, data -> data.text)
        .build())
    .build();

//添加数据源
adapter.setDataSource(dataSource = new SectionDataSource<>(adapter,data -> data.sub));

dataSource.setSectionData(list);

编写过程中遇到的其它问题

由于是builder所以编写的时候泛型泛滥并且还重构过一次,遇到了一些实际实现过程中需要注意的问题。

1. Builder层次设计。在Builder进行复杂设置的过程中,我们往往会设置第二层甚至第三层builder来满足特定配置的需要,比如上面的设计中就出现了两层,即adapterbuilder和itembuilder。事实上在一开始的设计中,还有第三层builder,即holderbuilder专门用来处理holder的样式的,后来经过综合考虑简化掉了。另外一开始是和dataSource一起build出来的,因此当时的构建过程大概是:
                           HolderBuilder
                          /             \
               ItemBuilder -           - ItemBuilder
              /                                     \
AdapterBuilder -                                   - AdapterBuilder - DataSourceBuilder

这样从左至右的构建的。因此,简化的目的,最重要的还是要让api便于理解和使用,只有把过于复杂的概念进行分离,并且可选性的组合,才更合理。同时遇到还有一个问题是多层builder到底是使用链式递增还是参数式递增呢?链式递增可以保障编写的流畅性,但过于复杂的操作链层次感不住代码不够清晰。而参数式虽然会打破链条,但一个是有缩进代码结构也一目了然,最后是推荐第二种。当然由于最终builder设计简单,因此两种都支持了。

2. Builder泛型切换。在builder设计过程中,为了满足一些特定的代码设计,需要通过泛型切换在实现。

一个是需要builder返回特定类型的实例,在一开始设计DataSourceBuilder过程中,是默认builder返回的simpleDataSource实例,通过配置loadable()之后build出来就是LoadableDataSource实例了,以此类推。这个的实现方式是隐藏一个泛型作为返回值并且进行传递。简单的代码示例如下:
public static class Builder<DATA,RETURN extends Type> {
    private int keepData;

    private Builder() {
    }

    private Builder(int keepData) {
        this.keepData = keepData;
    }

    public static <D> Builder<D,Type> create(){
        return new Builder<>();
    }

    public Builder<DATA,RETURN> normalAct(){
        return this;
    }

    public Builder<DATA,ChildType> child(){
        return new Builder<>(keepData);
    }
        
    public RETURN build(){
        if(xxx)
            return (RETURN)new Type();
        else
            return (RETURN)new SubType();
    }
}

其中3个地方比较关键,1个是类型泛型中有默认return的类型RETURN,但是并不是交给用户配置,而是在create静态方法中提供默认配置;其次,在特殊的转换方法(child())中,重新new一个builder对象,把已配置数据拷贝过去,返回return泛型为需求类型;最后,在build方法中,用过相应的配置逻辑选择要构建的实例类型,并且需要通过(RETURN)进行强制转型并且返回,要注意的是这里(RETURN)强转是一个危险的强转,需要对代码完全清晰把握才这样做。

另外还尝试了一种设计,比如builderA正在操作,通过一个方法将当前操作链转换为builderB其extend于builderA,这时就可以同时使用builderA和builderB的方法,因此一旦在builderB中调用起父类的方法,操作链就会回到builderA了。那么怎么解决呢,我们想到用泛型可不可以解决呢,即每次builder进行链式操作的返回值,就是一个泛型,且是该builder本身。本着这个想法,我们编写了如下代码:
public static class BuilderA<DATA,BUILDER extends BuilderA> {
    public static <D> BuilderA<D,BuilderA> create(){
        return new BuilderA<>();
    }

    public BUILDER normalAct1(){
        return (BUILDER)this;
    }

    public BuilderB<DATA,BuilderB> toBuilderB(){
        return new BuilderB<>();
    }
}

public static class BuilderB<DATA,BUILDER extends BuilderA> extends BuilderA<DATA,BUILDER>{
    public BUILDER normalAct2(){
        return (BUILDER)this;
    }
}

也包含2个要注意的点,即builder的泛型即builder本身,无论是create方法还是类型转换方法,同时每个正常的操作方法return this的时候需要将this强转为BUILDER。不过测试一下就知道,这样写的话,经过两次调用,builder就变成了Object无法操作了,究其原因,就会发现,每次强转都把this,类型为BuilderA<DATA,BUILDER extends BuilderA>,强转为了BUILDER,即泛型减少了一层,到最后的时候,自然就转为了Object了。解决方法呢?经过各种姿势的测试,并没有合适的解决方法,只有在create和转换类型的时候多写一些泛型层次用于消耗,虽然很搞笑但是也算是解决方式了。最后create看起来是这样的:
public static <D> BuilderA<D,BuilderA<D,BuilderA<D,BuilderA<D,BuilderA<D,BuilderA<D,BuilderA<D,BuilderA<D,BuilderA<D,BuilderA<D,BuilderA<D,BuilderA<D,BuilderA<D,BuilderA<D,BuilderA>>>>>>>>>>>>>> create(){
    return new BuilderA<>();
}

这里有15层泛型,可以支持15次正常的链式操作,toBuilderB同理,考虑到本来提供的方法数目,设定稍比方法数目略大的层次的泛型基本就够用了,实在不行用相同的方式写一个续命操作函数不就好啦。当然,最后经过讨论和优化,没有采用这种方法,不过这也算是一种可行的尝试吧。
logo