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

Kotlin词典APP数据异常求助:Flow无数据显示问题排查

词典APP数据加载问题排查

问题现象

使用Kotlin开发词典APP,采用MVVM+Clean架构,通过Jsoup从网络抓取词汇信息,用Flow传递数据,但出现以下问题:

  • 界面无词汇数据展示
  • 控制台仅输出kotlinx.coroutines.flow.SafeFlow@1493a74
  • MostUsedWordScreen中打印的state.mostWordUsedItems为空列表[]

相关代码

ExtractedData(数据实体类)

data class ExtractedData(
    var id :Int = 0,
    var word:String = "",
    var meaning :String = ""
)

WordInfoRepositoryImpl(数据层实现)

class WordInfoRepositoryImpl @Inject constructor(
    private val api:DictionaryApi
) : WordInfoRepository {

    // 从网络获取词汇及释义
    override fun getEventsList(): Flow<Resource<MutableList<ExtractedData>>> = flow {
        emit(Resource.Loading())
        val listData = mutableListOf<ExtractedData>()

        try {
            val url = "https://ielts.com.au/australia/prepare/article-100-new-english-words-and-phrases-updated-2020"
            val doc = withContext(Dispatchers.IO){
                Jsoup.connect(url).get() // 提示"Inappropriate blocking method call",但已放在IO协程中
            }

            val table = doc.select("table")
            val rows = table.select("tr")
            val eventsSize = rows.size

            for (i in 1 until eventsSize) {
                val row = rows[i]
                val cols = row.select("td")
                val word = cols[0].text()
                val meaning = cols[1].text()
                listData.add(ExtractedData(i,word,meaning))
            }
        } catch (e: IOException) {
            emit(Resource.Error("IO Exception"))
        } catch (e : HttpException) {
            emit(Resource.Error("HTTP EXCEPTION"))
        }

        emit(Resource.Success(listData))
    }
}

WordInfoRepository(领域层接口)

interface WordInfoRepository {
    fun getEventsList(): Flow<Resource<MutableList<ExtractedData>>>
}

GetWordsAndMeaningsOnTheInternetUseCase(领域层用例)

class GetWordsAndMeaningsOnTheInternetUseCase@Inject constructor(
    private val repository: WordInfoRepository
){
    operator fun invoke() : Flow<Resource<MutableList<ExtractedData>>> {
        return repository.getEventsList()
    }
}

MostUsedWordScreenViewModel(ViewModel层)

@HiltViewModel
class MostUsedWordScreenViewModel @Inject constructor(
    private val getWordsAndMeaningsOnTheInternetUseCase: GetWordsAndMeaningsOnTheInternetUseCase
) : ViewModel() {

    private var searchJob: Job? = null
    private val _state = mutableStateOf(MostUsedWordState())
    val state: State<MostUsedWordState> = _state

    init {
        fetchData()
    }

    private fun fetchData()  {
        searchJob?.cancel()
        searchJob = viewModelScope.launch(IO) {
            getWordsAndMeaningsOnTheInternetUseCase().onEach { result ->
                when (result) {
                    is Resource.Success -> {
                        _state.value = state.value.copy(
                            mostWordUsedItems = result.data ?: mutableListOf(),
                            isLoading = false
                        )
                    }
                    is Resource.Error -> {
                        _state.value = state.value.copy(
                            mostWordUsedItems = result.data ?: mutableListOf(),
                            isLoading = false
                        )
                    }
                    is Resource.Loading -> {
                        _state.value = state.value.copy(
                            mostWordUsedItems = result.data ?: mutableListOf(),
                            isLoading = true
                        )
                    }
                }
            }
        }
    }
}

MostUsedWordScreen(UI层)

@Composable
fun MostUsedWordScreen(viewModel: MostUsedWordScreenViewModel = hiltViewModel()) {
    val state = viewModel.state.value
    println("state --- >>> "+state.mostWordUsedItems)
    LazyColumn(modifier = Modifier.fillMaxSize()) {
        items(state.mostWordUsedItems.size) { i ->
            val wordInfo = state.mostWordUsedItems[i]
            if(i > 0) {
                Spacer(modifier = Modifier.height(8.dp))
            }
            MostUsedWordItem(word = wordInfo)
            if(i < state.mostWordUsedItems.size - 1) {
                Divider()
            }
        }
    }
}

@Composable
fun MostUsedWordItem(word : ExtractedData ) {
    Column(modifier = Modifier
        .padding(5.dp)
        .fillMaxWidth()) {
        Text(text = word.word,
            modifier = Modifier.padding(3.dp),
            textAlign = TextAlign.Center,
            fontSize = 18.sp)
    }
}

问题定位与解决方案

核心问题:Flow未被收集

Flow是冷流,只有被收集时才会执行流中的逻辑。原ViewModel代码中,getWordsAndMeaningsOnTheInternetUseCase().onEach { ... }仅定义了流的转换操作,但没有启动收集,导致整个网络请求和数据解析逻辑完全没执行,自然没有数据更新到UI状态。

修复代码(ViewModel的fetchData方法)

将onEach配合launchIn启动收集,或者直接使用collect:

private fun fetchData()  {
    searchJob?.cancel()
    searchJob = viewModelScope.launch {
        getWordsAndMeaningsOnTheInternetUseCase()
            .onEach { result ->
                when (result) {
                    is Resource.Success -> {
                        _state.value = state.value.copy(
                            mostWordUsedItems = result.data ?: mutableListOf(),
                            isLoading = false
                        )
                    }
                    is Resource.Error -> {
                        _state.value = state.value.copy(
                            mostWordUsedItems = result.data ?: mutableListOf(),
                            isLoading = false
                        )
                    }
                    is Resource.Loading -> {
                        _state.value = state.value.copy(
                            // Loading状态保留原有数据,避免列表清空
                            isLoading = true
                        )
                    }
                }
            }
            // 启动Flow收集
            .launchIn(viewModelScope)
    }
}

次要优化点

  1. Loading状态保留原有数据:原代码在Loading时会把mostWordUsedItems设为空列表,导致界面清空,建议仅更新isLoading状态,保留现有数据。
  2. Jsoup选择器准确性:检查目标页面的HTML结构,确保doc.select("table")能正确定位到包含词汇的表格,必要时使用更精确的选择器(比如doc.select("table[class='some-class']"))。
  3. Jsoup阻塞警告:虽然不影响功能,但可以通过@Suppress("BlockingMethodInNonBlockingContext")注解消除警告,或者将整个Jsoup操作块放在withContext(Dispatchers.IO)内(已做)。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 19:50:26