Android Kotlin观测数据处理异常排查:代码实现是否正确?
问题描述
我开发了一款从数据库获取数据的Android应用,数据下载过程中屏幕会显示进度条与提示文本告知用户正在下载,下载完成后该文本与进度条会消失。但应用存在异常表现:有时会出现卡顿,有时进度条和文本消失但数据并未正确加载。Spinner仅用于Activity启动初期展示默认值。
相关代码
Repository层
fun getAllZebra(): LiveData<List<Int>>{ return sybaseDao.getAllZebra() }
ViewModel层
@HiltViewModel class ZebraViewModel @Inject constructor( private val repository: SybaseRepository ):ViewModel(){ val skladnici = repository.getSkladnik().asLiveData() fun getAllZebra(): LiveData<List<Int>>{ return repository.getAllZebra() } }
Activity层
@AndroidEntryPoint class ZebraActivity : AppCompatActivity() { var idZebra : Int = 0 var idExp: Int = 0 private val zebraViewModel: ZebraViewModel by viewModels() private val spinner3 by lazy {findViewById<Spinner>(R.id.zebra_spinner)} override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) SybaseApi.initilaizeBaseUrl(this) val binding = ActivityZebraBinding.inflate(layoutInflater) val view = binding.root setContentView(view) binding.apply { zebraViewModel.skladnici.observe(this@ZebraActivity) { result -> val skladnici: List<Skladnik>? = result.data if (skladnici != null) { Log.e("Skladnici", skladnici.size.toString()) } else { Log.e("Skladnici", "NULL") } progressBarLogin.isVisible = result is Resource.Loading && result.data.isNullOrEmpty() textViewProgressBar.isVisible = result is Resource.Loading && result.data.isNullOrEmpty() textViewProgressBar.text = "Loading data from database" textViewErrorLogin.isVisible = result is Resource.Error && result.data.isNullOrEmpty() textViewErrorLogin.text = result.error?.localizedMessage // 此处存在逻辑错误 textViewErrorLogin.isVisible = result is Resource.Success textViewErrorLogin.text = "Data uploaded:" } } } override fun onResume() { super.onResume() SybaseApi.initilaizeBaseUrl(this) initSpinnerData() } private fun initSpinnerData(){ val allDevices = this?.let { ArrayAdapter<Any>(it, R.layout.simple_item_spinner, R.id.tv_simple_item) } allDevices?.clear() // 每次onResume都会重新订阅LiveData,导致多次回调 zebraViewModel.getAllZebra() .observe(this, { devices -> devices?.forEach{ // 冗余判断,且可能导致数据添加不及时 allDevices?.getPosition(it)?.let { it1 -> if (it1 < 0) allDevices?.add(it) } } } ) spinner3.adapter = allDevices spinner3.onItemSelectedListener = object : AdapterView.OnItemSelectedListener{ override fun onItemSelected(parent: AdapterView<*>?, p1: View?, position: Int, p3: Long) { if (parent != null) { val idZebraString = parent.getItemAtPosition(position).toString() idZebra = idZebraString.toInt() } } override fun onNothingSelected(p0: AdapterView<*>?) { Toast.makeText(this@ZebraActivity, "You need to choose some item", Toast.LENGTH_LONG).show() } } } }
代码问题与修复方案
1. UI状态逻辑覆盖错误
问题点:在skladnici的观察者中,最后两行代码直接将textViewErrorLogin的可见性设置为Success状态,完全覆盖了之前的Error状态判断。这会导致错误状态无法正常显示、成功状态错误复用错误提示控件,造成UI逻辑混乱,甚至进度条消失后数据并未正确加载的假象。
修复:
新增一个成功提示的TextView(比如textViewSuccess),用when语句清晰区分三种状态的UI控制:
binding.apply { zebraViewModel.skladnici.observe(this@ZebraActivity) { result -> val skladnici: List<Skladnik>? = result.data when(result) { is Resource.Loading -> { progressBarLogin.isVisible = true textViewProgressBar.isVisible = true textViewProgressBar.text = "Loading data from database" textViewErrorLogin.isVisible = false textViewSuccess.isVisible = false } is Resource.Success -> { progressBarLogin.isVisible = false textViewProgressBar.isVisible = false textViewErrorLogin.isVisible = false textViewSuccess.isVisible = true textViewSuccess.text = "Data uploaded:" // 处理成功数据 skladnici?.let { Log.e("Skladnici", it.size.toString()) // 这里添加数据加载后的业务逻辑 } ?: Log.e("Skladnici", "NULL") } is Resource.Error -> { progressBarLogin.isVisible = false textViewProgressBar.isVisible = false textViewSuccess.isVisible = false textViewErrorLogin.isVisible = true textViewErrorLogin.text = result.error?.localizedMessage ?: "Unknown error" Log.e("Skladnici", "Error: ${result.error?.localizedMessage}") } } } }
2. LiveData重复订阅与内存泄漏风险
问题点:initSpinnerData在onResume中调用,每次Activity回到前台都会重新订阅getAllZebra()的LiveData,导致多次回调、UI重复更新,还可能引发内存泄漏。
修复:
将Spinner数据订阅移到onCreate中,仅订阅一次:
override fun onCreate(savedInstanceState: Bundle?) { // ... 其他代码 initSpinnerData() } override fun onResume() { super.onResume() SybaseApi.initilaizeBaseUrl(this) // 移除initSpinnerData调用,避免重复订阅 } private fun initSpinnerData(){ val allDevices = ArrayAdapter<Any>(this, R.layout.simple_item_spinner, R.id.tv_simple_item) spinner3.adapter = allDevices // 只订阅一次LiveData zebraViewModel.getAllZebra().observe(this) { devices -> allDevices.clear() devices?.let { allDevices.addAll(it) } allDevices.notifyDataSetChanged() } // ... 其他Spinner逻辑 }
3. Spinner适配器数据更新逻辑冗余
问题点:原代码中getPosition遍历适配器数据的判断逻辑冗余,多次调用会降低性能,还可能导致数据更新不及时。
修复:
直接清空适配器后添加所有数据,如需去重则先处理数据:
zebraViewModel.getAllZebra().observe(this) { devices -> allDevices.clear() devices?.distinct()?.let { allDevices.addAll(it) } // 去重可选 allDevices.notifyDataSetChanged() }
4. 潜在的UI线程卡顿问题
问题点:如果Sybase数据库操作未在后台线程执行,会阻塞UI线程导致卡顿。
修复:
确保Repository层的数据库操作在后台线程执行,比如用协程:
// Repository层示例 fun getAllZebra(): LiveData<List<Int>>{ return liveData(Dispatchers.IO) { emit(sybaseDao.getAllZebra().value ?: emptyList()) } }
5. 初始化逻辑重复
问题点:SybaseApi.initilaizeBaseUrl(this)在onCreate和onResume重复调用,造成不必要的性能损耗。
修复:
仅在onCreate中调用一次初始化:
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) SybaseApi.initilaizeBaseUrl(this) // 仅初始化一次 // ... 其他代码 } override fun onResume() { super.onResume() // 移除重复初始化 }
内容的提问来源于stack exchange,提问作者jenik2205

