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

Android Kotlin Compose按字母解析JSON指定内容的问题求助

问题描述

开发一款词典应用,已将数据集转为JSON文件,希望实现用户点击对应字母时提取该字母相关搭配数据(如点击A展示A开头的搭配)。但在Android Kotlin Compose中用Gson解析JSON时遇到两个问题:

  1. 抛出Expected BEGIN_ARRAY but was BEGIN_OBJECT错误
  2. 当前代码会加载全部数据,不符合按需提取需求

JSON文件示例

{
    "A": [
        {
            "collocation": "above average",
            "meaning": "more than average, esp. in amount, age, height, weight etc. "
        },
        {
            "collocation": "absolutely necessary",
            "meaning": "totally or completely necessary"
        }
    ], 
    "B": [
        {
            "collocation": "back pay",
            "meaning": "money a worker earned in the past but hasn't been paid yet  "
        },
        {
            "collocation": "back road",
            "meaning": "a small country road "
        }
    ]
}

View层代码

val jsonFileString = viewModel.getJsonDataFromCollocationsJson(context, fileName = "collocations.json")
Log.i("data", jsonFileString ?: "null")

val gson = Gson()
val listCollocationType = object : TypeToken<List<Collocations>>() {}.type

var collocations: List<Collocations> = gson.fromJson(jsonFileString, listCollocationType)
collocations.forEachIndexed { idx, collocations -> Log.i("data", "> Item $idx:\n$collocations") }

ViewModel层代码

fun getJsonDataFromCollocationsJson(context: Context, fileName: String): String? {
    var jsonString: String? = null
    try {
        jsonString = context.assets.open(fileName).bufferedReader().use { it.readText() }
    } catch (e: IOException) {
        e.printStackTrace()
        return null
    }
    return jsonString
}

错误日志

/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.enestigli.dictionaryapp, PID: 5885
    com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 1 column 2 path $
        at com.google.gson.Gson.fromJson(Gson.java:939)
        at com.google.gson.Gson.fromJson(Gson.java:892)
        at com.google.gson.Gson.fromJson(Gson.java:841)
        at com.enestigli.dictionaryapp.presentation.CollocationScreen.CollocationScreenKt$gridView$2$1$1$1$1$1$1.invoke(CollocationScreen.kt:166)
        at com.enestigli.dictionaryapp.presentation.CollocationScreen.CollocationScreenKt$gridView$2$1$1$1$1$1$1.invoke(CollocationScreen.kt:158)
        at androidx.compose.foundation.ClickableKt$clickable$4$gesture$1$2.invoke-k-4lQ0M(Clickable.kt:153)
        at androidx.compose.foundation.ClickableKt$clickable$4$gesture$1$2.invoke(Clickable.kt:142)
        at androidx.compose.foundation.gestures.TapGestureDetectorKt$detectTapAndPress$2$1$1.invokeSuspend(TapGestureDetector.kt:220)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:178)
        at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
        at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)
        at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)
        at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)
        at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:328)
        at androidx.compose.ui.input.pointer.SuspendingPointerInputFilter$PointerEventHandlerCoroutine.offerPointerEvent(SuspendingPointerInputFilter.kt:560)
        at androidx.compose.ui.input.pointer.SuspendingPointerInputFilter.dispatchPointerEvent(SuspendingPointerInputFilter.kt:452)
        at androidx.compose.ui.input.pointer.SuspendingPointerInputFilter.onPointerEvent-H0pRuoY(SuspendingPointerInputFilter.kt:465)
        at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:310)
        at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:297)
        at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:297)
        at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:297)
        at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:297)
        at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:297)
        at androidx.compose.ui.input.pointer.NodeParent.dispatchMainEventPass(HitPathTracker.kt:179)
        at androidx.compose.ui.input.pointer.HitPathTracker.dispatchChanges(HitPathTracker.kt:98)
        at androidx.compose.ui.input.pointer.PointerInputEventProcessor.process-BIzXfog(PointerInputEventProcessor.kt:80)
        at androidx.compose.ui.platform.AndroidComposeView.sendMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1261)
        at androidx.compose.ui.platform.AndroidComposeView.handleMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1211)
        at androidx.compose.ui.platform.AndroidComposeView.dispatchTouchEvent(AndroidComposeView.android.kt:1150)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3121)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2802)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3121)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2802)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3121)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2802)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3121)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2802)
        at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:498)
        at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1899)
        at android.app.Activity.dispatchTouchEvent(Activity.java:4262)
E/AndroidRuntime:     at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:456)
        at android.view.View.dispatchPointerEvent(View.java:15263)
        at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:6548)
        at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:6348)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:5804)
        at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:5861)
        at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:5827)
        at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:5992)
        at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:5835)
        at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:6049)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:5808)
        at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:5861)
        at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:5827)
        at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:5835)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:5808)
        at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:8857)
        at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:8808)
        at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:8777)
        at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:8980)
        at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:267)
        at android.os.MessageQueue.nativePollOnce(Native Method)
        at android.os.MessageQueue.next(MessageQueue.java:335)
        at android.os.Looper.loopOnce(Looper.java:161)
        at android.os.Looper.loop(Looper.java:288)
        at android.app.ActivityThread.main(ActivityThread.java:7898)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
        Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [androidx.compose.ui.platform.MotionDurationScaleImpl@8b42480, androidx.compose.runtime.BroadcastFrameClock@2d2bab9, StandaloneCoroutine{Cancelling}@cb40efe, AndroidUiDispatcher@baff65f]
    Caused by: java.lang.IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 1 column 2 path $
        at com.google.gson.stream.JsonReader.beginArray(JsonReader.java:350)
        at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.read(CollectionTypeAdapterFactory.java:80)
        at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.read(CollectionTypeAdapterFactory.java:61)
        at com.google.gson.Gson.fromJson(Gson.java:927)
解决方案

1. 修复Gson解析错误

JSON外层是Object,键为字母、值为搭配数组,当前代码试图直接解析成List<Collocations>,导致类型不匹配错误。

步骤1:规范数据类定义

// 单个搭配的数据类
data class Collocation(
    val collocation: String,
    val meaning: String
)

步骤2:修改解析逻辑

直接解析成Map<String, List<Collocation>>,匹配JSON结构:

val jsonFileString = viewModel.getJsonDataFromCollocationsJson(context, fileName = "collocations.json")
jsonFileString?.let { json ->
    val gson = Gson()
    val collocationMapType = object : TypeToken<Map<String, List<Collocation>>>() {}.type
    val collocationMap: Map<String, List<Collocation>> = gson.fromJson(json, collocationMapType)
    
    // 点击字母A时获取对应数据
    val aCollocations = collocationMap["A"]
    aCollocations?.forEachIndexed { idx, item -> 
        Log.i("data", "> Item $idx: ${item.collocation} - ${item.meaning}") 
    }
}

2. 实现按需加载

方式1:流式解析(不加载全量数据)

用JsonReader流式读取,只解析目标字母对应的内容:

// ViewModel中新增方法
fun getCollocationsByLetter(context: Context, fileName: String, targetLetter: String): List<Collocation>? {
    return try {
        context.assets.open(fileName).use { inputStream ->
            Gson().newJsonReader(inputStream.bufferedReader()).use { reader ->
                reader.beginObject()
                var targetList: List<Collocation>? = null
                while (reader.hasNext()) {
                    val key = reader.nextName()
                    if (key == targetLetter) {
                        val listType = object : TypeToken<List<Collocation>>() {}.type
                        targetList = Gson().fromJson(reader, listType)
                        reader.skipValue() // 跳过剩余内容提升性能
                        break
                    } else {
                        reader.skipValue() // 跳过非目标字母数据
                    }
                }
                reader.endObject()
                targetList
            }
        }
    } catch (e: IOException) {
        e.printStackTrace()
        null
    }
}

View层调用:

// 点击字母时触发
viewModel.getCollocationsByLetter(context, "collocations.json", "A")?.let { collocations ->
    // 展示数据逻辑
}

方式2:拆分JSON文件(简单高效)

按字母拆分JSON文件(如collocations_a.json、collocations_b.json),点击时加载对应文件:

// ViewModel中修改加载方法
fun getCollocationsByLetter(context: Context, letter: String): List<Collocation>? {
    val fileName = "collocations_${letter.lowercase()}.json"
    return try {
        context.assets.open(fileName).bufferedReader().use { json ->
            val listType = object : TypeToken<List<Collocation>>() {}.type
            Gson().fromJson(json, listType)
        }
    } catch (e: IOException) {
        e.printStackTrace()
        null
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 09:41:20