Skip to content

创建页面流程

Feature 层的页面都遵循统一套路:先定义类型安全路由,再实现 ViewModel、Compose 三层视图,最后在模块 Graph 中暴露。下面以“商品详情”页面为例,结合模板说明具体步骤。

提示:实际业务可直接复制文档中的代码片段,再替换成自己的字段;若已配置 Android Studio 模板(见页面模板),可用一键生成 Route / Screen / ViewModel 文件。

1. 定义路由

app/src/main/java/com/joker/kit/navigation/routes/GoodsRoutes.kt 中声明路由对象:

kotlin
object GoodsRoutes {
    @Serializable
    data class Detail(val goodsId: Long)
}

所有路由都必须是 @Serializable,以便在 Compose Navigation 中使用 composable<Route>() 并通过 toRoute() 自动解析参数。

2. 注册导航 & Graph

feature/goods/navigation/GoodsDetailNavigation.kt 中提供 NavGraphBuilder 扩展:

kotlin
fun NavGraphBuilder.goodsDetailScreen() {
    composable<GoodsRoutes.Detail> {
        GoodsDetailRoute()
    }
}

然后在 GoodsGraph.kt 内整合当前模块所有页面:

kotlin
fun NavGraphBuilder.goodsGraph(navController: NavHostController) {
    goodsDetailScreen()
    goodsCommentScreen()
    goodsSearchScreen()
}

最后在 AppNavHostNavHost { ... } 中新增 goodsGraph(navController, sharedTransitionScope),这样单个模块的开发者无需直接编辑 AppNavHost。

3. 创建 ViewModel

ViewModel 继承 BaseNetWorkViewModel(或其他基类),负责:

  • SavedStateHandle 读取路由参数:private val route = savedStateHandle.toRoute<GoodsRoutes.Detail>()
  • 暴露 uiState、弹窗状态等 StateFlow
  • 封装操作:navigateBack()retryRequest()showSpecModal()

示例(节选):

kotlin
@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 承载顶部栏与内边距:

kotlin
@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

kotlin
@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:

kotlin
@Composable
private fun GoodsDetailContent(
    data: GoodsDetail,
    specModalVisible: Boolean,
    onShowSpecModal: () -> Unit,
    // ...
) {
    // 页面布局、省略具体 UI
}

5. 预览与资源

  • 每个 Screen 至少提供浅色/深色两组 @Preview,并通过 BaseNetWorkUiState.Success(mockData()) 注入演示数据。
  • 所有文案写入模块内的 res/values/strings.xmlvalues-en/strings.xml,遵循 snake_case 命名并按功能分组。
  • 颜色/尺寸/图标优先引用 core/designsystemcore/ui,保持全局一致。

6. 模板

参见页面模板,可直接复制 Route/Screen/ViewModel/Navigation 的标准代码;生成后根据实际业务补全数据类型与仓库调用。

完成以上步骤后,一个 Feature 页面即可接入导航、状态、UI 组件体系,后续只需聚焦业务逻辑本身。