NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

如何把 2000+ 行代码的详情页重构到不足 200 行

  • 2019-09-25
  • 本文字数:7591 字

    阅读完需:约 25 分钟

如何把2000+行代码的详情页重构到不足200行

最近在做重构,将一个 2000+行代码的详情页重构到不足 200 行。200 行的代码实现 2000+行代码的的业务逻辑?!当然不是了。我只是把原本耦合在一个页面的业务逻辑合理有效的拆分到多个模块中了。

1 背景

传统的 MVC 模式,业务代码都堆积在 Activity 中。随着业务需求的复杂,我们的页面文件代码量越来越多,类似房源详情这种可以达到 2K+行。


例如 Link 的新房详情页面,2182 行(单 onClick 回调方法占据了 265 行)。这样的代码存在着诸多问题,耦合性高,可读性差,维护成本非常大。本次采用新的架构方法(MVP+模块化),可以有效的解决以上问题。MVP 模式解耦合。模块化职责分明,大大减少单文件代码量,有效提升可读性,降低维护成本。

2 架构方案

基于 MVP 的设计模式,组合模块化(Part&ViewPart)。

3 方案概述

3.1 MVP

MVP 模式这里不多说,与传统的 MVP 模式不同的是,我们允许一个页面可以存在多个 P,多个 V,一个 P 可以对应多个 V。这样是为了我们每个模块可以完成自己的职责。P 层由 Activity 统一管理。当然如果一个 PV 独立于页面(且有复用性),则可以独立出这个 PV。

3.2 模块化

模块化:简单的讲就是讲业务拆分为多个模块,每个模块尽可能独立的负责自己的逻辑处理与 UI 展示。这样以来就实现了职责分明,并且大大减少了主 Activity 或 Fragment 的代码量,而且维护起来也比较方便。

3.2.1 模块化的方式

我们使用 Part&ViewPart 来实现模块化。

3.2.2 Part

Part 是我们一个页面的大容器中的每个模块,我们称之为 Part.这里的大容器就是我们页面的流式容器,Part 要求对应的大容器必须是 LinearLayout。


我们提供一个 BasePart 抽象类,所有的 Part 都继承该类。我们先看一下 BasePart 的代码:


  1/**  2 * 模块化基类<br/>  3 * 必须满足以下条件:<br/>  4 * 1.父容器必须是LinearLayout<br/>  5 * 2.父容器的所有view都必须是通过Part添加的<br/>  6 * 3.必须通过@#query方法创建part<br/>  7 * 4.View添加位置必须固定且已知,暂不支持动态权重<br/>  8 */  9public abstract class BasePart<T> { 10  private Context mContext; 11  private LayoutInflater mInflate; 12  private View mView; 13  private ViewGroup mParent; 14  private T mData; 15  private String tag; 16 17  public static <D extends BasePart> D query(LinearLayout parent, Class<D> clazz) { 18    return query(parent, clazz, null); 19  } 20 21  public static <D extends BasePart> D query(LinearLayout parent, Class<D> clazz, String tag) { 22    BasePart target; 23    if (TextUtils.isEmpty(tag)) { 24      tag = clazz.getSimpleName(); 25    } 26    // 先根据tag从parent中取,如果没找到就创建。 27    // 这里不check数据,调用bindData方法后再处理数据,回调onBindData 28    target = findPartByTag(parent, tag); 29    if (target == null) { 30      target = createPart(clazz); 31      target.mParent = parent; 32      target.mContext = parent.getContext(); 33      target.mInflate = LayoutInflater.from(target.mContext); 34      target.tag = tag; 35    } 36    return (D) target; 37  } 38 39  public void bindData(T data) { 40    mData = data; 41 42    LinkedHashMap<String, Boolean> mStateMap = (LinkedHashMap) mParent.getTag(R.id.part_state); 43    if (mStateMap == null) { 44      mStateMap = new LinkedHashMap<>(); 45      mParent.setTag(R.id.part_state, mStateMap); 46    } 47 48    if (isValid(data)) { 49      if (mView == null) { 50        mView = onCreateView(); 51        ButterKnife.bind(this, mView); 52        mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { 53          @Override 54          public void onViewAttachedToWindow(View v) { 55 56          } 57 58          @Override 59          public void onViewDetachedFromWindow(View v) { 60            onDestroyView(); 61          } 62        }); 63        // fixme 只有数据有效的part才会存储到view中 64        mView.setTag(R.id.part_cache, this); 65        // 把这个对象和view绑定 66        init(mView); 67        // 如果当前存储的数据中不包含这个tag,那么就直接add 68        if (!mStateMap.containsKey(tag)) { 69          mParent.addView(mView); 70        } else { 71          mParent.addView(mView, findAddPosition(mStateMap, tag)); 72        } 73        mStateMap.put(tag, true); 74      } 75      onBindData(data); 76    } else { 77      if (mView != null) { 78        mParent.removeView(mView); 79      } 80      mStateMap.put(tag, false); 81    } 82  } 83 84  protected final View inflate(@LayoutRes int resource) { 85    return mInflate.inflate(resource, mParent, false); 86  } 87 88  /** 89   * 数据是否有效 90   * 91   * @param data data 92   * @return false will remove relative view 93   */ 94  protected abstract boolean isValid(T data); 95 96  protected abstract View onCreateView(); 97 98  protected void onDestroyView() { 99  }100101  /**102   * 执行在{@link #bindData(Object)}之后,{@link #onBindData(Object)}之前,{@link #isValid(Object)==true}且view没有初始化时才会回调103   */104  protected abstract void init(View view);105106  /**107   * 只用于子类实现,外界调用{@link #bindData(Object)}。108   * 该方法在{@link #isValid(Object) == true }时才回调109   *110   * @param data data111   */112  protected abstract void onBindData(T data);113114  private static <D extends BasePart> D createPart(Class<D> clazz) {115    try {116      return clazz.getConstructor().newInstance();117    } catch (Exception e) {118      throw new RuntimeException(e);119    }120  }121122  private int findAddPosition(LinkedHashMap<String, Boolean> hashMap, String tag) {123    // 查找指定tag之前的所有被添加的view的个数,作为add时候的index,必须保证该容器的所有view都是通过part添加的124    int count = 0;125    for (Map.Entry<String, Boolean> entry : hashMap.entrySet()) {126      if (entry.getKey().equals(tag)) return count;127      if (entry.getValue()) count++;128    }129    return count;130  }131132  /**133   * find target by tag,null if not exist134   *135   * @param tag tag136   */137  private static BasePart findPartByTag(ViewGroup parent, String tag) {138    for (int i = 0; i < parent.getChildCount(); i++) {139      View view = parent.getChildAt(i);140      BasePart part = (BasePart) view.getTag(R.id.part_cache);141      if (part.tag.equals(tag)) {142        return part;143      }144    }145    return null;146  }147148  public T getData() {149    return mData;150  }151152  protected Context getContext() {153    return mContext;154  }155156  public LayoutInflater getInflate() {157    return mInflate;158  }159160  protected View getView() {161    return mView;162  }163164  protected ViewGroup getParent() {165    return mParent;166  }167}
复制代码


简单描述下,BasePart 所做的事情:根据子类判断的数据是否有效,当数据有效时才会将子模块 view 添加到父容器中。这样无效的数据模块也就不会被添加进来,避免不必要的渲染浪费。添加进来的 view 会和对应的 part 做一个映射,并将 part 缓存下来,支持单独刷新某个 Part。


下面来看下 Part 的使用方式。


首先我们要实现一个 Part,BasePart 是个抽象类,必须实现 4 个抽象方法。


 1public class XxxPart extends BasePart<T>{ 2  @Override 3  protected boolean isValid(T data) { 4    return false; 5  } 6 7  @Override 8  protected View onCreateView() { 9    return null;10  }1112  @Override13  protected void init(View view) {1415  }1617  @Override18  protected void onBindData(T data) {1920  }21}
复制代码


  • isValid 用于判断数据是否有效,数据有效,我们的模块才会被加载,避免不必要的渲染

  • onCreateView 用于创建 View

  • init 用于初始化

  • onBindData 用于绑定数据


除了上面四个必须实现的方法,BasePart 还提供一个非抽象的方法,==onDestroyView()==,用于处理销毁逻辑,如 Handler,请求等。


接下来看一下,Part 是怎么加载、调用的。Part 的使用很简单,只需要调用 query 方法,并 bindData 就够了。query 方法返回 Part,因此支持模块间的相互调用及跨模块更新。


1public static <D extends BasePart> D query(LinearLayout parent, Class<D> clazz, String tag){...}
复制代码


简单分析一下。query 需要 3 个参数。


  • parent 即该模块所在的父容器,也就是我们前面说的页面大容器。

  • clazz 模块 Part 的 class 对象(是的,这里用的反射构建的 Part 的实例)

  • tag 每个 Part 都会有一个 tag 与之对应,为了我们缓存 part。如果没有设置,会默认取 class.simpleName

3.2.3 ViewPart

前面有说到我们这里的模块化分为 Part 和 ViewPart。这里的 ViewPart 其实就是一个 View。在非大容器中的模块,我们可以使用 ViewPart 的方式将其模块化。我们模块化的原则是尽量将各个模块的业务交由各个模块自己完成,职责分明,逻辑清晰。


简单介绍完这些概念之后,下面来具体看一下重构的过程。

4 重构过程

4.1 梳理逻辑模块

新房房源详情页大大小小加一起,一共 14 个业务逻辑。其中除引导图外,其他我们均采用模块化的方式处理。分享逻辑和底部 Bar 采用 ViewPart 的方式,其他采用 Part 的方式。HouseDetailActivity 采用 MVP 的模式。


  • 分享引导图

  • 分享逻辑

  • 楼盘信息模块

  • 一房一价模块

  • 其他信息模块

  • 竞对盘推荐模块

  • 激励政策 模块

  • 动态模块

  • 特价房模块

  • 客户规则模块

  • 佣金*规则模块

  • 楼盘销售特点模块

  • 户型模块

  • 底部 bar

4.2 主页面 MVP 架构

首先看一下,详情页整体的 MVP 契约类。


 1public interface HouseDetailContract { 2  interface Present extends BasePresenter<View> { 3    void getHouseResponse(String projectId); 4 5    void getShareResponse(String projectId); 6 7    void follow(String projectId, boolean follow); 8  } 910  interface View extends BaseView {11    void onGetHouseResult(@NonNull NewHouseResBlockDetailBean data);1213    void onUpdateStateLayout(boolean isSuccess, boolean netError);14  }1516  interface IFollowView extends IBaseView {17    /**18     * @param followed 是否已关注19     * @param error null表示成功,false表示失败20     */21    void updateFollowState(boolean followed, String error);22  }2324  interface IShareView extends IBaseView {25    void onShare(ShareDialog.ShareToThirdAppBean shareBean, ShareDialog.ShareToSmsBean smsBean);2627    void onGetShareResultFail(String error);28  }29}
复制代码


可以看到,这里一个 P,有多个 V。且逻辑中还有两个比较独立且被复用的 PV,抽离出来了,这里不再列举。简单看下主 Present 的实现。


 1public class HouseDetailPresent extends HttpPresenter<HouseDetailContract.View> 2  implements HouseDetailContract.Present { 3 4  private HouseDetailContract.IFollowView iFollowView; 5  private HouseDetailContract.IShareView iShareView; 6 7  public void attachFollowView(HouseDetailContract.IFollowView iFollowView) { 8    this.iFollowView = iFollowView; 9  }1011  public void attachShareView(HouseDetailContract.IShareView iShareView) {12    this.iShareView = iShareView;13  }1415  @Override16  public void getHouseResponse(String projectId) {17    enqueue(ApiClient.create(HouseApi.class).getHousesDetailResult(projectId),18      new SimpleCallback<Result<NewHouseResBlockDetailBean>>() {1920        @Override21        protected void onNetworkError(HttpCall<Result<NewHouseResBlockDetailBean>> call, Throwable t) {22          mView.onUpdateStateLayout(false, true);23        }2425        @Override26        public void onResponse(HttpCall<Result<NewHouseResBlockDetailBean>> call,27          Result<NewHouseResBlockDetailBean> entity) {28          if (hasData()) {29            mView.onGetHouseResult(entity.data);30            mView.onUpdateStateLayout(true, false);31          } else {32            mView.onUpdateStateLayout(false, false);33          }34        }35      });36  }3738  @Override39  public void follow(String projectId, final boolean follow) {40    enqueue(ApiClient.create(HouseApi.class).followResblock(projectId, follow ? 1 : 0),41      new SimpleCallback<Result>() {42        @Override43        public void onResponse(HttpCall<Result> call, Result entity) {44          if (isSuccess()) {45            iFollowView.updateFollowState(follow, null);46          } else {47            iFollowView.updateFollowState(follow,48              Result.getErrorMsg(entity, follow ? "关注失败" : "取消关注失败"));49          }50        }51      }, true); // 是否需要loading52  }5354  @Override55  public void getShareResponse(String projectId) {56    ...57  }58}
复制代码


主 Present 会绑定其他两个模块的 V,并完成对应的 P 层逻辑和 V 的回调。


再来看下我们的详情页,这里重点看下,获取到数据之后加载 part 的处理逻辑。


 1@Override 2  public void onGetHouseResult(@NonNull NewHouseResBlockDetailBean data) { 3    this.bean = data; 4    setTitle(data.name); 5    bottomPart.bindData(data); 6    sharePart.bindData(data.projectName); 7    initDMShare(); 8    BasePart.query(container, HouseDetailInfoPart.class).bindData(data); 9    BasePart.query(container, OnePricePart.class).bindData(data);10    BasePart.query(container, HouseDetailOtherInfoPart.class).bindData(data);11    BasePart.query(container, RecommendPart.class).bindData(data);12    BasePart.query(container, IncentivePolicyPart.class).bindData(data.incentivePolicy);13    BasePart.query(container, DynamicPart.class).bindData(data.newDynamic);14    BasePart.query(container, DiscountHousePart.class).bindData(data);15    BasePart.query(container, CustomerRulePart.class).bindData(data.customerRule);16    BasePart.query(container, CommissionRulePart.class).bindData(data.commissionRule);17    BasePart.query(container, SaleFeaturePart.class).bindData(data.saleFeature);18    BasePart.query(container, FramePart.class).bindData(data);19  }
复制代码


所有根据返回数据操作 UI 及逻辑都在这个方法里。其中 bottomPart 和 sharePart 就是两个普通的 View。


下面看一个简单的 Part。


 1public class IncentivePolicyPart extends BasePart<NewHouseResBlockDetailBean.IncentivePolicy> { 2 3  @BindView(R2.id.tv_encourage_policy_detail) 4  CommonTextView tvEncouragePolicyDetail; 5 6  @Override 7  protected boolean isValid(NewHouseResBlockDetailBean.IncentivePolicy data) { 8    return data != null && !TextUtils.isEmpty(data.excitationPolicy); 9  }1011  @Override12  protected View onCreateView() {13    return inflate(R.layout.lib_newhouse_detail_encourages_policy);14  }1516  @Override17  protected void init(View view) {18    view.setOnClickListener(new View.OnClickListener() {19      @Override20      public void onClick(View v) {21        if (!TextUtils.isEmpty(getData().excitationPolicyUrl)) {22          CommonWebActivity.startActivity(getContext(), getData().excitationPolicyUrl,23            getContext().getString(R.string.newhouse_jili_zhengce));24        }25      }26    });27  }2829  @Override30  protected void onBindData(NewHouseResBlockDetailBean.IncentivePolicy bean) {31    tvEncouragePolicyDetail.setText(bean.excitationPolicy);32  }33}
复制代码


View 创建和初始化,数据源的有效性判断和数据绑定,层次分明,方便维护。


ViewPart 的代码稍多一些,这里简单取一部分代码。


 1public class HouseDetailBottomPart extends LinearLayout implements HouseDetailContract.IFollowView { 2    ... // ButterKnife 初始化view 3 4    // 绑定present 和V 5    public void setPresent(HouseDetailPresent present) { 6        this.present = present; 7        present.attachFollowView(this); 8    } 910    // 绑定数据,更新UI11    public void bindData(NewHouseResBlockDetailBean bean) {12      ...13    }1415    @OnClick(R2.id.ll_attention)16    void onFollow() { // 关注/取消关注17        present.follow(bean.projectId, bean.isFollow == 0);18    }1920    ...2122}
复制代码


可以看到这里的 PartView 也用了 MVP,自身便是个 V。


到这里,我们已经大致看完了,Part 模式相关的全部内容。下面来总结下 Part 的优点和存在的不足。

5 优点

  • 大量减少主控制器(Activity or Fragment)代码量

  • 层次分明,解耦。各业务模块的业务由模块自身处理

  • 无效的模块将不会被加载,避免不必要的渲染

  • 支持模块局部刷新。某模块数据变化时,直接刷新该模块即可,不需要重建 view,不影响其他模块

  • 支持模块间的相互调用

6 不足

Part 的方式并不适用所有的页面,part 主要解决的是线性容器下多模块的场景


  • 父容器必须是 LinearLayout 且父容器的所有子 view 都必须是通过 Part 添加的

  • 必须通过 query 方法创建 part

  • View 添加位置必须固定且已知,暂不支持动态权重


作者介绍:


公台(企业代号名),目前负责贝壳找房新房 B 端 Android。


本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。


原文链接:


https://mp.weixin.qq.com/s/LLt8u5WVLxLrg_acMXLvEA


2019-09-25 08:001260

评论

发布
暂无评论
发现更多内容

【作业-02】产品思维和产品意识

西西里奇

产品经理

产品训练营·第二周作业 & 总结

tiu

产品经理训练营第二次作业

猫。

产品思维和产品意识-利益相关方学习总结

mas

作业二

hopeful

极客大学产品经理训练营

我的自学编程之路

IT蜗壳-Tango

七日更

产品经理训练营第二章作业(一)

新盛

抽奖助手stackholder

再见陛下

年底想跳槽?先想清楚自己要什么。

一笑

面试 工作 28天写作

产品利益相关方小结

skylar

产品思维和产品意识

王一凡

产品经理 产品经理训练营 极客大学产品经理训练营

产品经理-第二周作业

LLL777

共识算法的简单理解(二)

石君

28天写作 拜占庭容错 PBFT

利益相关方以及优先级

skylar

利益相关者练习

王一凡

产品经理 产品经理训练营 利益相关者

抽奖助手利益相关方和问题分析

June

产品经理训练营

关于利益相关者的作业

mas

第二次作业

Geek_d9dd01

Elasticsearch从入门到放弃:浅谈算分

Jackey

elasticsearch

产品经理训练营第二周总结

happy-黑皮

产品经理训练营

产品经理训练营 - 第二次作业

Geek_娴子

什么是视频?|视频号28天(19)

赵新龙

28天写作

第二周作业

正午看星星

极客大学产品经理训练营

第2章:产品思维总结

让时间说真话

产品经理

有了分身术也不能解决的问题「幻想短篇 19/28」

道伟

28天写作

作业2

YING꯭YING

第二/三周作业

🌴stolmpj

28天瞎写的第二百二十九天:存储过程的故事

树上

28天写作

产品经理的书籍清单【持续更新】

学习高手song轻松

产品

GNUCash 4: 我的使用经验

lidaobing

GNUCash 28天写作

利益相关方分析-公司及团队维度

梁媛

产品经理

如何把2000+行代码的详情页重构到不足200行_文化 & 方法_公台_InfoQ精选文章