8. 添加详细信息视图
在本节中,您将编写第二个GraphQL 查询,以请求关于单个 发射 的详细信息。
创建详细信息查询
创建一个名为 LaunchDetails.graphql
。
类似于对 $cursor
的操作,添加一个名为 id
的变量。注意,这次这个变量为非可选类型。您将无法像对 $cursor
那样传递 null
。
因为这是一个详细信息视图,所以请请求大小为 LARGE
的 missionPatch。同时请求火箭类型和名称:
query LaunchDetails($id: ID!) {launch(id: $id) {idsitemission {namemissionPatch(size: LARGE)}rocket {nametype}isBooked}}
记得你始终可以在工作室资源管理器中实验,并查看左侧边栏以获取可用字段列表。字段。
执行查询并更新UI
在LaunchDetails.kt
中,声明response
和一个LaunchedEffect
以执行查询:
@Composablefun LaunchDetails(launchId: String) {var response by remember { mutableStateOf<ApolloResponse<LaunchDetailsQuery.Data>?>(null) }LaunchedEffect(Unit) {response = apolloClient.query(LaunchDetailsQuery(launchId)).execute()}
使用响应在UI中。这里我们也将使用Coil的AsyncImage
来处理占位符:
Column(modifier = Modifier.padding(16.dp)) {Row(verticalAlignment = Alignment.CenterVertically) {// Mission patchAsyncImage(modifier = Modifier.size(160.dp, 160.dp),model = response?.data?.launch?.mission?.missionPatch,placeholder = painterResource(R.drawable.ic_placeholder),error = painterResource(R.drawable.ic_placeholder),contentDescription = "Mission patch")Spacer(modifier = Modifier.size(16.dp))Column {// Mission nameText(style = MaterialTheme.typography.headlineMedium,text = response?.data?.launch?.mission?.name ?: "")// Rocket nameText(modifier = Modifier.padding(top = 8.dp),style = MaterialTheme.typography.headlineSmall,text = response?.data?.launch?.rocket?.name?.let { "🚀 $it" } ?: "",)// SiteText(modifier = Modifier.padding(top = 8.dp),style = MaterialTheme.typography.titleMedium,text = response?.data?.launch?.site ?: "",)}}
显示加载状态
由于response
被初始化为null
,你可以用这个作为结果尚未接收到的指示。
为了使代码结构更清晰,提取详情UI到一个单独的函数中,该函数以响应作为参数
@Composableprivate fun LaunchDetails(response: ApolloResponse<LaunchDetailsQuery.Data>) {Column(modifier = Modifier.padding(16.dp)) {
现在在原始的LaunchDetails
函数中,检查response
是否为null
,如果是,则显示一个加载指示器,否则调用带有响应的新函数:
@Composablefun LaunchDetails(launchId: String) {var response by remember { mutableStateOf<ApolloResponse<LaunchDetailsQuery.Data>?>(null) }LaunchedEffect(Unit) {response = apolloClient.query(LaunchDetailsQuery(launchId)).execute()}if (response == null) {Loading()} else {LaunchDetails(response!!)}}
处理错误
在执行你的查询时,可能会发生不同类型的错误:
- 获取错误:连接问题、HTTP错误、JSON解析错误等。在这种情况下,
response.exception
包含一个ApolloException
。既没有response.data
也没有response.errors
,都是null。 - GraphQL 请求错误: 在这种情况下,
response.errors
包含 GraphQL 错误。response.data
是 null。 - GraphQL 字段错误: 在这种情况下,
response.errors
包含 GraphQL 错误。response.data
包含部分数据。
现在我们先处理前两个:获取错误和 GraphQL 请求错误。
首先创建一个 LaunchDetailsState
封闭接口,以保留 UI 可能的状态:
private sealed interface LaunchDetailsState {object Loading : LaunchDetailsStatedata class Error(val message: String) : LaunchDetailsStatedata class Success(val data: LaunchDetailsQuery.Data) : LaunchDetailsState}
然后在 LaunchDetails
中,检查 execute
返回的响应并将其映射到 State
:
@Composablefun LaunchDetails(launchId: String) {var state by remember { mutableStateOf<LaunchDetailsState>(Loading) }LaunchedEffect(Unit) {val response = apolloClient.query(LaunchDetailsQuery(launchId)).execute()state = when {response.data != null -> {// Handle (potentially partial) dataLaunchDetailsState.Success(response.data!!)}else -> {LaunchDetailsState.Error("Oh no... An error happened.")}}}// Use the state
现在使用该状态显示适当的 UI
// Use the statewhen (val s = state) {Loading -> Loading()is Error -> ErrorMessage(s.message)is Success -> LaunchDetails(s.data)}}
点击发射详情之前启用飞行模式。你应该看到这个:
这很好!
此方法处理获取和 GraphQL 请求错误,但忽略 GraphQL 字段 错误。
如果出现 GraphQL 字段 错误,相应的 Kotlin 属性是 null
并且在 response.errors
中存在错误:你的响应包含部分数据!
在 UI 代码中处理这种部分数据可能会很复杂。您可以通过查看 response.errors
早期处理它们。
处理部分数据
要全局处理 GraphQL 字段 错误并确保返回的数据不是部分数据,请使用 response.errors
:
state = when {response.errors.orEmpty().isNotEmpty() -> {// GraphQL errorLaunchDetailsState.Error(response.errors!!.first().message)}response.exception is ApolloNetworkException -> {// Network errorLaunchDetailsState.Error("Please check your network connectivity.")}response.data != null -> {// data (never partial)LaunchDetailsState.Success(response.data!!)}else -> {// Another fetch error, maybe a cache miss?// Or potentially a non-compliant server returning data: null without an errorLaunchDetailsState.Error("Oh no... An error happened.")}}
response.errors
包含发生的任何错误的详细信息。请注意,此代码还检查了 response.data!!
。理论上,服务器不应该同时将 response.data == null
和 response.hasErrors == false
设置为 true,但类型系统无法保证这一点。
要触发 GraphQL 字段 错误,将 LaunchDetailsQuery(launchId)
替换为 LaunchDetailsQuery("invalidId")
。禁用飞行模式并选择一个 launch。服务器将发送此响应:
{"errors": [{"message": "Cannot read property 'flight_number' of undefined","locations": [{"line": 1,"column": 32}],"path": ["launch"],"extensions": {"code": "INTERNAL_SERVER_ERROR"}}],"data": {"launch": null}}
一切正常!您可以使用errors
字段添加更高级的错误管理。
在显示详细信息之前,请恢复正确的启动 ID:LaunchDetailsQuery(launchId)
。
处理 立即预订 按钮
要预订行程,用户必须登录。如果用户未登录,则点击 立即预订按钮应打开登录界面。
首先,让我们将一个lambda传递到LaunchDetails
以处理导航:
@Composablefun LaunchDetails(launchId: String, navigateToLogin: () -> Unit) {
lambda应在MainActivity中声明,在该Activity中处理导航
composable(route = "${NavigationDestinations.LAUNCH_DETAILS}/{${NavigationArguments.LAUNCH_ID}}") { navBackStackEntry ->LaunchDetails(launchId = navBackStackEntry.arguments!!.getString(NavigationArguments.LAUNCH_ID)!!,navigateToLogin = {navController.navigate(NavigationDestinations.LOGIN)})}
接下来,返回到LaunchDetails.kt
并替换TODO
为对处理按钮点击的函数的调用:
onClick = {onBookButtonClick(launchId = data.launch?.id ?: "",isBooked = data.launch?.isBooked == true,navigateToLogin = navigateToLogin)}
private fun onBookButtonClick(launchId: String, isBooked: Boolean, navigateToLogin: () -> Unit): Boolean {if (TokenRepository.getToken() == null) {navigateToLogin()return false}if (isBooked) {// TODO Cancel booking} else {// TODO Book}return false}
TokenRepository
是一个辅助类,它处理在EncryptedSharedPreference中保存和检索用户令牌。我们将在登录时使用它来存储用户令牌。
返回一个布尔值将有助于以后根据执行是否发生来更新UI。
测试按钮
点击 运行。您的屏幕应如下所示:
目前您尚未登录,因此您无法预订行程,点击将始终导航到登录界面。
接下来,您将编写您的第一个突变 以登录后端。