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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 07:54:56