Android中UI线程与按钮行为异常问题求助
仓库Android应用开发问题及解决建议
背景
我主要从事C#和Python开发,刚接触Android开发。去年年初为公司开发了一款Android手持扫描器应用,可扫描条码并调用API展示结果。现需开发一款仓库应用,逻辑类似但需根据API返回结果显示按钮,点击按钮调用API更新数据。
现有MainActivity.kt代码
package com.classicoptical.binning import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.device.ScanDevice import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.util.Log import android.view.View import androidx.appcompat.app.AppCompatDelegate import com.classicoptical.binning.api.ApiRequest import com.classicoptical.binning.api.BASE_URL import com.classicoptical.binning.databinding.ActivityMainBinding import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import retrofit2.HttpException import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.io.IOException private const val SCAN_ACTION = "scan.rcv.message" class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var sm: ScanDevice private lateinit var barcodeStr: String private lateinit var job: String private var mScanReceiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { binding.tvWaiting.text = "Getting info..." val barocode = intent.getByteArrayExtra("barocode") val barocodelen = intent.getIntExtra("length", 0) val temp = intent.getByteExtra("barcodeType", 0.toByte()) val aimid = intent.getByteArrayExtra("aimid") barcodeStr = String(barocode!!, 0, barocodelen) makeApiRequest(barcodeStr) sm.stopScan() } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //supportActionBar?.hide() binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) sm = ScanDevice() sm.setOutScanMode(0) } public fun runMoveCommand() { val api = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build() .create(ApiRequest::class.java) GlobalScope.launch(Dispatchers.IO) { try { val response = api.moveBinLocation(job) Log.d("Main", "success: ${response.success}") binding.tvUpdate.setVisibility(View.VISIBLE) if (response.success == false) { binding.tvUpdate.text = "Error" } else { binding.tvUpdate.text = "Moved!!" } } catch (e: IOException) { Log.e("Main", "IOException: You may not have internet connection - ${e.message}") } catch (e: HttpException) { Log.e("Main", "HttpException: unexpected response - ${e.message}") } catch (e: Exception) { Log.e("Main", "Error: ${e.message}") } } } private fun makeApiRequest(scanned_opc: String) { val api = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build() .create(ApiRequest::class.java) GlobalScope.launch(Dispatchers.IO) { try { val response = api.getBinLocation(scanned_opc) Log.d("Main", "item_no: ${response.jobID}") if (response.jobID == null) { binding.tvWaiting.textSize = 18f binding.tvWaiting.text = "JobID Not recognized. Try again." binding.tvBinLocation.text = "BAD" } else { job = scanned_opc binding.tvWaiting.text = "${response.jobID}" binding.tvBinLocation.text = "${response.binLocation}" if (response.moveOldBinID == 0) { binding.btnMove.setVisibility(View.GONE); binding.tvMoveLocation.setVisibility(View.GONE); } else { binding.btnMove.setVisibility(View.VISIBLE); binding.tvMoveLocation.setVisibility(View.VISIBLE); binding.tvUpdate.text = "${response.moveOldBinLocation}"; binding.tvMoveLocation.text = "${response.moveOldBinLocation}"; } } } catch (e: IOException) { Log.e("Main", "IOException: You may not have internet connection - ${e.message}") } catch (e: HttpException) { Log.e("Main", "HttpException: unexpected response - ${e.message}") } catch (e: Exception) { Log.e("Main", "Error: ${e.message}") } } } override fun onPause() { super.onPause() if (sm != null) { sm.stopScan() } unregisterReceiver(mScanReceiver) } override fun onResume() { super.onResume() if (sm != null) { sm.openScan() sm.outScanMode = 0 } val filter = IntentFilter() filter.addAction(SCAN_ACTION) registerReceiver(mScanReceiver, filter) } override fun onDestroy() { super.onDestroy() if (sm != null) { sm.closeScan() sm.stopScan() sm.scanLaserMode = 8 } } }
遇到的问题
- 更新TextView正常,但在
makeApiRequest函数中,当moveOldBinID为0时,执行binding.btnMove.setVisibility(View.GONE)可隐藏按钮,但执行binding.tvMoveLocation.setVisibility(View.GONE)时报错:
Error: Only the original thread that created a view hierarchy can touch its views.
- 扫描
moveOldBinID不为0的条目时,执行binding.btnMove.setVisibility(View.VISIBLE)无效果,且无报错。 - 测试按钮点击时应用崩溃。
按钮布局(activity_main.xml)
<Button android:id="@+id/btn_move" android:layout_width="320dp" android:layout_height="wrap_content" android:layout_below="@id/tv_bin_location" android:background="@android:color/holo_blue_dark" android:layout_alignParentLeft="true" android:layout_centerHorizontal="true" android:layout_marginLeft="20dp" android:layout_marginTop="40dp" android:gravity="center" android:text="@string/button_text" android:textColor="@android:color/white" android:textSize="24dp" android:onClick="runMoveCommand" />
尝试过的方案(未成功)
曾尝试用runOnUiThread包裹UI操作,但因Kotlin语法错误(提示"Expecting an expression for Runnable()及unresolved reference for new")未能成功,代码如下:
} else { job = scanned_opc runOnUiThread(new Runnable() { @Override public void run() { binding.tvWaiting.text = "${response.jobID}" binding.tvBinLocation.text = "${response.binLocation}" if (response.moveOldBinID == 0) { binding.btnMove.setVisibility(View.GONE); binding.tvMoveLocation.setVisibility(View.GONE); } else { binding.btnMove.setVisibility(View.VISIBLE); binding.tvMoveLocation.setVisibility(View.VISIBLE); binding.tvUpdate.text = "${response.moveOldBinLocation}"; binding.tvMoveLocation.text = "${response.moveOldBinLocation}"; } } }); }
解决建议与代码修正
1. 核心问题:跨线程操作UI
Android所有UI操作必须在主线程执行,你当前在Dispatchers.IO协程(子线程)中直接操作UI,这是报错和无效果的根本原因。Kotlin中runOnUiThread支持直接传入Lambda表达式,不需要Java风格的new Runnable()写法。
修正后的makeApiRequest函数
private fun makeApiRequest(scanned_opc: String) { val api = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build() .create(ApiRequest::class.java) GlobalScope.launch(Dispatchers.IO) { try { val response = api.getBinLocation(scanned_opc) Log.d("Main", "item_no: ${response.jobID}") // 将所有UI操作移至主线程执行 runOnUiThread { if (response.jobID == null) { binding.tvWaiting.textSize = 18f binding.tvWaiting.text = "JobID Not recognized. Try again." binding.tvBinLocation.text = "BAD" } else { job = scanned_opc binding.tvWaiting.text = "${response.jobID}" binding.tvBinLocation.text = "${response.binLocation}" if (response.moveOldBinID == 0) { binding.btnMove.visibility = View.GONE binding.tvMoveLocation.visibility = View.GONE } else { binding.btnMove.visibility = View.VISIBLE binding.tvMoveLocation.visibility = View.VISIBLE binding.tvUpdate.text = "${response.moveOldBinLocation}" binding.tvMoveLocation.text = "${response.moveOldBinLocation}" } } } } catch (e: IOException) { Log.e("Main", "IOException: You may not have internet connection - ${e.message}") runOnUiThread { binding.tvWaiting.text = "网络连接异常,请检查" } } catch (e: HttpException) { Log.e("Main", "HttpException: unexpected response - ${e.message}") runOnUiThread { binding.tvWaiting.text = "请求错误,请重试" } } catch (e: Exception) { Log.e("Main", "Error: ${e.message}") runOnUiThread { binding.tvWaiting.text = "操作失败,请重试" } } } }
修正后的runMoveCommand函数
public fun runMoveCommand(view: View) { // 必须添加View参数,匹配xml中onClick的要求 val api = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build() .create(ApiRequest::class.java) GlobalScope.launch(Dispatchers.IO) { try { val response = api.moveBinLocation(job) Log.d("Main", "success: ${response.success}") runOnUiThread { binding.tvUpdate.visibility = View.VISIBLE binding.tvUpdate.text = if (response.success == false) "Error" else "Moved!!" } } catch (e: IOException) { Log.e("Main", "IOException: You may not have internet connection - ${e.message}") runOnUiThread { binding.tvUpdate.visibility = View.VISIBLE binding.tvUpdate.text = "网络连接异常,请检查" } } catch (e: HttpException) { Log.e("Main", "HttpException: unexpected response - ${e.message}") runOnUiThread { binding.tvUpdate.visibility = View.VISIBLE binding.tvUpdate.text = "请求错误,请重试" } } catch (e: Exception) { Log.e("Main", "Error: ${e.message}") runOnUiThread { binding.tvUpdate.visibility = View.VISIBLE binding.tvUpdate.text = "操作失败,请重试" } } } }
2. 按钮点击崩溃的原因
xml中android:onClick="runMoveCommand"要求对应的函数必须接收一个View类型的参数,原函数缺少该参数,导致点击时找不到对应方法崩溃,上述修正已添加该参数。
3. 额外优化建议
- 避免使用
GlobalScope,建议改用lifecycleScope(需添加依赖implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"),防止内存泄漏。 - 将Retrofit实例抽为全局单例,无需每次请求都重新创建。
内容的提问来源于stack exchange,提问作者Musical Coder
相关产品推荐
相关产品推荐

