Now in Android Feature 模块分析:一个功能是如何被组织起来的?

Now in Android Feature 模块分析:一个功能是如何被组织起来的?

核心观点

在 Now in Android 中,Feature 并不仅仅代表一个页面,而是一个完整的业务单元。

它负责:

  • 展示 UI;
  • 管理界面状态;
  • 响应用户行为;
  • 协调数据获取。

但它并不负责:

  • 数据存储;
  • 网络请求;
  • 共享业务逻辑。

这种边界划分使得 Feature 可以独立开发,同时避免业务层直接依赖底层实现。

Feature 是什么?

在 NIA 中,Feature 通常按照业务能力拆分,例如:

复制代码
feature:foryou
feature:search
feature:bookmarks
feature:topic
feature:interests

每个 Feature 都对应一个用户可以感知的功能。

这种拆分方式的核心思想是:

以业务为中心,而不是以技术分层为中心。

一个 Feature 是如何组织的?

feature:foryou 为例,其内部通常包含:
#mermaid-svg-8LBk7f1awcUFSaGa{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8LBk7f1awcUFSaGa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8LBk7f1awcUFSaGa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8LBk7f1awcUFSaGa .error-icon{fill:#552222;}#mermaid-svg-8LBk7f1awcUFSaGa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8LBk7f1awcUFSaGa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8LBk7f1awcUFSaGa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8LBk7f1awcUFSaGa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8LBk7f1awcUFSaGa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8LBk7f1awcUFSaGa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8LBk7f1awcUFSaGa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8LBk7f1awcUFSaGa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8LBk7f1awcUFSaGa .marker.cross{stroke:#333333;}#mermaid-svg-8LBk7f1awcUFSaGa svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8LBk7f1awcUFSaGa p{margin:0;}#mermaid-svg-8LBk7f1awcUFSaGa .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8LBk7f1awcUFSaGa .cluster-label text{fill:#333;}#mermaid-svg-8LBk7f1awcUFSaGa .cluster-label span{color:#333;}#mermaid-svg-8LBk7f1awcUFSaGa .cluster-label span p{background-color:transparent;}#mermaid-svg-8LBk7f1awcUFSaGa .label text,#mermaid-svg-8LBk7f1awcUFSaGa span{fill:#333;color:#333;}#mermaid-svg-8LBk7f1awcUFSaGa .node rect,#mermaid-svg-8LBk7f1awcUFSaGa .node circle,#mermaid-svg-8LBk7f1awcUFSaGa .node ellipse,#mermaid-svg-8LBk7f1awcUFSaGa .node polygon,#mermaid-svg-8LBk7f1awcUFSaGa .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8LBk7f1awcUFSaGa .rough-node .label text,#mermaid-svg-8LBk7f1awcUFSaGa .node .label text,#mermaid-svg-8LBk7f1awcUFSaGa .image-shape .label,#mermaid-svg-8LBk7f1awcUFSaGa .icon-shape .label{text-anchor:middle;}#mermaid-svg-8LBk7f1awcUFSaGa .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8LBk7f1awcUFSaGa .rough-node .label,#mermaid-svg-8LBk7f1awcUFSaGa .node .label,#mermaid-svg-8LBk7f1awcUFSaGa .image-shape .label,#mermaid-svg-8LBk7f1awcUFSaGa .icon-shape .label{text-align:center;}#mermaid-svg-8LBk7f1awcUFSaGa .node.clickable{cursor:pointer;}#mermaid-svg-8LBk7f1awcUFSaGa .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8LBk7f1awcUFSaGa .arrowheadPath{fill:#333333;}#mermaid-svg-8LBk7f1awcUFSaGa .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8LBk7f1awcUFSaGa .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8LBk7f1awcUFSaGa .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8LBk7f1awcUFSaGa .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8LBk7f1awcUFSaGa .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8LBk7f1awcUFSaGa .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8LBk7f1awcUFSaGa .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8LBk7f1awcUFSaGa .cluster text{fill:#333;}#mermaid-svg-8LBk7f1awcUFSaGa .cluster span{color:#333;}#mermaid-svg-8LBk7f1awcUFSaGa div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8LBk7f1awcUFSaGa .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8LBk7f1awcUFSaGa rect.text{fill:none;stroke-width:0;}#mermaid-svg-8LBk7f1awcUFSaGa .icon-shape,#mermaid-svg-8LBk7f1awcUFSaGa .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8LBk7f1awcUFSaGa .icon-shape p,#mermaid-svg-8LBk7f1awcUFSaGa .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8LBk7f1awcUFSaGa .icon-shape .label rect,#mermaid-svg-8LBk7f1awcUFSaGa .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8LBk7f1awcUFSaGa .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8LBk7f1awcUFSaGa .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8LBk7f1awcUFSaGa :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-8LBk7f1awcUFSaGa .screen>*{fill:#d6eaff!important;stroke:#4a90e2!important;color:#000!important;}#mermaid-svg-8LBk7f1awcUFSaGa .screen span{fill:#d6eaff!important;stroke:#4a90e2!important;color:#000!important;}#mermaid-svg-8LBk7f1awcUFSaGa .screen tspan{fill:#000!important;}#mermaid-svg-8LBk7f1awcUFSaGa .vm>*{fill:#dff5df!important;stroke:#4caf50!important;color:#000!important;}#mermaid-svg-8LBk7f1awcUFSaGa .vm span{fill:#dff5df!important;stroke:#4caf50!important;color:#000!important;}#mermaid-svg-8LBk7f1awcUFSaGa .vm tspan{fill:#000!important;}#mermaid-svg-8LBk7f1awcUFSaGa .state>*{fill:#fff4cc!important;stroke:#f4b400!important;color:#000!important;}#mermaid-svg-8LBk7f1awcUFSaGa .state span{fill:#fff4cc!important;stroke:#f4b400!important;color:#000!important;}#mermaid-svg-8LBk7f1awcUFSaGa .state tspan{fill:#000!important;} Compose Screen
ViewModel
UiState

它们共同组成一个完整的功能模块。

Screen:负责展示

Compose Screen 的职责包括:

  • 展示数据;
  • 接收用户输入;
  • 触发用户事件。

例如:ForYouScreen

它不直接访问 Repository。

它只关心:

当前应该显示什么。

ViewModel:负责协调

ViewModel 位于 UI 与数据层之间。

主要职责包括:

  • 获取数据;
  • 转换 UI State;
  • 响应用户操作;
  • 管理生命周期相关状态。

它回答的问题是:

UI 应该处于什么状态?

UiState:负责描述状态

NIA 大量使用 UDF(单向数据流)。

因此,Screen 并不直接读取 Repository。

而是消费一个统一的状态对象,例如:

复制代码
Loading
Success
Error

这种设计的价值在于:

  • 状态清晰;
  • 易于测试;
  • 降低 UI 复杂度。

一个 Feature 是如何工作的?

从用户点击开始,大致流程如下:
#mermaid-svg-3Aqqeb7p8K26dxn7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3Aqqeb7p8K26dxn7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3Aqqeb7p8K26dxn7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3Aqqeb7p8K26dxn7 .error-icon{fill:#552222;}#mermaid-svg-3Aqqeb7p8K26dxn7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3Aqqeb7p8K26dxn7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3Aqqeb7p8K26dxn7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3Aqqeb7p8K26dxn7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3Aqqeb7p8K26dxn7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3Aqqeb7p8K26dxn7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3Aqqeb7p8K26dxn7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3Aqqeb7p8K26dxn7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3Aqqeb7p8K26dxn7 .marker.cross{stroke:#333333;}#mermaid-svg-3Aqqeb7p8K26dxn7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3Aqqeb7p8K26dxn7 p{margin:0;}#mermaid-svg-3Aqqeb7p8K26dxn7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3Aqqeb7p8K26dxn7 .cluster-label text{fill:#333;}#mermaid-svg-3Aqqeb7p8K26dxn7 .cluster-label span{color:#333;}#mermaid-svg-3Aqqeb7p8K26dxn7 .cluster-label span p{background-color:transparent;}#mermaid-svg-3Aqqeb7p8K26dxn7 .label text,#mermaid-svg-3Aqqeb7p8K26dxn7 span{fill:#333;color:#333;}#mermaid-svg-3Aqqeb7p8K26dxn7 .node rect,#mermaid-svg-3Aqqeb7p8K26dxn7 .node circle,#mermaid-svg-3Aqqeb7p8K26dxn7 .node ellipse,#mermaid-svg-3Aqqeb7p8K26dxn7 .node polygon,#mermaid-svg-3Aqqeb7p8K26dxn7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3Aqqeb7p8K26dxn7 .rough-node .label text,#mermaid-svg-3Aqqeb7p8K26dxn7 .node .label text,#mermaid-svg-3Aqqeb7p8K26dxn7 .image-shape .label,#mermaid-svg-3Aqqeb7p8K26dxn7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-3Aqqeb7p8K26dxn7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3Aqqeb7p8K26dxn7 .rough-node .label,#mermaid-svg-3Aqqeb7p8K26dxn7 .node .label,#mermaid-svg-3Aqqeb7p8K26dxn7 .image-shape .label,#mermaid-svg-3Aqqeb7p8K26dxn7 .icon-shape .label{text-align:center;}#mermaid-svg-3Aqqeb7p8K26dxn7 .node.clickable{cursor:pointer;}#mermaid-svg-3Aqqeb7p8K26dxn7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3Aqqeb7p8K26dxn7 .arrowheadPath{fill:#333333;}#mermaid-svg-3Aqqeb7p8K26dxn7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3Aqqeb7p8K26dxn7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3Aqqeb7p8K26dxn7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3Aqqeb7p8K26dxn7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3Aqqeb7p8K26dxn7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3Aqqeb7p8K26dxn7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3Aqqeb7p8K26dxn7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3Aqqeb7p8K26dxn7 .cluster text{fill:#333;}#mermaid-svg-3Aqqeb7p8K26dxn7 .cluster span{color:#333;}#mermaid-svg-3Aqqeb7p8K26dxn7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3Aqqeb7p8K26dxn7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3Aqqeb7p8K26dxn7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-3Aqqeb7p8K26dxn7 .icon-shape,#mermaid-svg-3Aqqeb7p8K26dxn7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3Aqqeb7p8K26dxn7 .icon-shape p,#mermaid-svg-3Aqqeb7p8K26dxn7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3Aqqeb7p8K26dxn7 .icon-shape .label rect,#mermaid-svg-3Aqqeb7p8K26dxn7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3Aqqeb7p8K26dxn7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3Aqqeb7p8K26dxn7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3Aqqeb7p8K26dxn7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-3Aqqeb7p8K26dxn7 .user>*{fill:#f5f5f5!important;stroke:#757575!important;color:#000!important;}#mermaid-svg-3Aqqeb7p8K26dxn7 .user span{fill:#f5f5f5!important;stroke:#757575!important;color:#000!important;}#mermaid-svg-3Aqqeb7p8K26dxn7 .user tspan{fill:#000!important;}#mermaid-svg-3Aqqeb7p8K26dxn7 .screen>*{fill:#d6eaff!important;stroke:#4a90e2!important;color:#000!important;}#mermaid-svg-3Aqqeb7p8K26dxn7 .screen span{fill:#d6eaff!important;stroke:#4a90e2!important;color:#000!important;}#mermaid-svg-3Aqqeb7p8K26dxn7 .screen tspan{fill:#000!important;}#mermaid-svg-3Aqqeb7p8K26dxn7 .vm>*{fill:#dff5df!important;stroke:#4caf50!important;color:#000!important;}#mermaid-svg-3Aqqeb7p8K26dxn7 .vm span{fill:#dff5df!important;stroke:#4caf50!important;color:#000!important;}#mermaid-svg-3Aqqeb7p8K26dxn7 .vm tspan{fill:#000!important;}#mermaid-svg-3Aqqeb7p8K26dxn7 .repo>*{fill:#fff4cc!important;stroke:#f4b400!important;color:#000!important;}#mermaid-svg-3Aqqeb7p8K26dxn7 .repo span{fill:#fff4cc!important;stroke:#f4b400!important;color:#000!important;}#mermaid-svg-3Aqqeb7p8K26dxn7 .repo tspan{fill:#000!important;}#mermaid-svg-3Aqqeb7p8K26dxn7 .data>*{fill:#ffd6d6!important;stroke:#db4437!important;color:#000!important;}#mermaid-svg-3Aqqeb7p8K26dxn7 .data span{fill:#ffd6d6!important;stroke:#db4437!important;color:#000!important;}#mermaid-svg-3Aqqeb7p8K26dxn7 .data tspan{fill:#000!important;} User Action
Compose Screen
ViewModel
Repository
Database / Network

这意味着:

Feature 并不是一个孤立页面。

它是:

Screen + ViewModel + State 的组合体。

为什么 Repository 不放在 Feature 中?

这是 NIA 非常重要的一个设计选择。

如果 Repository 放在 Feature 中:

复制代码
feature:search
└─ SearchRepository

那么:

  • Bookmark 功能可能无法复用;
  • 多个 Feature 容易出现重复实现;
  • 数据边界变得模糊。

因此,NIA 将 Repository 放入 Core 层。

Feature 只依赖抽象能力,而不依赖具体实现。

为什么 Feature 不直接访问 Database?

如果:

复制代码
Screen
↓
Database

那么:

  • UI 与存储强耦合;
  • 数据来源难以切换;
  • 测试成本增加。

通过 Repository 隔离后:
#mermaid-svg-XexqCZXgzoKUAF7l{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XexqCZXgzoKUAF7l .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XexqCZXgzoKUAF7l .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XexqCZXgzoKUAF7l .error-icon{fill:#552222;}#mermaid-svg-XexqCZXgzoKUAF7l .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XexqCZXgzoKUAF7l .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XexqCZXgzoKUAF7l .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XexqCZXgzoKUAF7l .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XexqCZXgzoKUAF7l .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XexqCZXgzoKUAF7l .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XexqCZXgzoKUAF7l .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XexqCZXgzoKUAF7l .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XexqCZXgzoKUAF7l .marker.cross{stroke:#333333;}#mermaid-svg-XexqCZXgzoKUAF7l svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XexqCZXgzoKUAF7l p{margin:0;}#mermaid-svg-XexqCZXgzoKUAF7l .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XexqCZXgzoKUAF7l .cluster-label text{fill:#333;}#mermaid-svg-XexqCZXgzoKUAF7l .cluster-label span{color:#333;}#mermaid-svg-XexqCZXgzoKUAF7l .cluster-label span p{background-color:transparent;}#mermaid-svg-XexqCZXgzoKUAF7l .label text,#mermaid-svg-XexqCZXgzoKUAF7l span{fill:#333;color:#333;}#mermaid-svg-XexqCZXgzoKUAF7l .node rect,#mermaid-svg-XexqCZXgzoKUAF7l .node circle,#mermaid-svg-XexqCZXgzoKUAF7l .node ellipse,#mermaid-svg-XexqCZXgzoKUAF7l .node polygon,#mermaid-svg-XexqCZXgzoKUAF7l .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XexqCZXgzoKUAF7l .rough-node .label text,#mermaid-svg-XexqCZXgzoKUAF7l .node .label text,#mermaid-svg-XexqCZXgzoKUAF7l .image-shape .label,#mermaid-svg-XexqCZXgzoKUAF7l .icon-shape .label{text-anchor:middle;}#mermaid-svg-XexqCZXgzoKUAF7l .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XexqCZXgzoKUAF7l .rough-node .label,#mermaid-svg-XexqCZXgzoKUAF7l .node .label,#mermaid-svg-XexqCZXgzoKUAF7l .image-shape .label,#mermaid-svg-XexqCZXgzoKUAF7l .icon-shape .label{text-align:center;}#mermaid-svg-XexqCZXgzoKUAF7l .node.clickable{cursor:pointer;}#mermaid-svg-XexqCZXgzoKUAF7l .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XexqCZXgzoKUAF7l .arrowheadPath{fill:#333333;}#mermaid-svg-XexqCZXgzoKUAF7l .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XexqCZXgzoKUAF7l .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XexqCZXgzoKUAF7l .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XexqCZXgzoKUAF7l .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XexqCZXgzoKUAF7l .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XexqCZXgzoKUAF7l .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XexqCZXgzoKUAF7l .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XexqCZXgzoKUAF7l .cluster text{fill:#333;}#mermaid-svg-XexqCZXgzoKUAF7l .cluster span{color:#333;}#mermaid-svg-XexqCZXgzoKUAF7l div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XexqCZXgzoKUAF7l .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XexqCZXgzoKUAF7l rect.text{fill:none;stroke-width:0;}#mermaid-svg-XexqCZXgzoKUAF7l .icon-shape,#mermaid-svg-XexqCZXgzoKUAF7l .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XexqCZXgzoKUAF7l .icon-shape p,#mermaid-svg-XexqCZXgzoKUAF7l .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XexqCZXgzoKUAF7l .icon-shape .label rect,#mermaid-svg-XexqCZXgzoKUAF7l .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XexqCZXgzoKUAF7l .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XexqCZXgzoKUAF7l .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XexqCZXgzoKUAF7l :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-XexqCZXgzoKUAF7l .feature>*{fill:#dff5df!important;stroke:#4caf50!important;color:#000!important;}#mermaid-svg-XexqCZXgzoKUAF7l .feature span{fill:#dff5df!important;stroke:#4caf50!important;color:#000!important;}#mermaid-svg-XexqCZXgzoKUAF7l .feature tspan{fill:#000!important;}#mermaid-svg-XexqCZXgzoKUAF7l .core>*{fill:#fff4cc!important;stroke:#f4b400!important;color:#000!important;}#mermaid-svg-XexqCZXgzoKUAF7l .core span{fill:#fff4cc!important;stroke:#f4b400!important;color:#000!important;}#mermaid-svg-XexqCZXgzoKUAF7l .core tspan{fill:#000!important;}#mermaid-svg-XexqCZXgzoKUAF7l .infra>*{fill:#ffd6d6!important;stroke:#db4437!important;color:#000!important;}#mermaid-svg-XexqCZXgzoKUAF7l .infra span{fill:#ffd6d6!important;stroke:#db4437!important;color:#000!important;}#mermaid-svg-XexqCZXgzoKUAF7l .infra tspan{fill:#000!important;} Screen
ViewModel
Repository
Database / Network

Feature 只关心:

"我要什么数据?"

而不是:

"这些数据从哪里来?"

Feature 模块带来的价值

对于大型项目而言,Feature 拆分具有明显优势:

  • 支持多人并行开发;
  • 降低模块之间的影响范围;
  • 便于独立测试;
  • 有利于按业务演进。

其本质是:

将复杂系统拆分为多个相对独立的业务单元。

它是否适用于所有项目?

不一定。

如果项目规模较小:

复制代码
1~3 名开发者
功能较少
生命周期较短

过度拆分 Feature 可能导致:

  • Module 数量膨胀;
  • Gradle 配置复杂;
  • 导航维护成本增加。

但当项目逐渐增长时:

复制代码
10+ 名开发者
长期维护
频繁迭代

Feature 模块化带来的收益会越来越明显。

我的结论

Now in Android 的 Feature 设计,本质上是在建立业务边界。

它试图回答的问题不是:

"一个页面应该放在哪里?"

而是:

"一个业务能力应该如何独立存在?"

因此,Feature 模块真正封装的不是 UI。

而是:

一个完整的业务功能。

理解这一点,比记住目录结构更重要。

好的 Feature 设计,不在于拆得越细越好。

而在于:

每个 Feature 是否代表一个清晰、稳定、可演进的业务边界。

相关推荐
Che2n3JigW2 小时前
Now in Android 项目结构分析:这个 App 是如何搭建起来的?
android·architecture·now in android·modularization·structure
恋猫de小郭2 小时前
flutter_agent_lens 用 MCP 服务,将 Flutter DevTools 暴露给 AI
android·前端·flutter
AI玫瑰助手2 小时前
Python函数:内置函数(len/max/min/sorted等)详解
android·开发语言·python
Kapaseker2 小时前
Kotlin 集合:只读不等于不可变
android·kotlin
风华圆舞2 小时前
一个 Flutter 项目同时保留 Android、iOS、HarmonyOS 支持的实践
android·flutter·ios
顾林海2 小时前
Android来时路:Android 是什么
android
2501_915921432 小时前
uni-app 上架 iOS 的完整流程(无需依赖 Mac)
android·macos·ios·小程序·uni-app·iphone·webview
Che2n3JigW2 小时前
Now in Android Core 模块分析:共享能力是如何被抽离的?
android·architecture·now in android·modularization·core module
黄林晴3 小时前
绝了!Compose Multiplatform 也能实现 iOS26 液态玻璃的效果了
android·kotlin