跳到主要内容

模块怎么写

三类模块

类型职责示例
app入口、导航图装配、全局主题/日志MainActivityBaseApplication
feature某一产品能力的屏 + ViewModel + 导航feature:devicesfeature:auth
core可被多 feature 复用的基础设施core:datacore: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.ktCompose 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)
  • 刷新 / 创建 / 启停走 suspend Repository 方法,用 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 的步骤

  1. settings.gradle.kts 注册
    include(":feature:yourfeature")
  2. 创建模块目录与 build.gradle.kts
    对照现有 feature(依赖 core:datacore:ui、Hilt、Compose 等)
  3. 写 Screen / ViewModel / Navigation
  4. app 的导航图中挂接
    只允许 app(或明确的图宿主)依赖该 feature
  5. 需要新 API / 缓存时扩展 core
    • 新 DTO / Retrofit 接口 → core:network
    • 新表 / DAO → core:database(含 schema 导出策略)
    • 新 Repository 接口与 OfflineFirst 实现 → core:data
    • 跨 feature 共享模型 → core:model
  6. 最小验证
./gradlew :feature:yourfeature:compileDebugKotlin
./gradlew :app:compileDebugKotlin

依赖与禁止事项

允许:

  • feature → core:data 的 Repository 接口
  • feature → core:modelcore:uicore:designsystem
  • core:data → database / network / datastore

禁止:

  • feature A → feature B
  • feature → 直接 implementation Room / 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 日志
  • 跑过最窄编译任务且通过

下一步:本地开发