GraphQL基本使用
一、整体架构
graph TD
A[Android Client] --> B[Apollo Android Client]
B --> C[GraphQL Schema]
C --> D[GraphQL Server]
D --> E[Database]
F[Code Generation] --> G[Generated Models]
G --> A
二、基本配置
// 1. build.gradle 配置
plugins {
id("com.apollographql.apollo3") version "3.x.x"
}
dependencies {
implementation("com.apollographql.apollo3:apollo-runtime:3.x.x")
implementation("com.apollographql.apollo3:apollo-coroutines-support:3.x.x")
}
apollo {
service("service") {
packageName.set("com.example.graphql")
schemaFile.set(file("src/main/graphql/schema.graphqls"))
}
}
// 2. Apollo Client 初始化
class GraphQLConfig {
fun createApolloClient(): ApolloClient {
return ApolloClient.Builder()
.serverUrl("https://your-graphql-server.com/graphql")
.addHttpHeader("Authorization", "Bearer $token")
.build()
}
}
三、查询定义
# 1. 基本查询 (user.graphql)
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
}
}
}
# 2. 变更操作 (updateUser.graphql)
mutation UpdateUser($id: ID!, $input: UserInput!) {
updateUser(id: $id, input: $input) {
id
name
email
}
}
# 3. 订阅 (userUpdates.graphql)
subscription OnUserUpdate($userId: ID!) {
userUpdated(userId: $userId) {
id
name
status
}
}
四、Repository 实现
// 1. Repository 定义
class UserRepository(
private val apolloClient: ApolloClient
) {
// 查询用户
suspend fun getUser(id: String): User? {
return try {
val response = apolloClient.query(GetUserQuery(id))
.execute()
response.data?.user?.toUser()
} catch (e: ApolloException) {
null
}
}
// 更新用户
suspend fun updateUser(id: String, input: UserInput): Result<User> {
return try {
val response = apolloClient.mutation(
UpdateUserMutation(id, input)
).execute()
if (response.hasErrors()) {
Result.failure(Exception(response.errors?.first()?.message))
} else {
Result.success(response.data?.updateUser?.toUser()!!)
}
} catch (e: ApolloException) {
Result.failure(e)
}
}
// 订阅用户更新
fun subscribeToUserUpdates(userId: String): Flow<User> {
return apolloClient.subscription(OnUserUpdateSubscription(userId))
.toFlow()
.map { it.data?.userUpdated?.toUser()!! }
}
}
五、ViewModel 集成
// 1. ViewModel 实现
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun loadUser(id: String) {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val user = repository.getUser(id)
_uiState.value = UiState.Success(user)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message)
}
}
}
// 处理订阅
fun observeUserUpdates(userId: String) {
viewModelScope.launch {
repository.subscribeToUserUpdates(userId)
.catch { e ->
_uiState.value = UiState.Error(e.message)
}
.collect { user ->
_uiState.value = UiState.Success(user)
}
}
}
}
六、UI 实现
// 1. Compose UI
@Composable
fun UserScreen(
viewModel: UserViewModel,
userId: String
) {
val uiState by viewModel.uiState.collectAsState()
LaunchedEffect(userId) {
viewModel.loadUser(userId)
viewModel.observeUserUpdates(userId)
}
when (val state = uiState) {
is UiState.Loading -> LoadingIndicator()
is UiState.Success -> UserContent(state.user)
is UiState.Error -> ErrorMessage(state.message)
}
}
// 2. 缓存处理
class CacheConfig {
fun configureCache(apolloClient: ApolloClient): ApolloClient {
return apolloClient.toBuilder()
.normalizedCache(
MemoryCacheFactory()
)
.build()
}
}
七、错误处理和优化
// 1. 错误处理
class ErrorHandler {
fun handleGraphQLError(error: ApolloException): ErrorResult {
return when (error) {
is ApolloNetworkException -> ErrorResult.Network
is ApolloParseException -> ErrorResult.Parse
is ApolloCanceledException -> ErrorResult.Cancelled
else -> ErrorResult.Unknown
}
}
}
// 2. 性能优化
class PerformanceOptimization {
fun optimizeQueries(apolloClient: ApolloClient) {
apolloClient.toBuilder()
.defaultCachePolicy(CachePolicy.CACHE_FIRST)
.enableAutoPersistedQueries()
.build()
}
}
八、测试
// 1. Repository 测试
class UserRepositoryTest {
@Test
fun `test user query`() = runTest {
val mockApolloClient = MockApolloClient.Builder()
.mockQuery(GetUserQuery::class) {
mockUserData
}
.build()
val repository = UserRepository(mockApolloClient)
val result = repository.getUser("123")
assertEquals(expectedUser, result)
}
}
// 2. ViewModel 测试
class UserViewModelTest {
@Test
fun `test loading state`() = runTest {
val viewModel = UserViewModel(mockRepository)
viewModel.loadUser("123")
assertEquals(UiState.Loading, viewModel.uiState.value)
}
}
九、最佳实践和注意事项
- 查询优化
// 1. 选择性获取字段
query GetUserBasic($id: ID!) {
user(id: $id) {
id
name
# 只获取需要的字段
}
}
// 2. 使用片段
fragment UserFields on User {
id
name
email
}
query GetUser($id: ID!) {
user(id: $id) {
...UserFields
}
}
- 缓存策略
// 配置缓存策略
val apolloClient = ApolloClient.Builder()
.normalizedCache(
MemoryCacheFactory(),
CacheKeyResolver()
)
.defaultCachePolicy(CachePolicy.CACHE_FIRST)
.build()
十、完整流程图
sequenceDiagram
participant App
participant ViewModel
participant Repository
participant ApolloClient
participant GraphQLServer
App->>ViewModel: 请求数据
ViewModel->>Repository: 调用查询
Repository->>ApolloClient: 执行 GraphQL 操作
ApolloClient->>GraphQLServer: 发送请求
GraphQLServer-->>ApolloClient: 返回数据
ApolloClient-->>Repository: 处理响应
Repository-->>ViewModel: 转换数据
ViewModel-->>App: 更新 UI
总结要点:
-
架构设计
- 使用 Repository 模式
- 清晰的职责分离
- 适当的错误处理
-
性能优化
- 合理使用缓存
- 优化查询结构
- 处理网络问题
-
开发流程
- Schema 先行
- 代码生成
- 类型安全
-
测试策略
- 单元测试
- 集成测试
- Mock 测试
-
最佳实践
- 版本控制
- 文档维护
- 性能监控
通过遵循这些原则,可以:
- 提高开发效率
- 确保代码质量
- 优化应用性能
- 提供更好的用户体验
对比 GraphQL 和 JSON 的优劣势
一、基本概念对比
graph TD
A[数据交换格式] --> B[GraphQL]
A --> C[JSON]
B --> D[查询语言]
B --> E[类型系统]
B --> F[实时订阅]
C --> G[数据格式]
C --> H[REST API]
C --> I[简单直观]
二、语法对比
// 1. GraphQL 查询
"""
query GetUser {
user(id: "123") {
name
email
posts {
title
comments {
text
}
}
}
}
"""
// 2. JSON/REST API
"""
GET /api/users/123
{
"name": "John",
"email": "john@example.com",
"posts": [
{
"title": "Post 1",
"content": "...",
"comments": [...]
}
]
}
"""
三、主要差异对比
1. 数据获取
// 1. GraphQL - 精确获取
class GraphQLExample {
// 只获取需要的字段
val query = """
query {
user(id: "123") {
name // 只要名字
email // 只要邮箱
}
}
"""
}
// 2. REST/JSON - 完整响应
class RestExample {
// 获取完整用户对象
@GET("users/{id}")
fun getUser(@Path("id") id: String): User {
// 返回所有字段,可能包含不需要的数据
}
}
2. 多端点请求
// 1. GraphQL - 单个请求
class GraphQLMultiRequest {
val query = """
query {
user(id: "123") {
name
posts { title }
followers { name }
}
}
"""
}
// 2. REST/JSON - 多个请求
class RestMultiRequest {
// 需要多个接口调用
fun getUserData(userId: String) {
val user = api.getUser(userId)
val posts = api.getUserPosts(userId)
val followers = api.getUserFollowers(userId)
}
}
四、优势对比
1. GraphQL 优势
// 1. 类型安全
class GraphQLAdvantages {
// Schema 定义
"""
type User {
id: ID!
name: String!
age: Int
posts: [Post!]!
}
"""
// 自动生成类型安全的代码
class Query {
fun user(id: String): User
}
}
// 2. 版本控制
class GraphQLVersioning {
// 添加新字段不影响现有查询
"""
type User {
id: ID!
name: String!
// 新增字段
avatar: String
}
"""
}
2. JSON/REST 优势
// 1. 简单直观
class JsonAdvantages {
// 简单的数据结构
data class User(
val id: String,
val name: String
)
// 直观的 API 设计
@GET("users/{id}")
fun getUser(@Path("id") id: String): User
}
// 2. 缓存友好
class RestCaching {
@GET("users")
@Headers("Cache-Control: max-age=3600")
fun getUsers(): List<User>
}
五、性能对比
// 1. GraphQL 性能考虑
class GraphQLPerformance {
// 1. 数据过度获取
val goodQuery = """
query {
user(id: "123") {
name // 只获取需要的
}
}
"""
// 2. 批量查询优化
val batchQuery = """
query {
users(ids: ["1", "2", "3"]) {
name
}
}
"""
}
// 2. REST/JSON 性能考虑
class RestPerformance {
// 1. 数据冗余
@GET("users/{id}")
fun getUser(@Path("id") id: String): User // 返回所有字段
// 2. 多次请求
suspend fun getUsersData(ids: List<String>) {
ids.forEach { id ->
api.getUser(id) // N+1 问题
}
}
}
六、使用场景对比
// 1. GraphQL 适用场景
class GraphQLUseCases {
// 1. 复杂数据关系
val complexQuery = """
query {
organization {
departments {
employees {
projects {
tasks
}
}
}
}
}
"""
// 2. 实时数据
val subscription = """
subscription {
userStatus(id: "123") {
online
lastSeen
}
}
"""
}
// 2. REST/JSON 适用场景
class RestUseCases {
// 1. 简单CRUD操作
@POST("users")
fun createUser(@Body user: User)
// 2. 文件上传
@Multipart
@POST("upload")
fun uploadFile(@Part file: MultipartBody.Part)
}
七、开发成本对比
// 1. GraphQL 开发成本
class GraphQLDevelopment {
// 1. Schema 定义
"""
type Query {
user(id: ID!): User
posts: [Post!]!
}
"""
// 2. 类型生成
apollo {
generateKotlinModels.set(true)
}
}
// 2. REST/JSON 开发成本
class RestDevelopment {
// 1. 简单接口定义
interface ApiService {
@GET("users")
fun getUsers(): List<User>
}
// 2. 序列化配置
@JsonSerializable
data class User(val name: String)
}
八、维护成本对比
graph LR
A[维护成本] --> B[GraphQL]
A --> C[REST/JSON]
B --> D[Schema维护]
B --> E[类型更新]
B --> F[查询优化]
C --> G[接口版本]
C --> H[文档更新]
C --> I[兼容性]
九、选择建议
-
选择 GraphQL 当:
- 需要灵活的数据查询
- 有复杂的数据关系
- 需要实时数据
- 多客户端适配
- 带宽敏感
-
选择 REST/JSON 当:
- 简单的CRUD操作
- 文件上传/下载
- 需要好的缓存支持
- 团队更熟悉REST
- 项目规模较小
十、总结
-
GraphQL 优势:
- 精确获取数据
- 强类型系统
- 单个请求获取多个资源
- 实时订阅
- 版本演进友好
-
GraphQL 劣势:
- 学习曲线陡
- 需要额外的工具支持
- 缓存较复杂
- 文件处理不便
- 服务端实现复杂
-
JSON/REST 优势:
- 简单直观
- 广泛支持
- 缓存机制成熟
- 工具生态丰富
- 容易理解和调试
-
JSON/REST 劣势:
- 数据过度获取
- 多个请求问题
- 版本控制复杂
- 文档维护困难
- 客户端适配成本高
通过以上对比,可以:
- 根据项目需求选择合适的方案
- 理解各自的优劣势
- 在适当场景下混合使用
- 做出更好的技术决策
Android 中使用 GraphQL 的完整开发流程
一、项目初始化流程
graph TD
A[项目配置] --> B[添加依赖]
B --> C[Schema下载]
C --> D[代码生成]
D --> E[Apollo Client配置]
E --> F[开发实现]
二、基础配置
// 1. build.gradle (项目级)
buildscript {
dependencies {
classpath("com.apollographql.apollo3:apollo-gradle-plugin:3.x.x")
}
}
// 2. build.gradle (模块级)
plugins {
id("com.apollographql.apollo3")
}
dependencies {
implementation("com.apollographql.apollo3:apollo-runtime:3.x.x")
implementation("com.apollographql.apollo3:apollo-coroutines-support:3.x.x")
implementation("com.apollographql.apollo3:apollo-normalized-cache:3.x.x")
}
apollo {
service("api") {
packageName.set("com.example.graphql")
schemaFile.set(file("src/main/graphql/schema.graphqls"))
generateKotlinModels.set(true)
}
}
三、Schema 和查询定义
# 1. schema.graphqls
type Query {
users: [User!]!
user(id: ID!): User
}
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
}
# 2. queries/user.graphql
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
}
}
}
# 3. mutations/updateUser.graphql
mutation UpdateUser($id: ID!, $input: UserInput!) {
updateUser(id: $id, input: $input) {
id
name
email
}
}
四、Apollo Client 配置
// 1. Apollo Client 设置
class ApolloConfig @Inject constructor(
private val context: Context
) {
fun createApolloClient(): ApolloClient {
// 创建 HTTP 拦截器
val loggingInterceptor = HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BODY)
// 创建 OkHttpClient
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer ${getToken()}")
.build()
chain.proceed(request)
}
.build()
// 配置 Apollo Client
return ApolloClient.Builder()
.serverUrl("https://your-graphql-server.com/graphql")
.okHttpClient(okHttpClient)
.normalizedCache(
normalizedCacheFactory = MemoryCacheFactory(),
cacheKeyGenerator = TypePolicyCacheKeyGenerator()
)
.build()
}
}
// 2. DI 配置
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideApolloClient(config: ApolloConfig): ApolloClient {
return config.createApolloClient()
}
}
五、Repository 实现
// 1. Repository 接口
interface UserRepository {
suspend fun getUser(id: String): Result<User>
suspend fun updateUser(id: String, input: UserInput): Result<User>
fun observeUserUpdates(id: String): Flow<User>
}
// 2. Repository 实现
class UserRepositoryImpl @Inject constructor(
private val apolloClient: ApolloClient
) : UserRepository {
override suspend fun getUser(id: String): Result<User> {
return try {
val response = apolloClient.query(GetUserQuery(id))
.execute()
if (response.hasErrors()) {
Result.failure(Exception(response.errors?.first()?.message))
} else {
val user = response.data?.user?.toUser()
if (user != null) {
Result.success(user)
} else {
Result.failure(Exception("User not found"))
}
}
} catch (e: ApolloException) {
Result.failure(e)
}
}
override suspend fun updateUser(
id: String,
input: UserInput
): Result<User> {
return try {
val response = apolloClient.mutation(
UpdateUserMutation(id, input)
).execute()
if (response.hasErrors()) {
Result.failure(Exception(response.errors?.first()?.message))
} else {
val updatedUser = response.data?.updateUser?.toUser()
if (updatedUser != null) {
Result.success(updatedUser)
} else {
Result.failure(Exception("Update failed"))
}
}
} catch (e: ApolloException) {
Result.failure(e)
}
}
override fun observeUserUpdates(id: String): Flow<User> {
return apolloClient.subscription(UserUpdatedSubscription(id))
.toFlow()
.map { it.data?.userUpdated?.toUser() ?: throw Exception("Invalid data") }
}
}
六、ViewModel 实现
@HiltViewModel
class UserViewModel @Inject constructor(
private val userRepository: UserRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Initial)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
init {
// 获取保存的用户ID
savedStateHandle.get<String>("userId")?.let { userId ->
loadUser(userId)
}
}
fun loadUser(id: String) {
viewModelScope.launch {
_uiState.value = UiState.Loading
userRepository.getUser(id)
.onSuccess { user ->
_uiState.value = UiState.Success(user)
}
.onFailure { error ->
_uiState.value = UiState.Error(error.message)
}
}
}
fun updateUser(id: String, input: UserInput) {
viewModelScope.launch {
_uiState.value = UiState.Loading
userRepository.updateUser(id, input)
.onSuccess { user ->
_uiState.value = UiState.Success(user)
}
.onFailure { error ->
_uiState.value = UiState.Error(error.message)
}
}
}
fun observeUpdates(id: String) {
viewModelScope.launch {
userRepository.observeUserUpdates(id)
.catch { error ->
_uiState.value = UiState.Error(error.message)
}
.collect { user ->
_uiState.value = UiState.Success(user)
}
}
}
}
七、UI 实现
@Composable
fun UserScreen(
viewModel: UserViewModel = hiltViewModel(),
userId: String
) {
val uiState by viewModel.uiState.collectAsState()
LaunchedEffect(userId) {
viewModel.loadUser(userId)
viewModel.observeUpdates(userId)
}
when (val state = uiState) {
is UiState.Initial -> {
// 初始状态
}
is UiState.Loading -> {
LoadingIndicator()
}
is UiState.Success -> {
UserContent(
user = state.user,
onUpdateClick = { input ->
viewModel.updateUser(userId, input)
}
)
}
is UiState.Error -> {
ErrorMessage(
message = state.message,
onRetry = {
viewModel.loadUser(userId)
}
)
}
}
}
@Composable
fun UserContent(
user: User,
onUpdateClick: (UserInput) -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(text = user.name)
Text(text = user.email)
// 用户帖子列表
LazyColumn {
items(user.posts) { post ->
PostItem(post = post)
}
}
}
}
八、错误处理
// 1. 错误处理工具
sealed class GraphQLError : Exception() {
object Network : GraphQLError()
object Authentication : GraphQLError()
data class Server(override val message: String) : GraphQLError()
data class Client(override val message: String) : GraphQLError()
}
// 2. 错误处理扩展
fun ApolloResponse<*>.handleErrors(): GraphQLError? {
return when {
hasErrors() -> {
val error = errors?.first()
when (error?.extensions?.get("code")) {
"UNAUTHENTICATED" -> GraphQLError.Authentication
"INTERNAL_SERVER_ERROR" -> GraphQLError.Server(error.message)
else -> GraphQLError.Client(error?.message ?: "Unknown error")
}
}
else -> null
}
}
九、测试
// 1. Repository 测试
class UserRepositoryTest {
private lateinit var apolloClient: ApolloClient
private lateinit var repository: UserRepository
@Before
fun setup() {
apolloClient = MockApolloClient.Builder()
.mockQuery(GetUserQuery::class) {
mockUserData
}
.build()
repository = UserRepositoryImpl(apolloClient)
}
@Test
fun `test get user success`() = runTest {
val result = repository.getUser("123")
assertTrue(result.isSuccess)
}
}
// 2. ViewModel 测试
class UserViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
private lateinit var viewModel: UserViewModel
private lateinit var repository: FakeUserRepository
@Before
fun setup() {
repository = FakeUserRepository()
viewModel = UserViewModel(repository)
}
@Test
fun `load user updates ui state`() = runTest {
viewModel.loadUser("123")
assertEquals(UiState.Loading, viewModel.uiState.value)
advanceTimeBy(1000)
assertTrue(viewModel.uiState.value is UiState.Success)
}
}
十、开发流程总结
-
项目设置
- 添加依赖
- 配置 Apollo
- 下载 Schema
- 生成代码
-
开发步骤
- 定义查询
- 实现 Repository
- 创建 ViewModel
- 构建 UI
- 添加测试
-
最佳实践
- 使用类型安全的查询
- 实现错误处理
- 添加缓存策略
- 优化性能
- 维护文档
通过遵循这个流程,可以:
- 保持代码组织清晰
- 确保类型安全
- 提高开发效率
- 方便维护和测试