HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

如何把 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:001377

评论

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

焱融科技年度报告如约而至,如 Beijing 初雪

焱融科技

云计算 分布式 云原生 高性能 文件存储

微博评论高性能高可用计算架构

Evan

PHP 遇见 Serverless,帮你解决这些痛点!

Serverless Devs

模块五课程作业

李晓笛

一起看看MySQL中的隐藏列

华为云开发者联盟

MySQL 事务 主键 唯一索引 隐藏列

QuanXiang 2022 Q1 开源路线图

全象云低代码

开源 工作流 低代码 表单

SAE 最佳实践范本:助力视野数科进入云原生“快车道”

Serverless Devs

Serverless SAE

模块五作业

黄秀明

「架构实战营」

jar包与war包的部署

你?

ReactNative进阶(二十七):createMaterialTopTabNavigator 顶部导航组件

No Silver Bullet

1月月更 ReactNative

TiDB 在国信证券海量数据高并发场景中的实践

PingCAP

可能是推荐系统最详细且简单的入门教程

JavaEdge

1月月更

微博评论架构设计

supermenG

架构师训练营 4 期

尚硅谷2022版Java课程体系,霸气来袭

@零度

Java 尚硅谷

模块五作业

Anlumina

#架构实战营

模块五 - 微博评论系统高性能高可用设计

圈圈gor

架构实战营 #架构实战营 「架构实战营」

使用 Lambda 表达式实现超强的排序功能

看山

Lambda java8 排序 1月月更

过年回家前,先签收这份“外挂”

龙智—DevSecOps解决方案

Atlassian Confluence 到期日提醒 confluence插件

迪斯尼《曼达洛人》艺术总监和制作设计师亲自揭秘背后的虚拟场景制作

龙智—DevSecOps解决方案

perforce 曼达洛人 西部世界 虚拟场景

模块五作业-微博评论高性能高可用计算架构

CH

架构实战营

Jira Software 年度总结:12个重要功能大放送!

龙智—DevSecOps解决方案

Jira Atlassian Atlassian Jira

java开发之Mybatis 快问快答

@零度

mybatis JAVA开发

MultipartFile与File的一些事

华为云开发者联盟

spring File 文件 MultipartFile 中转文件

『TDengine2021用户故事』征文活动进入投票阶段!

TDengine

tdengine

一文读懂HarmonyOS服务卡片怎么换肤

HarmonyOS开发者

HarmonyOS

【云图说】DDS读写两步走,带您领略只读节点的风采

华为云开发者联盟

数据库 节点 DDS 文档数据库 DDS读写

Spring 都在用的技术,你确定不过来看看?3️⃣

XiaoLin_Java

架构实战营模块五作业

lchx08

「架构实战营」

低代码实现探索(二十五)DDD,事件,指令

零道云-混合式低代码平台

如何快速实现离散企业全链路数字化管理

华为云开发者联盟

数据采集 华为云IoT 智能制造 黑湖科技 数据中枢

作业:架构实战营模块 5

Poplar89

「架构实战营」

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