You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Firestore泛型仓库问题:Map替代POJO查询后类型信息丢失求助

泛型Firestore仓库Map操作的类型丢失问题

问题描述

我正在实现一个针对Firestore集合的泛型仓库,使用Map而非POJO进行数据操作。目前已完成一个函数,可从指定文档中仅检索所需字段,将字段转换为期望类型后,将新Map封装在RequestState密封类中返回。但在函数外部使用返回结果时遇到问题:必须将结果强制转换为Map<String, Any>,导致所有类型信息丢失。

现有代码

sealed class RequestState(val result: Any?) {
    class Error(exception: Exception) : RequestState(exception)
    class Loading() : RequestState(null)
    class Success(data: Any) : RequestState(data)
}

override suspend fun retrieveFields(
    documentName: String,
    fields: List<String>,
    expectedType: List<KClass<*>>
): Flow<RequestState> = callbackFlow {
    try {
        // 检查fields或expectedType是否为空
        if (fields.isEmpty() || expectedType.isEmpty()) throw EmptyArgument(requestName = "Retrieve Fields")

        // 检查fields和expectedType的大小是否一致
        if (fields.size != expectedType.size) throw InvalidSize(
            requestName = "Retrieve Fields",
            cause = "参数数量不匹配"
        )

        trySend(RequestState.Loading())

        // 获取指定名称的文档
        this@CollectionRepositoryImpl.collection.document(documentName).get()
            .addOnFailureListener { exception ->
                // 处理失败并关闭流
                trySend(RequestState.Error(exception))
                close()
            }.addOnSuccessListener { document ->
                // 创建可变Map存储获取到的字段
                val resultMap = mutableMapOf<String, Any>()
                document.data?.let { map ->
                    // 遍历字段和期望类型
                    fields.onEachIndexed { index, fieldName ->
                        if (fieldName in map.keys) {
                            // 存储字段及其期望类型到resultMap
                            val data = document to expectedType[index]
                            resultMap[fieldName] = data
                        } else {
                            Log.w(
                                "Test",
                                "文档$documentName中未找到字段$fieldName"
                            )
                            throw NoSuchField(fieldName, "Retrieve Fields")
                        }
                    }
                }
                // 发送成功结果并关闭流
                trySend(RequestState.Success(resultMap))
                close()
            }

        awaitClose()
    } catch (e: Exception) {
        // 处理异常并关闭流
        trySend(RequestState.Error(e))
        close()
    }
}

解决方案

1. 泛型改造RequestState密封类

让密封类支持泛型,使Success状态能携带具体类型信息,避免后续强制转换丢失类型:

sealed class RequestState<out T> {
    data class Error(val exception: Exception) : RequestState<Nothing>()
    object Loading : RequestState<Nothing>()
    data class Success<out T>(val data: T) : RequestState<T>()
}

将result字段拆分到各个子类,既符合密封类的职责单一设计,又通过泛型明确了数据类型。

2. 优化retrieveFields函数的类型安全

原代码存在两个问题:一是用两个独立列表传递字段和类型,容易出现索引匹配错误;二是错误地将DocumentSnapshot与类型配对存储,而非字段值。以下是修正后的泛型版本:

override suspend fun <T : Map<String, *>> retrieveFields(
    documentName: String,
    fieldTypeMap: Map<String, KClass<*>>
): Flow<RequestState<T>> = callbackFlow {
    try {
        if (fieldTypeMap.isEmpty()) throw EmptyArgument(requestName = "Retrieve Fields")

        trySend(RequestState.Loading)

        this@CollectionRepositoryImpl.collection.document(documentName).get()
            .addOnFailureListener { exception ->
                trySend(RequestState.Error(exception))
                close()
            }.addOnSuccessListener { document ->
                val resultMap = mutableMapOf<String, Any>()
                document.data?.let { docData ->
                    fieldTypeMap.forEach { (fieldName, expectedType) ->
                        val value = docData[fieldName] ?: run {
                            Log.w("Test", "文档$documentName中未找到字段$fieldName")
                            throw NoSuchField(fieldName, "Retrieve Fields")
                        }
                        // 校验字段类型是否符合预期
                        if (expectedType.isInstance(value)) {
                            resultMap[fieldName] = value
                        } else {
                            throw IllegalArgumentException(
                                "字段$fieldName类型不匹配:期望${expectedType.simpleName},实际${value::class.simpleName}"
                            )
                        }
                    }
                } ?: throw IllegalStateException("文档$documentName不存在或无数据")

                @Suppress("UNCHECKED_CAST")
                trySend(RequestState.Success(resultMap as T))
                close()
            }

        awaitClose()
    } catch (e: Exception) {
        trySend(RequestState.Error(e))
        close()
    }
}
  • 用Map<String, KClass<*>>替代两个列表,确保字段与类型一一对应,避免索引错误
  • 提取文档中的字段值并校验类型,确保存入Map的是符合预期类型的数据
  • 泛型返回Flow<RequestState<T>>,调用时可明确指定Map的类型

3. 调用示例

现在调用时无需强制转换,直接获取带有类型约束的Map:

val fieldTypeMap = mapOf(
    "username" to String::class,
    "age" to Int::class,
    "isVerified" to Boolean::class
)

viewModelScope.launch {
    repository.retrieveFields<Map<String, Any>>("user_123", fieldTypeMap)
        .collect { state ->
            when (state) {
                RequestState.Loading -> // 显示加载UI
                is RequestState.Error -> // 处理异常,如Toast提示
                is RequestState.Success -> {
                    // 直接使用state.data,类型为Map<String, Any>,且字段类型已校验
                    val username = state.data["username"] as String
                    val age = state.data["age"] as Int
                    // 业务逻辑处理
                }
            }
        }
}

内容的提问来源于stack exchange,提问作者MCDragusanu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 23:15:03