Android - GraphQL的使用及注意事项

277 阅读2分钟

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. 查询优化
// 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
    }
}
  1. 缓存策略
// 配置缓存策略
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

总结要点:

  1. 架构设计

    • 使用 Repository 模式
    • 清晰的职责分离
    • 适当的错误处理
  2. 性能优化

    • 合理使用缓存
    • 优化查询结构
    • 处理网络问题
  3. 开发流程

    • Schema 先行
    • 代码生成
    • 类型安全
  4. 测试策略

    • 单元测试
    • 集成测试
    • Mock 测试
  5. 最佳实践

    • 版本控制
    • 文档维护
    • 性能监控

通过遵循这些原则,可以:

  1. 提高开发效率
  2. 确保代码质量
  3. 优化应用性能
  4. 提供更好的用户体验

对比 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[兼容性]

九、选择建议

  1. 选择 GraphQL 当:

    • 需要灵活的数据查询
    • 有复杂的数据关系
    • 需要实时数据
    • 多客户端适配
    • 带宽敏感
  2. 选择 REST/JSON 当:

    • 简单的CRUD操作
    • 文件上传/下载
    • 需要好的缓存支持
    • 团队更熟悉REST
    • 项目规模较小

十、总结

  1. GraphQL 优势:

    • 精确获取数据
    • 强类型系统
    • 单个请求获取多个资源
    • 实时订阅
    • 版本演进友好
  2. GraphQL 劣势:

    • 学习曲线陡
    • 需要额外的工具支持
    • 缓存较复杂
    • 文件处理不便
    • 服务端实现复杂
  3. JSON/REST 优势:

    • 简单直观
    • 广泛支持
    • 缓存机制成熟
    • 工具生态丰富
    • 容易理解和调试
  4. JSON/REST 劣势:

    • 数据过度获取
    • 多个请求问题
    • 版本控制复杂
    • 文档维护困难
    • 客户端适配成本高

通过以上对比,可以:

  1. 根据项目需求选择合适的方案
  2. 理解各自的优劣势
  3. 在适当场景下混合使用
  4. 做出更好的技术决策

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

十、开发流程总结

  1. 项目设置

    • 添加依赖
    • 配置 Apollo
    • 下载 Schema
    • 生成代码
  2. 开发步骤

    • 定义查询
    • 实现 Repository
    • 创建 ViewModel
    • 构建 UI
    • 添加测试
  3. 最佳实践

    • 使用类型安全的查询
    • 实现错误处理
    • 添加缓存策略
    • 优化性能
    • 维护文档

通过遵循这个流程,可以:

  1. 保持代码组织清晰
  2. 确保类型安全
  3. 提高开发效率
  4. 方便维护和测试