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 是否代表一个清晰、稳定、可演进的业务边界。