Refactoring a 2000‑Line Android Detail Page to Under 200 Lines Using MVP and Modularization
This article describes how a large, tightly‑coupled Android detail page was refactored into a clean, modular MVP architecture that splits business logic into reusable Parts, reducing the file size from over 2000 lines to fewer than 200 while improving readability, maintainability, and rendering performance.
Background
In traditional MVC Android projects, business logic often accumulates inside an Activity , leading to massive files (e.g., a house‑detail page with 2,182 lines of code). Such monolithic pages suffer from high coupling, poor readability, and high maintenance cost.
Architecture Solution
The new solution adopts an MVP pattern combined with a modular approach called Part & ViewPart . MVP decouples the view from the presenter, while modularization splits the UI into independent modules (Parts) that each manage their own data and UI.
Part Concept
A Part is a lightweight module that extends the abstract BasePart . It must implement four abstract methods:
isValid(T data) – determines whether the data for this module is valid; only valid modules are added to the parent container.
onCreateView() – creates the module's View .
init(View view) – performs view initialization (e.g., setting listeners).
onBindData(T data) – binds the data to the UI.
Additionally, BasePart provides a non‑abstract onDestroyView() for cleanup.
BasePart Implementation
public abstract class BasePart
{
private Context mContext;
private LayoutInflater mInflate;
private View mView;
private ViewGroup mParent;
private T mData;
private String tag;
public static
D query(LinearLayout parent, Class
clazz) {
return query(parent, clazz, null);
}
public static
D query(LinearLayout parent, Class
clazz, String tag) {
BasePart target;
if (TextUtils.isEmpty(tag)) {
tag = clazz.getSimpleName();
}
target = findPartByTag(parent, tag);
if (target == null) {
target = createPart(clazz);
target.mParent = parent;
target.mContext = parent.getContext();
target.mInflate = LayoutInflater.from(target.mContext);
target.tag = tag;
}
return (D) target;
}
public void bindData(T data) {
mData = data;
LinkedHashMap
mStateMap = (LinkedHashMap) mParent.getTag(R.id.part_state);
if (mStateMap == null) {
mStateMap = new LinkedHashMap<>();
mParent.setTag(R.id.part_state, mStateMap);
}
if (isValid(data)) {
if (mView == null) {
mView = onCreateView();
ButterKnife.bind(this, mView);
mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override public void onViewAttachedToWindow(View v) {}
@Override public void onViewDetachedFromWindow(View v) { onDestroyView(); }
});
mView.setTag(R.id.part_cache, this);
init(mView);
if (!mStateMap.containsKey(tag)) {
mParent.addView(mView);
} else {
mParent.addView(mView, findAddPosition(mStateMap, tag));
}
mStateMap.put(tag, true);
}
onBindData(data);
} else {
if (mView != null) {
mParent.removeView(mView);
}
mStateMap.put(tag, false);
}
}
protected final View inflate(@LayoutRes int resource) {
return mInflate.inflate(resource, mParent, false);
}
protected abstract boolean isValid(T data);
protected abstract View onCreateView();
protected abstract void init(View view);
protected abstract void onBindData(T data);
protected void onDestroyView() {}
private static
D createPart(Class
clazz) {
try { return clazz.getConstructor().newInstance(); }
catch (Exception e) { throw new RuntimeException(e); }
}
private static int findAddPosition(LinkedHashMap
map, String tag) {
int count = 0;
for (Map.Entry
entry : map.entrySet()) {
if (entry.getKey().equals(tag)) return count;
if (entry.getValue()) count++;
}
return count;
}
private static BasePart findPartByTag(ViewGroup parent, String tag) {
for (int i = 0; i < parent.getChildCount(); i++) {
View view = parent.getChildAt(i);
BasePart part = (BasePart) view.getTag(R.id.part_cache);
if (part != null && part.tag.equals(tag)) return part;
}
return null;
}
public T getData() { return mData; }
protected Context getContext() { return mContext; }
protected LayoutInflater getInflate() { return mInflate; }
protected View getView() { return mView; }
protected ViewGroup getParent() { return mParent; }
}Using a Part
To create a concrete module, extend BasePart and implement the four abstract methods. Example:
public class XxxPart extends BasePart
{
@Override protected boolean isValid(T data) { return false; }
@Override protected View onCreateView() { return null; }
@Override protected void init(View view) { }
@Override protected void onBindData(T data) { }
}Modules are instantiated and bound with data via the static query method:
BasePart.query(container, HouseDetailInfoPart.class).bindData(data);Refactoring Process
The original 2,182‑line detail page was split into 14 logical modules (e.g., share logic, house info, price, incentives, dynamic content, etc.). Most modules were implemented as Part objects; two simple UI pieces were implemented as ViewPart . The main Activity now follows MVP, delegating data fetching to a presenter and UI rendering to the individual Parts.
MVP Contract
public interface HouseDetailContract {
interface Present extends BasePresenter
{
void getHouseResponse(String projectId);
void getShareResponse(String projectId);
void follow(String projectId, boolean follow);
}
interface View extends BaseView {
void onGetHouseResult(@NonNull NewHouseResBlockDetailBean data);
void onUpdateStateLayout(boolean isSuccess, boolean netError);
}
interface IFollowView extends IBaseView {
void updateFollowState(boolean followed, String error);
}
interface IShareView extends IBaseView {
void onShare(ShareDialog.ShareToThirdAppBean shareBean, ShareDialog.ShareToSmsBean smsBean);
void onGetShareResultFail(String error);
}
}Presenter Example
public class HouseDetailPresent extends HttpPresenter
implements HouseDetailContract.Present {
private HouseDetailContract.IFollowView iFollowView;
private HouseDetailContract.IShareView iShareView;
public void attachFollowView(HouseDetailContract.IFollowView view) { this.iFollowView = view; }
public void attachShareView(HouseDetailContract.IShareView view) { this.iShareView = view; }
@Override public void getHouseResponse(String projectId) {
enqueue(ApiClient.create(HouseApi.class).getHousesDetailResult(projectId),
new SimpleCallback
>() {
@Override protected void onNetworkError(HttpCall
> call, Throwable t) {
mView.onUpdateStateLayout(false, true);
}
@Override public void onResponse(HttpCall
> call, Result
entity) {
if (hasData()) {
mView.onGetHouseResult(entity.data);
mView.onUpdateStateLayout(true, false);
} else {
mView.onUpdateStateLayout(false, false);
}
}
}, true);
}
@Override public void follow(String projectId, final boolean follow) {
enqueue(ApiClient.create(HouseApi.class).followResblock(projectId, follow ? 1 : 0),
new SimpleCallback
() {
@Override public void onResponse(HttpCall
call, Result entity) {
if (isSuccess()) {
iFollowView.updateFollowState(follow, null);
} else {
iFollowView.updateFollowState(follow, Result.getErrorMsg(entity, follow ? "关注失败" : "取消关注失败"));
}
}
}, true);
}
// getShareResponse omitted for brevity
}Loading Parts After Data Retrieval
@Override public void onGetHouseResult(@NonNull NewHouseResBlockDetailBean data) {
bean = data;
setTitle(data.name);
bottomPart.bindData(data);
sharePart.bindData(data.projectName);
BasePart.query(container, HouseDetailInfoPart.class).bindData(data);
BasePart.query(container, OnePricePart.class).bindData(data);
// ... other parts
BasePart.query(container, IncentivePolicyPart.class).bindData(data.incentivePolicy);
// ... remaining parts
}Advantages
Greatly reduces the size of the main Activity/Fragment by moving logic into independent modules.
Clear separation of concerns; each module handles its own data and UI.
Invalid modules are not added to the view hierarchy, avoiding unnecessary rendering.
Supports partial refresh – only the affected Part needs to be rebound.
Modules can call each other via the static query method, enabling cross‑module interaction.
Limitations
Only works when the parent container is a LinearLayout and all children are added through Parts.
Parts must be created via the query method; manual instantiation is not supported.
View insertion order is fixed; dynamic weight or reordering is not currently supported.
Conclusion
The Part‑based modular MVP architecture provides a practical way to refactor large Android pages, improving maintainability, readability, and performance while keeping the codebase manageable. However, developers should be aware of its constraints regarding container type and view ordering.
Beike Product & Technology
As Beike's official product and technology account, we are committed to building a platform for sharing Beike's product and technology insights, targeting internet/O2O developers and product professionals. We share high-quality original articles, tech salon events, and recruitment information weekly. Welcome to follow us.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.