加入我们,于10月8日至10日在纽约市学习关于GraphQL Federation和API平台工程的最新技巧、趋势和新闻。加入我们参加2024年纽约市GraphQL大会
文档
免费开始
您正在查看此软件以前版本的文档。 切换到最新的稳定版本。

8. 添加详细视图


在本节中,您将编写第二个 请求有关单个

创建详细查询

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

如 $cursor 所示,添加一个名为 id 的变量。注意,这次这个变量是一个非可选类型。您无法像对 $cursor 那样传递 null。

由于是详细视图,请请求 missionPatch 的 LARGE 大小。也要请求火箭的类型和名称:

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!!)
}
}

处理协议错误

在执行查询时,可能会发生两种类型的错误:

  • 协议错误。连接问题、HTTP错误或JSON解析错误将抛出 ApolloException
  • 应用程序错误。在这种情况下, response.errors 将包含应用程序错误,而 response.data 可能是 null

抛出 ApolloException 异常时,您需要在调用周围添加 try/catch 块。

首先创建一个 LaunchDetailsState 封闭接口,它将包含 UI 的可能状态:

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

然后,在 LaunchDetails 中,将调用 execute 放在 try/catch 块中,并将结果包装到 State 中:

app/src/main/java/com/example/rocketreserver/LaunchDetails.kt
@Composable
fun LaunchDetails(launchId: String) {
var state by remember { mutableStateOf<LaunchDetailsState>(Loading) }
LaunchedEffect(Unit) {
state = try {
Success(apolloClient.query(LaunchDetailsQuery(launchId)).execute().data!!)
} catch (e: ApolloException) {
ProtocolError(e)
}
}
// Use the state

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

app/src/main/java/com/example/rocketreserver/LaunchDetails.kt
// Use the state
when (val s = state) {
Loading -> Loading()
is ProtocolError -> ErrorMessage("Oh no... A protocol error happened: ${s.exception.message}")
is Success -> LaunchDetails(s.data)
}
}

在点击发射的详细信息之前,启用飞行模式。您应该看到以下内容:

Oh no

这是好的!但这还不够。即使在协议级别上请求执行正确,它也可能包含特定于您服务器的应用错误。

处理应用错误

要处理应用错误,您可以检查 response.hasErrors()。首先,向封闭接口添加一个新状态:

app/src/main/java/com/example/rocketreserver/LaunchDetails.kt
private sealed interface LaunchDetailsState {
object Loading : LaunchDetailsState
data class ProtocolError(val exception: ApolloException) : LaunchDetailsState
data class ApplicationError(val errors: List<Error>) : LaunchDetailsState
data class Success(val data: LaunchDetailsQuery.Data) : LaunchDetailsState
}

然后,在 try 块中,检查 response.hasErrors() 并将结果包装到新状态中:

app/src/main/java/com/example/rocketreserver/LaunchDetails.kt
state = try {
val response = apolloClient.query(LaunchDetailsQuery(launchId)).execute()
if (response.hasErrors()) {
ApplicationError(response.errors!!)
} else {
Success(response.data!!)
}
} catch (e: ApolloException) {
ProtocolError(e)
}

您还应该更新条件表达式来处理 ApplicationError 情况:

app/src/main/java/com/example/rocketreserver/LaunchDetails.kt
when (val s = state) {
Loading -> Loading()
is ApplicationError -> ErrorMessage(text = s.errors.first().message)
is ProtocolError -> ErrorMessage("Oh no... A protocol error happened: ${s.exception.message}")
is Success -> LaunchDetails(s.data)
}

response.errors 包含任何发生的错误详情。注意,此代码也对 response.data!! 进行了空值检查。在理论上,服务器不应同时设置 response.data == nullresponse.hasErrors == false,但是类型系统无法保证这一点。

要触发错误,将 LaunchDetailsQuery(launchId) 替换为 LaunchDetailsQuery("invalidId")。禁用飞行模式并选择一个发射物。服务器将发送以下响应:

(错误)
{
"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中声明

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。

隐私政策

公司