加入我们,于10月8日至10日在纽约市学习有关GraphQL联邦和API平台工程的最新技巧、趋势和新闻。加入纽约市2024年GraphQL峰会
文档
免费开始

8. 添加详细信息视图


在本节中,您将编写第二个 ,以请求关于单个 的详细信息。

创建详细信息查询

创建一个名为 GraphQL 查询LaunchDetails.graphql

类似于对 $cursor 的操作,添加一个名为 id 的变量。注意,这次这个变量为非可选类型。您将无法像对 $cursor 那样传递 null

因为这是一个详细信息视图,所以请请求大小为 LARGE 的 missionPatch。同时请求火箭类型和名称:

app/src/main/graphql/LaunchDetails.graphql
query LaunchDetails($id: ID!) {
launch(id: $id) {
id
site
mission {
name
missionPatch(size: LARGE)
}
rocket {
name
type
}
isBooked
}
}

记得你始终可以在工作室资源管理器中实验,并查看左侧边栏以获取可用字段列表。

执行查询并更新UI

LaunchDetails.kt中,声明response和一个LaunchedEffect以执行查询:

app/src/main/java/com/example/rocketreserver/LaunchDetails.kt
@Composable
fun LaunchDetails(launchId: String) {
var response by remember { mutableStateOf<ApolloResponse<LaunchDetailsQuery.Data>?>(null) }
LaunchedEffect(Unit) {
response = apolloClient.query(LaunchDetailsQuery(launchId)).execute()
}

使用响应在UI中。这里我们也将使用Coil的AsyncImage来处理占位符:

app/src/main/java/com/example/rocketreserver/LaunchDetails.kt
Column(
modifier = Modifier.padding(16.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
// Mission patch
AsyncImage(
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 name
Text(
style = MaterialTheme.typography.headlineMedium,
text = response?.data?.launch?.mission?.name ?: ""
)
// Rocket name
Text(
modifier = Modifier.padding(top = 8.dp),
style = MaterialTheme.typography.headlineSmall,
text = response?.data?.launch?.rocket?.name?.let { "🚀 $it" } ?: "",
)
// Site
Text(
modifier = Modifier.padding(top = 8.dp),
style = MaterialTheme.typography.titleMedium,
text = response?.data?.launch?.site ?: "",
)
}
}

显示加载状态

由于response被初始化为null,你可以用这个作为结果尚未接收到的指示。

为了使代码结构更清晰,提取详情UI到一个单独的函数中,该函数以响应作为参数

app/src/main/java/com/example/rocketreserver/LaunchDetails.kt
@Composable
private fun LaunchDetails(response: ApolloResponse<LaunchDetailsQuery.Data>) {
Column(
modifier = Modifier.padding(16.dp)
) {

现在在原始的LaunchDetails函数中,检查response是否为null,如果是,则显示一个加载指示器,否则调用带有响应的新函数:

app/src/main/java/com/example/rocketreserver/LaunchDetails.kt
@Composable
fun 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!!)
}
}

处理错误

在执行你的查询时,可能会发生不同类型的错误:

现在我们先处理前两个:获取错误和 GraphQL 请求错误。

首先创建一个 LaunchDetailsState 封闭接口,以保留 UI 可能的状态:

app/src/main/java/com/example/rocketreserver/LaunchDetails.kt
private sealed interface LaunchDetailsState {
object Loading : LaunchDetailsState
data class Error(val message: String) : LaunchDetailsState
data class Success(val data: LaunchDetailsQuery.Data) : LaunchDetailsState
}

然后在 LaunchDetails 中,检查 execute 返回的响应并将其映射到 State

app/src/main/java/com/example/rocketreserver/LaunchDetails.kt
@Composable
fun 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) data
LaunchDetailsState.Success(response.data!!)
}
else -> {
LaunchDetailsState.Error("Oh no... An error happened.")
}
}
}
// Use the state

现在使用该状态显示适当的 UI

app/src/main/java/com/example/rocketreserver/LaunchDetails.kt
// Use the state
when (val s = state) {
Loading -> Loading()
is Error -> ErrorMessage(s.message)
is Success -> LaunchDetails(s.data)
}
}

点击发射详情之前启用飞行模式。你应该看到这个:

Oh no

这很好!

此方法处理获取和 GraphQL 请求错误,但忽略 GraphQL 错误。

如果出现 GraphQL 字段 错误,相应的 Kotlin 属性是 null 并且在 response.errors 中存在错误:你的响应包含部分数据!

在 UI 代码中处理这种部分数据可能会很复杂。您可以通过查看 response.errors 早期处理它们。

处理部分数据

要全局处理 GraphQL 字段 错误并确保返回的数据不是部分数据,请使用 response.errors:

app/src/main/java/com/example/rocketreserver/LaunchDetails.kt
state = when {
response.errors.orEmpty().isNotEmpty() -> {
// GraphQL error
LaunchDetailsState.Error(response.errors!!.first().message)
}
response.exception is ApolloNetworkException -> {
// Network error
LaunchDetailsState.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 error
LaunchDetailsState.Error("Oh no... An error happened.")
}
}

response.errors 包含发生的任何错误的详细信息。请注意,此代码还检查了 response.data!!。理论上,服务器不应该同时将 response.data == nullresponse.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
}
}
Oh no

一切正常!您可以使用errors 字段添加更高级的错误管理。

在显示详细信息之前,请恢复正确的启动 ID:LaunchDetailsQuery(launchId)

处理 立即预订 按钮

要预订行程,用户必须登录。如果用户未登录,则点击 立即预订按钮应打开登录界面。

首先,让我们将一个lambda传递到LaunchDetails以处理导航:

app/src/main/java/com/example/rocketreserver/LaunchDetails.kt
@Composable
fun LaunchDetails(launchId: String, navigateToLogin: () -> Unit) {

lambda应在MainActivity中声明,在该Activity中处理导航

app/src/main/java/com/example/rocketreserver/MainActivity.kt
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为对处理按钮点击的函数的调用:

app/src/main/java/com/example/rocketreserver/LaunchDetails.kt
onClick = {
onBookButtonClick(
launchId = data.launch?.id ?: "",
isBooked = data.launch?.isBooked == true,
navigateToLogin = navigateToLogin
)
}
app/src/main/java/com/example/rocketreserver/LaunchDetails.kt
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。

测试按钮

点击 运行。您的屏幕应如下所示:

Details

目前您尚未登录,因此您无法预订行程,点击将始终导航到登录界面。

   接下来,您将编写您的第一个突变   以登录后端。

上一页
7. 分页结果
下一页
9. 编写您的第一个突变
评分文章评分在GitHub上编辑编辑论坛Discord

©2024Apollo Graph Inc.,商号Apollo GraphQL。

隐私政策

公司