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 的情况。