当前位置: 首页 > news >正文

viewmodel协程中执行耗时操作,导致viewmodel创建两次,导致observer失效

这里写自定义目录标题


有问题的viewmodel

package com.bala.hittin.ui.vm

import android.content.Context
import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alibaba.fastjson.JSON
import com.alibaba.fastjson.JSONObject
import com.bala.hittin.UserHelper
import com.bala.hittin.base.SpHelper
import com.bala.hittin.bean.CityResult
import com.bala.hittin.bean.UserInfoBean
import com.bala.hittin.config.SPConfig
import com.bala.hittin.utils.convertCsvToJson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class LocalVM : ViewModel() {
    var cityData: HashMap<String, String> = HashMap()
    val inputCityData = MutableLiveData<List<CityResult>>()
    val location = MutableLiveData<String>()
    private var currentInput = ""
    private var pageIndex = 0

    suspend fun initPostCityData(context: Context) {
        JSON.parseArray(context.convertCsvToJson("us_zipcode.csv"), JSONObject::class.java)
            .forEach { jb ->
                cityData[jb.getString("Column1")] = (
                        jb.getString("Column2") + jb.getString("Column3").let {
                            var result = ""
                            if (!it.isNullOrEmpty())
                                result = ", ${it}"
                            result
                        }).trimStart('\"').trimEnd('\"')
            }
        refresh(currentInput)
    }

    fun refresh(input: String) {
        currentInput = input
        pageIndex = 0
        loadMore()
    }

    fun loadMore() {
        viewModelScope.launch(Dispatchers.IO) {
            try {
                val result = searchMap(currentInput, pageNumber = pageIndex)
                Log.e("aaaa", "loadMore success ${result.size}")
                inputCityData.postValue(result)
                pageIndex += 1
            } catch (e:Exception) {
                e.printStackTrace()
                Log.e("aaaa", "异常")
            }

        }
    }


    private fun searchMap(
        query: String,
        pageSize: Int = 50,
        pageNumber: Int = 0
    ): List<CityResult> {
        val results = mutableListOf<CityResult>()
        val queryLower = query.lowercase()
        val queryChars = queryLower.toSet()

        var startIndex = pageNumber * pageSize
        var endIndex = startIndex + pageSize

        var currentIndex = 0

        // 遍历Map并计算每个项的得分
        for ((key, value) in cityData) {
            if (currentIndex >= endIndex) {
                break
            }

            if (currentIndex >= startIndex) {
                val valueLower = value.lowercase()
                if (query.isEmpty()) {
                    results.add(
                        CityResult(
                            key = key,
                            value = value,
                            matchPosition = -1,
                            matchedChars = query.length,
                            sequenceScore = 0 // 直接包含给予最高顺序分数
                        )
                    )
                    currentIndex++
                    continue
                }

                // 1. 检查是否直接包含
                val position = valueLower.indexOf(queryLower)
                if (position != -1) {
                    results.add(
                        CityResult(
                            key = key,
                            value = value,
                            matchPosition = position,
                            matchedChars = query.length,
                            sequenceScore = 100 // 直接包含给予最高顺序分数
                        )
                    )
                    currentIndex++
                    continue
                }

                // 2. 计算字符匹配
                val valueChars = valueLower.toSet()
                val matchedChars = queryChars.intersect(valueChars).size

                if (matchedChars > 0) {
                    // 3. 计算顺序匹配分数
                    var sequenceScore = 0
                    var lastFoundIndex = -1
                    var currentScore = 0

                    for (queryChar in queryLower) {
                        val index = valueLower.indexOf(queryChar, lastFoundIndex + 1)
                        if (index != -1) {
                            if (lastFoundIndex == -1 || index == lastFoundIndex + 1) {
                                currentScore++
                            }
                            lastFoundIndex = index
                        }
                    }
                    sequenceScore = currentScore
                    val firstMatchPosition = queryChars.map { char ->
                        valueLower.indexOf(char)
                    }.filter { it != -1 }.minOrNull() ?: -1
                    results.add(
                        CityResult(
                            key = key,
                            value = value,
                            matchPosition = firstMatchPosition,
                            matchedChars = matchedChars,
                            sequenceScore = sequenceScore
                        )
                    )
                    currentIndex++
                }
            } else {
                currentIndex++
            }
        }

        // 排序
        return results.sortedWith(compareBy<CityResult> {
            // 1. 没有匹配的字符排在最后
            if (it.matchedChars == 0) 1 else 0
        }.thenBy {
            // 2. 匹配位置越靠前越好
            it.matchPosition
        }.thenByDescending {
            // 3. 匹配的字符数越多越好
            it.matchedChars
        }.thenByDescending {
            // 4. 顺序匹配分数越高越好
            it.sequenceScore
        }.thenBy {
            // 5. 按原始字符串首字母排序
            it.value.lowercase()
        })
    }


    fun getHistoryInput(context: Context): String {
        return SpHelper.getInstance(context, SPConfig.REGISTER)
            .getString(SPConfig.REGISTER_LOCATION, "")
            .toString()
    }

    fun saveInput(context: Context, input: String) {
        if (UserHelper.userInfo.value == null) {
            SpHelper.getInstance(context, SPConfig.REGISTER).put(SPConfig.REGISTER_LOCATION, input)
            UserHelper.registerToInfo(context)
        } else {
            UserHelper.userInfo.value = UserHelper.userInfo.value!!.apply {
                geoLocation = UserInfoBean.GeoLocation(input)
            }
        }

    }


}

修改后的viewmodel

package com.bala.hittin.ui.vm

import android.app.Application
import android.content.Context
import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alibaba.fastjson.JSON
import com.alibaba.fastjson.JSONObject
import com.bala.hittin.UserHelper
import com.bala.hittin.base.SpHelper
import com.bala.hittin.bean.CityResult
import com.bala.hittin.bean.UserInfoBean
import com.bala.hittin.config.SPConfig
import com.bala.hittin.utils.convertCsvToJson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class LocalVM(application: Application) : ViewModel() {
    private val context = application.applicationContext // 使用 Application Context
    var cityData: HashMap<String, String> = HashMap()
    val inputCityData = MutableLiveData<List<CityResult>>()
    val location = MutableLiveData<String>()
    private var currentInput = ""
    private var pageIndex = 0

    init {
        Log.e("LocalVM", "ViewModel created: $this")
        viewModelScope.launch(Dispatchers.IO) {
            initPostCityDataInternal()
            refresh(currentInput)
        }
    }

    private suspend fun initPostCityDataInternal() {
        JSON.parseArray(context.convertCsvToJson("us_zipcode.csv"), JSONObject::class.java)
            .forEach { jb ->
                cityData[jb.getString("Column1")] = (
                        jb.getString("Column2") + jb.getString("Column3").let {
                            var result = ""
                            if (!it.isNullOrEmpty())
                                result = ", ${it}"
                            result
                        }).trimStart('\"').trimEnd('\"')
            }
        Log.d("LocalVM", "initPostCityDataInternal finished, cityData size = ${cityData.size}")
    }

    override fun onCleared() {
        super.onCleared()
        Log.e("LocalVM", "onCleared")
        // 清理 ViewModel 持有的资源
    }

    fun refresh(input: String) {
        currentInput = input
        pageIndex = 0
        loadMore()
    }

    fun loadMore() {
        viewModelScope.launch(Dispatchers.IO) {
            try {
                val result = searchMap(currentInput, pageNumber = pageIndex)
                Log.e("aaaa", "loadMore success ${result.size}")
                inputCityData.postValue(result)
                pageIndex += 1
            } catch (e: Exception) {
                e.printStackTrace()
                Log.e("aaaa", "loadMore exception")
            }
        }
    }

    private fun searchMap(
        query: String,
        pageSize: Int = 50,
        pageNumber: Int = 0
    ): List<CityResult> {
        val results = mutableListOf<CityResult>()
        val queryLower = query.lowercase()
        val queryChars = queryLower.toSet()

        var startIndex = pageNumber * pageSize
        var endIndex = startIndex + pageSize

        var currentIndex = 0

        for ((key, value) in cityData) {
            if (currentIndex >= endIndex) break
            if (currentIndex >= startIndex) {
                val valueLower = value.lowercase()
                if (query.isEmpty()) {
                    results.add(CityResult(key = key, value = value, matchPosition = -1, matchedChars = query.length, sequenceScore = 0))
                    currentIndex++
                    continue
                }
                val position = valueLower.indexOf(queryLower)
                if (position != -1) {
                    results.add(CityResult(key = key, value = value, matchPosition = position, matchedChars = query.length, sequenceScore = 100))
                    currentIndex++
                    continue
                }
                val valueChars = valueLower.toSet()
                val matchedChars = queryChars.intersect(valueChars).size
                if (matchedChars > 0) {
                    var sequenceScore = 0
                    var lastFoundIndex = -1
                    var currentScore = 0
                    for (queryChar in queryLower) {
                        val index = valueLower.indexOf(queryChar, lastFoundIndex + 1)
                        if (index != -1) {
                            if (lastFoundIndex == -1 || index == lastFoundIndex + 1) currentScore++
                            lastFoundIndex = index
                        }
                    }
                    sequenceScore = currentScore
                    val firstMatchPosition = queryChars.map { valueLower.indexOf(it) }.filter { it != -1 }.minOrNull() ?: -1
                    results.add(CityResult(key = key, value = value, matchPosition = firstMatchPosition, matchedChars = matchedChars, sequenceScore = sequenceScore))
                    currentIndex++
                }
            } else {
                currentIndex++
            }
        }
        return results.sortedWith(compareBy<CityResult> { if (it.matchedChars == 0) 1 else 0 }
            .thenBy { it.matchPosition }
            .thenByDescending { it.matchedChars }
            .thenByDescending { it.sequenceScore }
            .thenBy { it.value.lowercase() })
    }

    fun getHistoryInput(context: Context): String {
        return SpHelper.getInstance(context, SPConfig.REGISTER)
            .getString(SPConfig.REGISTER_LOCATION, "")
            .toString()
    }

    fun saveInput(context: Context, input: String) {
        if (UserHelper.userInfo.value == null) {
            SpHelper.getInstance(context, SPConfig.REGISTER).put(SPConfig.REGISTER_LOCATION, input)
            UserHelper.registerToInfo(context)
        } else {
            UserHelper.userInfo.value = UserHelper.userInfo.value!!.apply {
                geoLocation = UserInfoBean.GeoLocation(input)
            }
        }
    }
}```

这表明将 CSV 数据的解析移动到 LocalVM 的 init 块中有效地解决了 ViewModel 被多次创建和过早清除的问题。

根本原因很可能是之前在 SATestActivity 的 initData() 中执行耗时的文件读取和解析操作,即使在 lifecycleScope.launch(Dispatchers.IO) 中进行,仍然在 Activity 的初始化阶段对主线程造成了某种程度的阻塞或干扰,导致系统错误地管理了 ViewModel 的生命周期。

将数据加载移动到 ViewModel 的 init 块中,确保了数据加载只在 ViewModel 首次创建时进行,并且在 Activity 处于活动状态并准备好观察 LiveData 之后。

总结一下我们学到的教训:

避免在 Activity 或 Fragment 的初始化阶段(特别是 onCreate 和紧随其后的方法)执行耗时的同步操作。 这可能会导致主线程阻塞,影响 Activity 的生命周期管理。
对于需要在 ViewModel 中加载的数据,考虑在 ViewModel 的 init 块中使用协程在后台线程中进行加载。 这样可以确保数据在 ViewModel 创建后异步加载,而不会阻塞 UI 线程。
使用 ViewModelProvider.Factory 来创建带有参数的 ViewModel 实例,例如需要 Application Context 的情况。

相关文章:

  • Linux 网络基础知识总结
  • 供应S620 支持 PD 的多协议双向快充移动电源解决方案
  • 保护PCBA的不同方法:喷三防漆 vs 镀膜
  • Ajax------免刷新地前后端交互
  • 力扣DAY46-50 | 热100 | 二叉树:展开为链表、pre+inorder构建、路径总和、最近公共祖先、最大路径和
  • 英伟达开源253B语言模型:Llama-3.1-Nemotron-Ultra-253B-v1 模型情况
  • #Hash 模式 vs History 模式
  • MCP基础学习四:MCP在AI应用中的集成(MCP在AI应用中的完整架构图)
  • 备赛蓝桥杯-Python-考前突击
  • EPLAN许可证更新教程
  • FTPClient开发遇到的坑
  • 新手宝塔部署thinkphp一步到位
  • 两个有序序列合并算法分析
  • MySQL数据库编程总结
  • 【SQL】常见SQL 行列转换的方法汇总 - 精华版
  • GHG认证是什么,GHG认证的意义?对企业发展好处
  • Python 类型转换详解
  • C++在嵌入式中表现如何?
  • springboot 处理编码的格式为opus的音频数据解决方案【java8】
  • AICon 2024年全球人工智能与大模型开发与应用大会(脱敏)PPT汇总(36份).zip
  • 做网站技术含量/自己创建个人免费网站
  • 佛山微信网站建设多少钱/淘宝关键词怎么做排名靠前
  • 建设机械员证书查询网站/2022年最新热点素材
  • 服务高端网站建设/怎么弄属于自己的网站
  • 网站批量创建程序/制作网站的步骤是什么
  • steam官方网站下载/南宁网络推广服务商