创建页面流程
Feature 层的页面都遵循统一套路:先定义类型安全路由,再实现 ViewModel、Compose 三层视图,最后在模块 Graph 中暴露。下面以“商品详情”页面为例,结合模板说明具体步骤。
提示:实际业务可直接复制文档中的代码片段,再替换成自己的字段;若已配置 Android Studio 模板(见页面模板),可用一键生成 Route / Screen / ViewModel 文件。
1. 定义路由
在 app/src/main/java/com/joker/kit/navigation/routes/GoodsRoutes.kt 中声明路由对象:
object GoodsRoutes {
@Serializable
data class Detail(val goodsId: Long)
}所有路由都必须是 @Serializable,以便在 Compose Navigation 中使用 composable<Route>() 并通过 toRoute() 自动解析参数。
2. 注册导航 & Graph
在 feature/goods/navigation/GoodsDetailNavigation.kt 中提供 NavGraphBuilder 扩展:
fun NavGraphBuilder.goodsDetailScreen() {
composable<GoodsRoutes.Detail> {
GoodsDetailRoute()
}
}然后在 GoodsGraph.kt 内整合当前模块所有页面:
fun NavGraphBuilder.goodsGraph(navController: NavHostController) {
goodsDetailScreen()
goodsCommentScreen()
goodsSearchScreen()
}最后在 AppNavHost 的 NavHost { ... } 中新增 goodsGraph(navController, sharedTransitionScope),这样单个模块的开发者无需直接编辑 AppNavHost。
3. 创建 ViewModel
ViewModel 继承 BaseNetWorkViewModel(或其他基类),负责:
- 从
SavedStateHandle读取路由参数:private val route = savedStateHandle.toRoute<GoodsRoutes.Detail>() - 暴露
uiState、弹窗状态等StateFlow - 封装操作:
navigateBack()、retryRequest()、showSpecModal()等
示例(节选):
@HiltViewModel
class GoodsDetailViewModel @Inject constructor(
navigator: AppNavigator,
userState: UserState,
savedStateHandle: SavedStateHandle,
private val repository: GoodsRepository,
) : BaseNetWorkViewModel<GoodsDetail>(navigator, userState, savedStateHandle) {
private val route = savedStateHandle.toRoute<GoodsRoutes.Detail>()
private val goodsId = route.goodsId
override fun requestApiFlow(): Flow<NetworkResponse<GoodsDetail>> =
repository.getGoodsInfo(goodsId.toString())
// 省略:StateFlow 定义、弹窗控制、导航回调等
}4. 实现 Route / Screen / Content
视图层遵循“三段式”结构,并保持注释完整(Route→Screen→Content),外层用 Scaffold 承载顶部栏与内边距:
@Composable
internal fun GoodsDetailRoute(
viewModel: GoodsDetailViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
val specModalVisible by viewModel.specModalVisible.collectAsState()
// ... 其他状态
GoodsDetailScreen(
uiState = uiState,
specModalVisible = specModalVisible,
onBackClick = viewModel::navigateBack,
onRetry = viewModel::retryRequest,
onShowSpecModal = viewModel::showSpecModal,
// ... 其他回调
)
}GoodsDetailScreen 负责 Loading / Error / Success 切换,并把成功态数据交给 GoodsDetailContent:
@Composable
internal fun GoodsDetailScreen(
uiState: BaseNetWorkUiState<GoodsDetail>,
specModalVisible: Boolean,
onRetry: () -> Unit,
onShowSpecModal: () -> Unit,
// ...
) {
AppScaffold(
titleText = stringResource(R.string.goods_detail_title),
onBackClick = onBackClick
) {
BaseNetWorkView(
uiState = uiState,
onRetry = onRetry,
) { detail ->
GoodsDetailContent(
data = detail,
specModalVisible = specModalVisible,
onShowSpecModal = onShowSpecModal,
// ...
)
}
}
}GoodsDetailContent 专注展示数据,不直接访问 ViewModel:
@Composable
private fun GoodsDetailContent(
data: GoodsDetail,
specModalVisible: Boolean,
onShowSpecModal: () -> Unit,
// ...
) {
// 页面布局、省略具体 UI
}5. 预览与资源
- 每个 Screen 至少提供浅色/深色两组
@Preview,并通过BaseNetWorkUiState.Success(mockData())注入演示数据。 - 所有文案写入模块内的
res/values/strings.xml与values-en/strings.xml,遵循 snake_case 命名并按功能分组。 - 颜色/尺寸/图标优先引用
core/designsystem与core/ui,保持全局一致。
6. 模板
参见页面模板,可直接复制 Route/Screen/ViewModel/Navigation 的标准代码;生成后根据实际业务补全数据类型与仓库调用。
完成以上步骤后,一个 Feature 页面即可接入导航、状态、UI 组件体系,后续只需聚焦业务逻辑本身。