android一种可配置的公共列表页封装方式

app项目中列表页面的封装实现

客户端项目常常遇到很多列表页面,而这些列表页面都很相似,无论逻辑还是ui,所以封装一个公用的列表页面可以极大地减轻开发负担,提高程序的可维护性。

列表页面的需求整理

  • 列表展示(必须支持)
  • 空页面展示(非必须)
  • 下拉刷新(非必须)
  • 上拉加载更多(非必须)
  • header展示(非必须)
  • 分割线(非必须)

上面就是一般情况下列表页面的核心需求,根据需求整理其公共逻辑和差异逻辑。

需求 公共逻辑 差异逻辑
列表展示 把列表每一个条目抽象成ItemView接口,把要展示的数据抽象成ViewBean接口 1.ui不同。2.点击事件处理不同
空白页面展示 空页面是一个view 1.就wdg来说,空页面有俩个元素—icon,和提示文字。其中只有提示文字有变化。
下拉刷新 下拉刷新页面(重新请求网络接口)
上拉加载更多 上拉加载更多 (请求分页数据)
header展示 header是一个view 1.ui不同。 2.点击事件不同。3.其它复杂交互
decoration 分割线展示 1.左右间距。2.颜色。3.粗细。4.挨着header的分割线和底部分割线的绘制控制

实现方式

  • 所有特性可配置

上面分析了列表页需要支持的特性,但是实际需求可能是某几个特性的组合,或者全部支持。这样把所有特性都做成可配置的,那么就比较灵活。

所以定义了一个ListFragmentConfig类,其成员变量为支持的特性和其它变量(列表元素的布局,header的布局等等)

  • 上下拉的支持

下拉刷新和上拉支持更多用开源库RefreshLayout支持。在view层上下拉的操作是一样的,变化的是数据接口以及数据不同,这里委托给Presenter层来处理。

  • 空页面和header的支持

header的支持—— 使用WrapperedAdapter实现,本质上还是RecyclerView.Adapter,每一个header都是一种类型的ItemView。header除了展示ui,还需要对数据进行一些操作,这里需要对Presenter进行操作。

  • 列表展示

列表展示最主要的变化是数据不同每一项展示的ui不同,另外点击事件的处理不同,或许还有更复杂的逻辑处理。对于这些变化我们不在公共列表页处理,委托给其它view层元素(headerview,parentFragment,Activity等等)

根据上述分析,抽象出几个接口

  1. ItemView—— 表示列表中的一个ui元素
  2. ViewBean—— 表示列表元素要渲染到界面的数据
  3. UsePresenter—— 表示要使用Presenter
  4. 定义一个公共适配器把ItemViewViewBean联系起来
public class CommonRecyclerViewAdapter extends RecyclerView.Adapter>

几个接口的具体代码如下

public interface ItemView {
    void bindData(T bean);
}

public interface ViewBean extends Serializable{
}

public interface UsePresenter {
    default void attachPresenter(T presenter) {}
}
  • MVP架构

定义公共列表的Presenter层BaseListPresenter,抽象出其公共逻辑—— 设置请求参数,刷新,加载更多 ,消息总线订阅和解订阅。

public interface BaseListPresenter extends BasePresenter {
    
    /**
     * 刷新数据
     */
    default void refreshRequest(){};
    
    /**
     * 加载更多数据
     */
    default void loadMore(){};

    /**
     * 设置额外的参数
     */
    default void setExtraParams(Map extraParams){}

    default void registBus(){
        if (!EventBus.getDefault().isRegistered(this)) {
            EventBus.getDefault().register(this);
        }
    }

    default void unRegisterBus(){
        if (EventBus.getDefault().isRegistered(this)) {
            EventBus.getDefault().unregister(this);
        }
    }
}

定义公共列表的View层BaseListView,抽象出其公共逻辑—— 展示数据列表到ui上,根据数据渲染header,展示空页面,清空列表。

public interface BaseListView extends BaseView {
    void renderListData(List viewBeans, boolean isRefresh, boolean hasMore);

    default void renderHeaderView(ViewBean viewBean){}

    default void showEmptyView(){};

    default void clearList(){};
}

上述就是实现层面的一些抽象设计。具体代码和其它细节处理可查看wdg项目相关源码。

使用例子

以wdg项目交易页下部分的几个tab为例

        //持仓
        ListFragmentConfig config = ListFragmentConfig.newBuilder()
            .orientation(OrientationHelper.VERTICAL)
            .viewBeanRes(R.layout.wdg_itemview_trade_hold)
            .listEmptyRes(R.layout.wdg_trade_empty_no_record)
            .isSupportRefresh(false)
            .isSupportLoadMore(false)
            .isAutoSubsribe(false)
            .isSupportEventBus(true)
            .listHeaderRes(R.layout.wdgapp_header_trade_hold)
            .isSupportEmpty(true)
            .aClass(WdgTradeHoldPresenter.class)
            .extraParams(extraParams)
            .emptyMsg(getString(R.string.wdgapp_no_holds))
            .build();
        WdgListFragment listFragment = WdgListFragment.newInstance(config);

        //挂单
        config = ListFragmentConfig.newBuilder()
            .orientation(OrientationHelper.VERTICAL)
            .viewBeanRes(R.layout.wdg_itemview_trade_order)
            .listEmptyRes(R.layout.wdg_trade_empty_no_record)
            .isSupportEmpty(true)
            .isSupportRefresh(false)
            .isSupportEventBus(true)
            .isAutoSubsribe(false)
            .isSupportLoadMore(false)
            .extraParams(extraParams)
            .aClass(WdgTradePendingOrderPresenter.class)
            .build();
        WdgListFragment pendingOrderFragment = WdgListFragment.newInstance(config);

        //委托
        config = ListFragmentConfig.newBuilder().orientation(OrientationHelper.VERTICAL)
            .viewBeanRes(R.layout.wdg_itemview_trade_order)
            .listEmptyRes(R.layout.wdg_trade_empty_no_record)
            .isSupportEmpty(true)
            .isSupportRefresh(false)
            .isSupportLoadMore(true)
            .extraParams(extraParams)
            .aClass(WdgTradeEntrustOrderPresenter.class)
            .build();
        WdgListFragment entrustFragment = WdgListFragment.newInstance(config);

        //成交
        config = ListFragmentConfig.newBuilder().orientation(OrientationHelper.VERTICAL)
            .viewBeanRes(R.layout.wdg_itemview_trade_deal)
            .listEmptyRes(R.layout.wdg_trade_empty_no_record)
            .isSupportEmpty(true)
            .isSupportRefresh(false)
            .isSupportLoadMore(false)
            .extraParams(extraParams)
            .aClass(WdgTradeDealPresenter.class)
            .build();
        WdgListFragment dealFragment = WdgListFragment.newInstance(config);

我们只需要根据需求配置即可,不关心列表页的具体实现。至于其它的变化我们可按如下一套规范来开发,以交易页持仓列表为例

  • 数据层的实现—— 根据接口新建ViewBean的实现类(列表元素要展示的数据bean)。

    public static class Hold implements ViewBean {
            @SerializedName("currency_id")
            public String currencyId;
    
            @SerializedName("currency_mark")
            public String currencyMark;
    
            public String num;
    
            public String forzennum;
    
            @SerializedName("currency_all_money")
            public String currencyAllMoney;
    
            public String price;
    }
    
  • 数据请求的实现—— 定义一个BaseListPresenter的实现类,实现其“刷新”/“加载更多”逻辑,根据需求实现其它逻辑。

    public class WdgTradeHoldPresenter implements BaseListPresenter {
    
        private BaseListView mView;
        private String mCurrencyId;
        private CompositeDisposable mComposite;
    
        public WdgTradeHoldPresenter(BaseListView mView) {
            this.mView = mView;
        }
    
        @Override
        public void setExtraParams(Map extraParams) {
            if (extraParams == null) {
                return;
            }
            mCurrencyId = (String) extraParams.get("currencyId");
        }
    
        @Override
        public void subscribe() {
            registBus();
            visible();
        }
    
        private void visible() {
            if (mComposite == null || mComposite.isDisposed()) {
                mComposite = new CompositeDisposable();
            }
            Disposable timerDispose = Flowable.interval(0, 3, TimeUnit.SECONDS)
                    .onBackpressureDrop()
                    .observeOn(SchedulerProvider.getInstance().ui())
                    .subscribe(aLong -> {
                        //请求持仓数据
                         Disposable dispose = NetworkManager.getInstance().getApiInterface()
                                .getTradeHold("")
                                .compose(SchedulerProvider.getInstance().applySchedulers())
                                .subscribe(response -> {
                                    mView.renderListData(response.getChichang(), true, false);
                                }, throwable -> {});
                        mComposite.add(dispose);
                    }, throwable -> {});
            mComposite.add(timerDispose);
        }
    
        @Override
        public void unsubscribe() {
            unRegisterBus();
            invisible();
        }
    
        private void invisible() {
            disPose(mComposite);
        }
    
        @Subscribe(threadMode = ThreadMode.MAIN)
        public void onEventHoldResponse(WdgTradeHoldResponse response) {
            mView.renderListData(response.getChichang(), true, false);
        }
    
        @Subscribe(threadMode = ThreadMode.MAIN)
        public void onEventSubscribe(WdgSubscribeEvent event) {
            if (event.isSubscribe()) {
                this.visible();
            } else {
                this.invisible();
            }
        }
    
        @Subscribe(threadMode = ThreadMode.MAIN)
        public void onEventCancelOrder(WdgCancelTradeEvent event) {
            refreshRequest();
        }
    
        @Subscribe(threadMode = ThreadMode.MAIN)
        public void onEventOrderSuccess(WdgOrderSuccessEvent event) {
            refreshRequest();
        }
    
        @Subscribe(threadMode = ThreadMode.MAIN)
        public void onEventTabChange(WdgTradeTabChangeEvent event) {
            if (event.pageIndex == 0) {
                this.visible();
            }
        }
    }
    
  • 根据设计图实现xml

    
    
    
        
    
        
    
        
    
        
    
        
    
        
    
        
    
    
    
  • 根据xml新建一个ItemView的实现类,实现其"绑定数据"逻辑。

    public class WdgTradeHoldRvItemView extends ConstraintLayout implements ItemView {
        @BindView(R.id.tv_ico_name)
        TextView tvIcoName;
        @BindView(R.id.tv_quantity)
        TextView tvQuantity;
        @BindView(R.id.tv_frozen)
        TextView tvFrozen;
        @BindView(R.id.tv_value)
        TextView tvValue;
        @BindView(R.id.tv_price)
        TextView tvPrice;
    
        public WdgTradeHoldRvItemView(Context context) {
            super(context);
        }
    
        public WdgTradeHoldRvItemView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public WdgTradeHoldRvItemView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        @Override
        protected void onFinishInflate() {
            super.onFinishInflate();
            ButterKnife.bind(this);
        }
    
        @Override
        public void bindData(WdgTradeHoldResponse.Hold bean) {
            if (bean == null) {
                return;
            }
            tvIcoName.setText(bean.getCurrencyMark());
            tvQuantity.setText(FormatUtil.formatWdgQuantity(bean.getNum()));
            tvFrozen.setText(FormatUtil.formatWdgQuantity(bean.getForzennum()));
            tvValue.setText(bean.getCurrencyAllMoney());
            tvPrice.setText(bean.getPrice());
        }
    }
    
  • 最后把这些变化和需求所要支持的特性配置,然后WdgListFragment根据这些配置构建列表页

    //持仓
            ListFragmentConfig config = ListFragmentConfig.newBuilder()
                .orientation(OrientationHelper.VERTICAL)
                .viewBeanRes(R.layout.wdg_itemview_trade_hold)
                .listEmptyRes(R.layout.wdg_trade_empty_no_record)
                .isSupportRefresh(false)
                .isSupportLoadMore(false)
                .isAutoSubsribe(false)
                .isSupportEventBus(true)
                .listHeaderRes(R.layout.wdgapp_header_trade_hold)
                .isSupportEmpty(true)
                .aClass(WdgTradeHoldPresenter.class)
                .extraParams(extraParams)
                .emptyMsg(getString(R.string.wdgapp_no_holds))
                .build();
            WdgListFragment listFragment = WdgListFragment.newInstance(config);
    

更多使用姿势

  • 点击事件的处理,实现CommonRecyclerViewAdapter.OnRecyclerViewItemClickListner接口

    @Override
        public void onItemClick(View view, RecyclerView rv, int position, WdgMarketListResponse.Currency data) {
            WdgCoinDetailActivity.start(getContext(), data.currency_id);
        }
    
  • ItemView中的多个控件的点击事件处理—— 重写点击事件,并把点击监听派发给需要处理的控件

    @Override
        public void setOnClickListener(@Nullable OnClickListener l) {
            tvCancel.setOnClickListener(l);
        }
    
  • header实现

    public class WdgIcoPropertyHeaderView extends ConstraintLayout implements ItemView {
    
        private TextView tvAll;
        private ImageView ivHide;
    
        private FrameLayout flDeposit;
        private FrameLayout flWithDraw;
    
        private WdgPropertyResponse mResponse;
    
        public WdgIcoPropertyHeaderView(Context context) {
            super(context);
        }
    
        public WdgIcoPropertyHeaderView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public WdgIcoPropertyHeaderView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        @Override
        protected void onFinishInflate() {
            super.onFinishInflate();
            tvAll = findViewById(R.id.tv_property_content);
            ivHide = findViewById(R.id.iv_crypt_state);
            flDeposit = findViewById(R.id.fl_deposit);
            flWithDraw = findViewById(R.id.fl_withdraw);
    
            flDeposit.setOnClickListener(v -> {
                WdgChargeCoinActivity.start(getContext());
            });
    
            flWithDraw.setOnClickListener(v -> {
                Context context = getContext();
                context.startActivity(new Intent(context, WdgWithdrawCoinActivity.class));
            });
    
            ivHide.setOnClickListener(v -> {
                boolean showMoney = SPUtils.getInstance().getBoolean(WdgCons.SP_KEY_SHOW_MONEY, true);
                showMoney = !showMoney;
                SPUtils.getInstance().put(WdgCons.SP_KEY_SHOW_MONEY, showMoney);
                updateMoneyAboutView(showMoney);
                RecyclerView rv = getParentRecyclerView(this);
                if (rv != null && rv.getAdapter() != null) {
                    rv.getAdapter().notifyDataSetChanged();
                }
            });
        }
    
        @Override
        public void bindData(WdgPropertyResponse bean) {
            if (bean == null || !bean.isSucceed()) {
                return;
            }
            mResponse = bean;
            boolean showMoney = SPUtils.getInstance().getBoolean(WdgCons.SP_KEY_SHOW_MONEY, true);
            updateMoneyAboutView(showMoney);
        }
    
        private void updateMoneyAboutView(boolean showMoney) {
            if (mResponse == null) {
                return;
            }
            if (showMoney) {
                tvAll.setText(mResponse.getTotalMoneyRaw());
                ivHide.setImageResource(R.drawable.wdgapp_icon_property_state_show);
            } else {
                ivHide.setImageResource(R.drawable.wdgapp_icon_property_state_hide);
                tvAll.setText(WdgPropertyFragment.CRYPT_SYMBOLS);
            }
        }
    
        private static RecyclerView getParentRecyclerView(@Nullable View view) {
            if (view == null) {
                return null;
            }
            ViewParent parent = view.getParent();
            if (parent instanceof RecyclerView) {
                return (RecyclerView) parent;
            } else if (parent instanceof View) {
                return getParentRecyclerView((View) parent);
            } else {
                return null;
            }
        }
    }
    

总结

本文整理了公共列表页的需求,设计和实现过程,使用的例子,以及使用规范。当然在实现细节和需求满足上还有很多瑕疵,希望大家共同完善,在保留接口不变的情况下提出更好的代码实现。尽管这个小框架不太完善,但优点也是显而易见的:

  • 把类似需求的变化项分解成清晰的互不相干的粒度进行处理,把公共的逻辑统一处理,提供更清晰的代码实现思维,减少出错的机率
  • 更好的维护性—— 假如需要修改空页面的样式,我们可以统一处理,而不必修改多处代码。对于特殊的空页面,框架也支持自定义布局设置进去/或者自己控制header进行show和hide的操作。

后续支持的特性

  • 灵活的分割线支持
    列表的参数类是ListFragmentConfig,分割线的参数类是DecorationConfig.如下所示
 DecorationConfig implements Serializable {
     int dividerHeight;          //分割线的高度
    float leftOffset;              //分割线离左边框的距离
    float rightOffset;           //分割线离右边框的距离
    int dividerColorRes;    //分割线的颜色
    int style;                     //分割线的风格,比如头部不含分割线,从第二个view开始绘制分割线等
    ...
}

 // 分割线的使用
  DecorationConfig decorationConfig=new DecorationConfig.Builder()
                .dividerHeight(1)
                .leftOffset(0)
                .rightOffset(0)
                .dividerColorRes(R.color.wdgapp_color_eeeeee)
                .style(WdgRVItemSplitDecoration.STYLE_NO_FIRST_DIVIDER)
                .build();

        config = ListFragmentConfig.newBuilder().orientation(OrientationHelper.VERTICAL)
            .viewBeanRes(R.layout.wdg_itemview_trade_order)
            .listEmptyRes(R.layout.wdg_trade_empty_no_record)
            .isSupportEmpty(true)
            .isSupportRefresh(false)
            .isSupportLoadMore(true)
            .decorationConfig(decorationConfig)     //分割线设置
            .extraParams(extraParams)
            .aClass(WdgTradeEntrustOrderPresenter.class)
            .build();
        WdgListFragment entrustFragment = WdgListFragment.newInstance(config);

你可能感兴趣的:(android一种可配置的公共列表页封装方式)