模块怎么写
三类模块
| 类型 | 职责 | 示例 |
|---|---|---|
| app | 入口、导航图装配、全局主题/日志 | MainActivity、BaseApplication |
| feature | 某一产品能力的屏 + ViewModel + 导航 | feature:devices、feature:auth |
| core | 可被多 feature 复用的基础设施 | core:data、core:ui |
新增业务能力时,默认加 feature:<name>,而不是把屏塞进 app 或另一个 feature。
Feature 内部形状
参考 feature:devices:
feature/devices/
build.gradle.kts
src/main/java/.../feature/devices/
DevicesListScreen.kt
DevicesListViewModel.kt
DeviceDetailScreen.kt
DeviceDetailViewModel.kt
DevicesNavigation.kt
src/test/java/.../ # 单元测试(可选但推荐)
常见约定:
| 文件 | 做什么 |
|---|---|
*Screen.kt | Compose UI:收集 uiState,把点击变成事件 |
*ViewModel.kt | @HiltViewModel:组合 Repository Flow,维护 UiState |
*Navigation.kt | 路由名、NavGraphBuilder 扩展、导 航回调类型 |
ViewModel 形态(示意)
data class DevicesListUiState(
val devices: List<Device> = emptyList(),
val isLoading: Boolean = true,
val errorMessage: String? = null,
// … 对话框、筛选、批量操作等瞬时 UI 状态
)
@HiltViewModel
class DevicesListViewModel @Inject constructor(
authRepository: AuthRepository,
private val devicesRepository: DevicesRepository,
) : ViewModel() {
val uiState: StateFlow<DevicesListUiState> = /* combine / flatMapLatest … */
}
要点:
- 构造注入 Repository,不注入
DevicesApi/DeviceDao - 列表数据来自
devicesRepository.observeDevices(orgId)(Room Flow) - 刷新 / 创建 / 启停走
suspendRepository 方法,用AppResult处理错误 - 一次性密钥(如创建设备 secret)只放在 UI 状态 / 一次性对话框,不写 Room(见 安全与密钥)
Screen 形态(示意)
@Composable
fun DevicesListScreen(
viewModel: DevicesListViewModel = hiltViewModel(),
onDeviceClick: (String) -> Unit,
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// 渲染 uiState;点击调用 viewModel 方法或 onDeviceClick
}
- 长列表用
LazyColumn,稳定 key,必要时contentType - 列表行全宽、少套 Card;详情再展开
- 不要在 lazy item lambda 里做重计算 / 网络
新增一个 feature 的步骤
- 在
settings.gradle.kts注册
include(":feature:yourfeature") - 创建模块目录与
build.gradle.kts
对照现有 feature(依赖core:data、core:ui、Hilt、Compose 等) - 写 Screen / ViewModel / Navigation
- 在
app的导航图中挂接
只允许app(或明确的图宿主)依赖该 feature - 需要新 API / 缓存时扩展 core
- 新 DTO / Retrofit 接口 →
core:network - 新表 / DAO →
core:database(含 schema 导出策略) - 新 Repository 接口与 OfflineFirst 实现 →
core:data - 跨 feature 共享模型 →
core:model
- 新 DTO / Retrofit 接口 →
- 最小验证
./gradlew :feature:yourfeature:compileDebugKotlin
./gradlew :app:compileDebugKotlin
依赖与禁止事项
允许:
- feature →
core:data的 Repository 接口 - feature →
core:model、core:ui、core:designsystem core:data→ database / network / datastore
禁止:
- feature A → feature B
- feature → 直接
implementationRoom / Retrofit 实现细节并绕过 Repository - Composable 内读 DataStore、开 OkHttp、跑长任务
- 为列表每一行发一次网络请求(N+1);应要求后端提供列表/批量 API
UI 列表约定(产品一致性)
参考工程 AGENTS.md 中的列表规则,交付时尽量对齐:
- 列表页用全宽行 + 分隔线,而不是整页包在 Card 里
- 可导航行:leading icon、主文案、可选 trailing 状态、chevron
- 父列表进子页时不要随意
popUpTo清栈;popUpTo留给底部 Tab 的 start destination - 屏标题不要在列表顶部再重复写一遍 section 标题
测试建议
| 层级 | 做法 |
|---|---|
| ViewModel | 假 Repository + Turbine / 收集 StateFlow |
| Repository | 假 API + 内存 DAO 或 Room in-memory |
| UI smoke | :app:connectedDebugAndroidTest(ComposeTestRule) |
优先 fake,少用脆弱 mock。
改非平凡逻辑时补测试。
自检清单
- 新屏落在
feature/*,不在无关模块里长出来 - 无 feature↔feature 依赖
- UI 不 直接碰 Room / Retrofit / DataStore
-
UiState不可变;错误信息可展示(优先后端message) - 敏感一次性字段不进 Room、不进 release 日志
- 跑过最窄编译任务且通过
下一步:本地开发。