第三篇:用 Lego 架构重构商品详情页:从 3000 行到 15 个独立组件
前言:商品详情页——架构的"试金石"
商品详情页是电商App公认的"架构试金石"。一个标准详情页往往包含10个以上业务区块:轮播图、价格营销、规格选择、用户评价、店铺信息、图文详情、推荐商品……
在传统架构中,我们见过太多真实且令人头疼的灾难:
总控制逻辑不清晰,没有真正的"交通枢纽":页面控制逻辑散落在 Activity、Fragment、Adapter 甚至工具类中。你刚在 Activity 里找到一段加载数据的代码,顺着调用链又跳到了 ViewModel,再往下可能又塞进了一个 Adapter 的内部回调。没有一个集中的地方能让你看清"这个页面到底按什么顺序做什么事"。
代码冗余严重:到处散落着冗余逻辑和细微差别的代码段。要么不知道如何抽取方法——总觉得抽出来那个方法名比代码本身还长,要么硬抽一个然后挂羊头卖狗肉,方法名叫 updatePrice,里面却顺便刷新了库存、埋了点儿数据、还弹了个 Toast。
零散的胶水代码到处都是:临时标志位、匿名回调、多层嵌套判断……散落在各处,读起来像一本没有目录、也没有章节划分的书。
组织结构混乱,职责边界模糊:看似分了几个类或几个包,但数据转换、UI 渲染、业务判断常常交织在一起。改一个价格展示逻辑,可能要顺着调用链跳转五六个文件,而且总担心踩到其他不相关的逻辑。
找东西找不到:一个价格格式化方法,可能藏在 GoodsUtils,也可能在某个 ViewModel 的私有方法里,甚至直接硬编码在 Adapter 的 onBindViewHolder 中。新人接手后,大部分时间不是在理解业务,而是在做"寻址考古"。
这些问题的本质,不是因为没用某种"先进架构",而是:
- 没有拆成最小颗粒——缺乏 Lego 架构所倡导的分治思想,代码块过大、职责过重,导致难以复用和维护;
- 缺少治理思想——没有建立清晰的代码组织规范和边界,任凭冗余逻辑和胶水代码四处滋生,最终失控。
正如第二篇所述,Lego 架构的核心是分治法与单一职责,以及治理思想、工具迭代、复用发现等实践方法。今天,我们用实际项目代码演示:如何运用 Lego 架构的"最小颗粒度 + 动态组装"思想,以商品详情页为例,把复杂的 UI 和业务逻辑拆解成独立、可复用、可插拔的积木。
一、核心设计:一切皆列表项——最小颗粒度的极致实践
根据 Lego 架构"无限拆分到最小颗粒"的原则,我们需要将整个页面拆解为一个个独立的 UI 单元。按照这一思路,我们选择用一个单一的 RecyclerView 承载所有的 UI 元素。这个做法初看可能有些反直觉,但它是分治思想在列表型页面中的自然投射。
这样做的好处是:
- 每个 UI 单元对应一个独立的列表项类型,彼此之间没有嵌套依赖
- 页面整体的滚动性能由
RecyclerView统一管理,避免了多层级嵌套带来的卡顿和内存问题 - 新增或删除某个区块不需要调整布局结构,只需增减对应的列表项
1.1 定义统一的积木接口
我们用 sealed class 定义所有列表项类型,这就是我们的"积木清单"。每个列表项积木的职责只有一个:描述一种 UI 区块所需的数据。
sealed class GoodsDetailListItem {
// 商品基础信息区块
data class PriceMarketing(val state: GoodsDetailProductSectionState) : GoodsDetailListItem() // 职责:展示价格、促销标签
data class ProductTitle(val state: GoodsDetailProductSectionState) : GoodsDetailListItem() // 职责:展示商品标题、副标题
data class ServiceSales(val state: GoodsDetailProductSectionState) : GoodsDetailListItem() // 职责:展示服务承诺、销量数据
data class AfterSales(val state: GoodsDetailProductSectionState) : GoodsDetailListItem() // 职责:展示售后保障信息
// 规格与数量区块
data class SpecSelection(val state: GoodsDetailProductSectionState) : GoodsDetailListItem() // 职责:展示规格选择器(重量/口味等)
data class PurchaseQuantity(val state: GoodsDetailProductSectionState) : GoodsDetailListItem() // 职责:展示购买数量加减控件
// 通用装饰区块
data object SectionDivider : GoodsDetailListItem() // 职责:区块之间的分割线
// 评价与店铺区块
data class Review(val state: GoodsDetailReviewState) : GoodsDetailListItem() // 职责:展示用户评价摘要(评分、评价条数)
data class Shop(val state: GoodsDetailShopState) : GoodsDetailListItem() // 职责:展示店铺信息(名称、评分、客服入口)
// 图文详情区块
data object DetailsTitle : GoodsDetailListItem() // 职责:展示"图文详情"标题
data class DetailImage(val imageUrl: String) : GoodsDetailListItem() // 职责:展示详情页的一张图片
// 推荐商品区块
data object RecommendTitle : GoodsDetailListItem() // 职责:展示"猜你喜欢"标题
data class RecommendProduct(val product: BrowseProduct) : GoodsDetailListItem() // 职责:展示单个推荐商品
// 尾部区块
data object ListFooter : GoodsDetailListItem() // 职责:列表尾部占位/加载完成标识
}
为什么这比传统方式好?
- 彻底消灭"上帝布局":没有任何嵌套的
ScrollView或LinearLayout - 每个列表项可以独立开发、独立测试、独立迭代
- 新增一个 UI 区块,只需新增一个子类,无需修改任何现有代码
二、数据分层:原始数据 → UI 状态——用 Mapper 封装数据转换
【Mapper职责:】 负责将后端原始数据转换为 UI 层可直接使用的不可变状态对象。它不包含任何业务逻辑,是一个纯函数,输入原始数据,输出 UI 状态。
有了列表项积木之后,我们需要为每个积木准备所需的数据。为了降低 UI 与后端数据模型的耦合,我们引入了数据转换层(Mapper),用来生产一个个独立的列表项积木数据。
object GoodsDetailProductSectionMapper {
// 职责:将 GoodsDetail 后端模型 + 用户选择(规格、数量) 转换为商品基础区块所需的 UI 状态
fun from(
detail: GoodsDetail,
shipFromCity: String?,
selectedWeightIndex: Int,
selectedFlavorIndex: Int,
quantity: Int,
): GoodsDetailProductSectionState {
val selectedSku = detail.skus.getOrNull(selectedWeightIndex) ?: detail.skus.firstOrNull()
val priceYuan = selectedSku?.priceYuan ?: "98.00"
val formattedPrice = formatDisplayPrice(priceYuan)
// ... 其他字段映射
return GoodsDetailProductSectionState(
priceYuan = formattedPrice,
title = detail.title,
weightSpecs = detail.skus.map { it.name },
// ...
)
}
}
核心价值:
-
隔离后端变化:后端改了字段名,只需改
Mapper,UI 层完全不受影响 - 统一数据格式:所有 UI 需要的格式化逻辑集中在 Mapper 中
- 彻底解耦:UI 层只依赖 UI 状态,不依赖任何后端数据模型
三、动态组装:Assembler——积木的说明书
【Assembler职责:】 负责根据当前状态(产品信息、推荐列表、实验开关等)动态组装最终的列表项序列。它决定显示哪些区块、以什么顺序显示,是页面的"总装车间"。
如果说列表项是积木,那么 Assembler 就是 Lego 的说明书。它告诉我们应该用哪些积木、按什么顺序、拼成什么样子。
object GoodsDetailListAssembler {
// 职责:根据传入的各区块状态,组装出完整的列表项 List
fun build(
productSection: GoodsDetailProductSectionState,
detail: GoodsDetail,
detailImageUrls: List<String>,
recommendProducts: List<BrowseProduct>,
showListEndFooter: Boolean,
): List<GoodsDetailListItem> {
val items = mutableListOf<GoodsDetailListItem>()
items += GoodsDetailListItem.PriceMarketing(productSection)
items += GoodsDetailListItem.ProductTitle(productSection)
items += GoodsDetailListItem.ServiceSales(productSection)
items += GoodsDetailListItem.SectionDivider
items += GoodsDetailListItem.AfterSales(productSection)
items += GoodsDetailListItem.SectionDivider
items += GoodsDetailListItem.SpecSelection(productSection)
items += GoodsDetailListItem.PurchaseQuantity(productSection)
items += GoodsDetailListItem.SectionDivider
items += GoodsDetailListItem.Review(GoodsDetailReviewMapper.from(detail))
items += GoodsDetailListItem.SectionDivider
items += GoodsDetailListItem.Shop(GoodsDetailShopMapper.from(detail))
items += GoodsDetailListItem.SectionDivider
items += GoodsDetailListItem.DetailsTitle
detailImageUrls.forEach { url ->
items += GoodsDetailListItem.DetailImage(url)
}
items += GoodsDetailListItem.RecommendTitle
recommendProducts.forEach { product ->
items += GoodsDetailListItem.RecommendProduct(product)
}
if (showListEndFooter) {
items += GoodsDetailListItem.ListFooter
}
return items
}
}
这个 Assembler 解决了传统架构的多个痛点:
| 痛点 | Assembler 的解法 |
|---|---|
| 动态性 | 根据后端数据、AB实验、用户身份,动态决定显示哪些区块 |
| 可配置性 | 调整区块顺序只需移动两行代码,不需改动任何 UI 逻辑 |
| 解耦性 | 区块是动态组装的,自然避免了逻辑被耦合到具体区块内部 |
| 可测试性 | 传入不同参数即可验证不同组装结果,无需启动 App |
四、ViewModel:纯粹的状态协调者
【ViewModel职责:】 作为页面级别的状态持有者和协调者。它不包含 UI 逻辑,也不包含复杂的业务逻辑——只负责:
- 持有页面所需的数据(LiveData/StateFlow)
- 接收来自 View 的用户操作(如点击规格、修改数量)
- 调用 Mapper 和 Assembler 生成新的 UI 状态
- 将状态暴露给 View 进行观察
class GoodsDetailViewModel : ViewModel() {
private val repository = GoodsRepository()
private val _detail = MutableLiveData<GoodsDetail>()
val detail: LiveData<GoodsDetail> = _detail
private val _listItems = MutableLiveData<List<GoodsDetailListItem>>()
val listItems: LiveData<List<GoodsDetailListItem>> = _listItems
private val _detailImageUrls = MutableLiveData<List<String>>()
val detailImageUrls: LiveData<List<String>> = _detailImageUrls
private var selectedWeightIndex = 0
private var selectedFlavorIndex = 0
private var purchaseQuantity = 1
fun load(spuId: String) {
viewModelScope.launch {
val detailResponse = repository.fetchGoodsDetail(spuId)
_detail.postValue(detailResponse)
rebuildListItems()
}
}
fun selectWeightSpec(index: Int) {
selectedWeightIndex = index
rebuildListItems()
}
fun selectFlavorSpec(index: Int) {
selectedFlavorIndex = index
rebuildListItems()
}
fun incrementQuantity() {
purchaseQuantity++
rebuildListItems()
}
fun decrementQuantity() {
if (purchaseQuantity > 1) {
purchaseQuantity--
rebuildListItems()
}
}
private fun rebuildListItems() {
val detail = _detail.value ?: return
val productSection = GoodsDetailProductSectionMapper.from(
detail = detail,
shipFromCity = detail.shipFromCity,
selectedWeightIndex = selectedWeightIndex,
selectedFlavorIndex = selectedFlavorIndex,
quantity = purchaseQuantity,
)
val items = GoodsDetailListAssembler.build(
productSection = productSection,
detail = detail,
detailImageUrls = detail.detailImages,
recommendProducts = emptyList(),
showListEndFooter = true,
)
_listItems.postValue(items)
_detailImageUrls.postValue(detail.detailImages)
}
}
整个 ViewModel 职责单一、逻辑清晰,新人也能快速上手。
五、Activity:薄薄的一层壳——架构与视图的胶水层
【Activity职责:】 作为页面与 Android 系统的交互入口,负责:
- 初始化视图绑定(
setContentView) - 设置
RecyclerView、Adapter 及其他 UI 组件 - 将用户点击事件转发给
ViewModel - 观察
ViewModel中的 LiveData,并更新 UI
Activity 并不"只是一个容器",它是架构与 Android 视图系统之间的重要胶水层。 在 Lego 架构中,我们仍然尊重 Activity 作为导航和生命周期管理者的角色,但把具体的 UI 组装和业务逻辑全部委托给了积木——这样 Activity 永远不会膨胀。
最终的 Activity 代码,简洁而清晰:
@Route(path = RouterConstants.GOODS_DETAIL)
class GoodsDetailActivity : BaseActivity() {
private lateinit var binding: ActivityGoodsDetailBinding
private val viewModel: GoodsDetailViewModel by lazy {
ViewModelProvider(this)[GoodsDetailViewModel::class.java]
}
private val imageAdapter = GoodsImagePagerAdapter()
private lateinit var detailAdapter: GoodsDetailAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityGoodsDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
setupReviewListPanel()
setupDetailRecyclerView()
setupScrollToTop()
setupTopBarScroll()
setupActions()
setupPager()
observeViewModel()
loadData()
}
private fun observeViewModel() {
viewModel.detail.observe(this) { detail ->
currentDetail = detail
currentReviewState = GoodsDetailReviewMapper.from(detail)
imageAdapter.submit(detail.bannerImages)
updateImageCount(position = 0, total = detail.bannerImages.size)
}
viewModel.listItems.observe(this) { items ->
val imageUrls = viewModel.detailImageUrls.value.orEmpty()
if (imageUrls.isNotEmpty()) {
detailAdapter.submit(items, imageUrls)
}
}
viewModel.detailImageUrls.observe(this) { imageUrls ->
val items = viewModel.listItems.value
if (!items.isNullOrEmpty() && imageUrls.isNotEmpty()) {
detailAdapter.submit(items, imageUrls)
}
}
}
}
整个 Activity 只负责三件事:
- 初始化视图和
Adapter - 把用户点击事件转发给
ViewModel - 观察
ViewModel的状态并更新 UI
六、独立组件
6.1 网格间距装饰器
【职责:GridSpacingDecoration】 通用的 RecyclerView 网格间距装饰器,负责在网格布局中为每个 item 添加等距的外边距。放在 common 模块中,可以在任何需要网格布局的项目中直接复用。
6.2 评价面板:最小化侵入的页面组合
商品详情页中,评价列表是一个复杂的独立功能,但我们希望它不影响主页面的简洁性。通过侧滑面板 + Fragment 的方式,实现最小化侵入的页面组合。
核心组件及职责:
| 组件 | 职责 |
|---|---|
ReviewListPanelController |
控制面板的动画(显示/隐藏)、管理 Fragment 的添加/移除 |
GoodsReviewListFragment |
独立承载评价列表的完整 UI 和交互逻辑 |
GoodsReviewListAdapter |
评价列表的 RecyclerView Adapter,负责渲染每条评价 |
GoodsDetailReviewMapper |
将详情数据转换为评价面板所需的 UI 状态 |
Lego 思想体现:
-
完全独立的 Fragment:评价功能完全封装在
GoodsReviewListFragment中,包含完整的列表逻辑
class GoodsReviewListFragment : Fragment() {
private val listAdapter = GoodsReviewListAdapter()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.rvReviewList.layoutManager = LinearLayoutManager(requireContext())
binding.rvReviewList.adapter = listAdapter
}
fun render(state: GoodsDetailReviewState) {
binding.tvReviewListTitle.text = getString(R.string.goods_review_section_title_format, state.totalCount)
listAdapter.submitList(GoodsReviewListMockData.reviews())
}
}
- 干净的 Controller:面板控制器只负责动画和 Fragment 管理,不包含任何业务逻辑
class ReviewListPanelController(
private val activity: FragmentActivity,
private val panelRoot: View,
private val scrim: View,
private val panel: View,
private val fragmentContainerId: Int,
) {
fun show(reviewState: GoodsDetailReviewState) {
ensureFragment(reviewState)
// 动画展示面板
}
fun hide() {
// 动画隐藏面板
}
}
- 主 Activity 极简:只需要初始化 Controller,评价逻辑完全不侵入主 Activity
private fun setupReviewListPanel() {
reviewListPanelController = ReviewListPanelController(
activity = this,
panelRoot = binding.reviewListPanelOverlay.root,
scrim = binding.reviewListPanelOverlay.reviewListScrim,
panel = binding.reviewListPanelOverlay.reviewListPanel,
fragmentContainerId = binding.reviewListPanelOverlay.reviewListFragmentContainer.id,
)
}
private fun showReviewListPanel() {
val reviewState = GoodsDetailReviewMapper.from(detail)
reviewListPanelController.show(reviewState)
}
- 按需创建 Fragment:评价面板的 Fragment 只在首次点击时创建,不会影响首屏性能。
架构优势:
- 评价功能可以独立开发、独立测试
- 主 Activity 不包含任何评价相关的 UI 逻辑
- 面板动画和 Fragment 管理封装在 Controller 中
- 未来可以轻松替换为其他实现(如全屏 Fragment、BottomSheet 等)
七、项目结构总览
7.1 模块目录树
androidArch/
│
├── app/ # 应用入口
│ ├── App.kt # Application
│ └── MainActivity.kt # 首页
│
├── common/ # 公共基础(可被所有模块依赖)
│ ├── base/
│ │ ├── BaseActivity.kt # Activity 基类(封装埋点、生命周期)
│ │ └── BaseViewModel.kt # ViewModel 基类
│ │
│ ├── utils/
│ │ ├── ToastUtils.kt # 全局吐司
│ │ ├── StatusBarUtils.kt # 状态栏操作
│ │ └── PaletteColorUtils.kt # 颜色提取
│ │
│ ├── router/
│ │ └── AppRouter.kt # 路由导航
│ │
│ └── widgets/
│ ├── GridSpacingDecoration.kt # 通用网格间距装饰器
│ └── IconFontView.kt # 图标字体组件
│
├── app_res/ # 资源中心(颜色、尺寸、Drawable)
│ ├── res/
│ │ ├── drawable/ # 50+ 通用 Drawable
│ │ ├── values/colors_*.xml # 三层颜色体系
│ │ └── values/dimens.xml # 尺寸 Token
│ └── assets/iconfont.ttf # 图标字体
│
├── feature-goods/ # 商品模块
│ ├── ui/
│ │ ├── GoodsDetailActivity.kt # 商品详情页(主控制器)
│ │ ├── GoodsDetailAdapter.kt # 列表适配器(14种ViewType)
│ │ ├── GoodsDetailListItem.kt # 列表项 sealed class
│ │ ├── GoodsDetailListAssembler.kt # 列表组装器
│ │ ├── GoodsDetailProductSectionMapper.kt # 数据→UI状态
│ │ ├── GoodsDetailReviewMapper.kt
│ │ ├── GoodsDetailShopMapper.kt
│ │ ├── ReviewListPanelController.kt # 评价面板控制器
│ │ ├── GoodsReviewListFragment.kt # 评价列表(独立Fragment)
│ │ └── DetailAnchorTab.kt # Tab枚举
│ │
│ ├── viewmodel/
│ │ └── GoodsDetailViewModel.kt # 状态协调者
│ │
│ ├── model/
│ │ ├── GoodsDetail.kt # 原始数据模型
│ │ ├── GoodsDetailProductSectionState.kt # UI状态模型
│ │ ├── GoodsDetailReviewState.kt
│ │ └── BrowseProduct.kt
│ │
│ └── data/
│ ├── GoodsRepository.kt # 数据仓库
│ └── GoodsDetailMockCatalog.kt # Mock数据
│
├── feature-login/ # 登录模块
│ ├── ui/LoginActivity.kt
│ ├── domain/UserRepository.kt
│ └── viewmodel/LoginViewModel.kt
│
└── tools/ # 纯工具(跨项目复用)
└── utils/
├── DateUtils.kt
├── StringUtils.kt
└── ValidateUtils.kt
7.2 核心类职责说明
| 模块 | 类名 | 职责 | 代码行数 |
|---|---|---|---|
| ui | GoodsDetailActivity | 视图绑定、生命周期、用户交互转发 | ~500 |
| ui | GoodsDetailAdapter | 14种ViewType的列表渲染 | ~400 |
| ui | GoodsDetailListAssembler | 动态组装列表项 | ~80 |
| ui | GoodsDetailListItem | 列表项类型定义 | ~50 |
| ui | ReviewListPanelController | 评价面板动画与Fragment管理 | ~150 |
| ui | DetailAnchorTab | Tab枚举(独立小类) | ~10 |
| mapper | GoodsDetailProductSectionMapper | 数据→商品区UI状态 | ~60 |
| mapper | GoodsDetailReviewMapper | 数据→评价区UI状态 | ~30 |
| mapper | GoodsDetailShopMapper | 数据→店铺区UI状态 | ~20 |
| vm | GoodsDetailViewModel | 数据加载、状态持有、触发重建 | ~150 |
| common | BaseActivity | 封装埋点、Edge-to-Edge | ~50 |
| common | GridSpacingDecoration | 通用网格间距装饰器 | ~40 |
| common | ToastUtils | 全局吐司 | ~30 |
7.3 数据流向
后端数据 Mapper转换 Assembler组装 ViewModel持有 Activity观察
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
GoodsDetail ──► GoodsDetailProduct ──► List<GoodsDetail ──► _listItems ─────► notifyDataSetChanged
SectionState ListItem>
本实战案例完整践行了第二篇提出的 Lego 架构核心准则:
-
Base类极简化:
GoodsDetailActivity仅继承了一个空壳BaseActivity(只处理埋点、生命周期等必备接缝),所有功能通过可插拔积木提供。 -
无限拆分:页面被拆分为 15+ 个独立的列表项积木,每个职责单一;评价面板拆分为独立的
Fragment+Controller。 -
工具迭代:价格格式化方法最初写在
feature_goods/utils,后经复盘发现多个模块需要,已迁移至common/utils成为共有积木——这正是第二篇所述的"私有 → 共有"演化路径。
关于治理思想、工具迭代三阶段(私有→共有→远程)、复用发现等详细方法论,已在第二篇完整阐述,此处不再赘述。
八、架构优势对比
| 维度 | 传统方式(未拆分) | Lego 方式 |
|---|---|---|
| 代码组织 | 1个 3000+ 行的 Activity | 10+ 个 100-300 行的独立组件 |
| 可维护性 | 牵一发动全身 | 独立修改,互不影响 |
| 可测试性 | 难以单独测试 | 每个组件可独立测试 |
| 可复用性 | 几乎无法复用 | 组件可在多个页面复用 |
| 扩展性 | 新增区块要改很多地方 | 新增区块只需新增 ListItem 和 Assembler 逻辑 |
| 并行开发 | 只能串行开发 | 多个开发者可并行开发不同组件 |
九、总结:理想的复杂页面
理想的复杂页面,在架构上表现为:页面由若干职责分明、边界清晰的功能区块构成。每个区块内部,满目皆是高度内聚的工具类、UI 组件,零散的胶水代码极少。大量的基础积木、组合积木、高级积木与 UI 组件,都是经过线上环境长期检验、稳定可靠的"技术资产"。最终,那个曾经臃肿不堪的复杂页面,变得健壮、优雅、敏捷、轻盈、可扩展、可测试、可维护、可读性极佳——仿佛一件精心设计的作品,而非一堆难以收拾的代码。
Lego 架构的完整体系可以概括为:
- 一个公理:分治法 + 单一职责
- 多个定理:治理思想(大局观、逻辑收敛)、工具迭代(私有→共有→远程)、复用发现(小颗粒独立便于扫描整合)
当你真正用 Lego 思想来构建应用时,你会发现:复杂的不是应用本身,而是你没有把它拆成足够小的积木,并且没有持续的治理和迭代。
当然,精通设计模式、代码审美、以及发现积木的眼睛,也是一名优秀工程师所需的基本素养。
下一篇预告: 我们将继续丰富我们的demo项目,探讨设计模式如何作为 Lego 架构的粘合剂,让你的积木组合更加灵活、更加稳固。敬请期待!
相关阅读:
Top comments (0)