Android主线程与Room数据库访问冲突问题及优化方案咨询
Hey there! Let's fix this Room threading issue the right way. Your current Thread + nested runOnUiThread approach works, but it's not ideal—you risk unmanaged threads, memory leaks, and it's just not as clean as modern Android patterns. Here are two recommended approaches that solve both the "main thread database access" and "UI update from background thread" problems elegantly:
1. Use ViewModel + LiveData (Room's Built-In Async Support)
Room natively supports returning LiveData for queries, which means it automatically runs the database operation on a background thread and posts updates to the main thread. Perfect for UI updates!
Step 1: Update your DAO to return LiveData
Modify your DAO method to return LiveData<MutableList<DBDrink>> instead of a direct list:
@Dao interface DrinkDao { @Query("SELECT * FROM drinks") fun drinks(): LiveData<MutableList<DBDrink>> }
Step 2: Create a ViewModel to hold the LiveData
ViewModel survives configuration changes (like screen rotations) and keeps your data logic separate from the Activity:
class DrinkViewModel(private val drinkDao: DrinkDao) : ViewModel() { val drinksLiveData: LiveData<MutableList<DBDrink>> = drinkDao.drinks() }
(You can use ViewModelProvider or Hilt to inject the DAO into the ViewModel, but for simplicity, here's a basic setup.)
Step 3: Observe the LiveData in your Activity
In your MainActivity, observe the LiveData—this runs on the main thread automatically, so you can safely update the UI and Adapter:
class MainActivity : AppCompatActivity(), MainAdapter.OnDrinkListener { private lateinit var drinkViewModel: DrinkViewModel private var adapter: MainAdapter? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Initialize binding, adapter, etc. adapter = MainAdapter(this) recyclerView.adapter = adapter // Initialize ViewModel val db = YourDatabase.getInstance(this) drinkViewModel = ViewModelProvider(this)[DrinkViewModel::class.java] // Observe LiveData for updates drinkViewModel.drinksLiveData.observe(this) { drinkList -> if (drinkList.isNotEmpty()) { empty.text = null clHistory.visibility = View.VISIBLE // Make sure to show it when there's data! adapter?.updateData(drinkList) } else { clHistory.visibility = View.INVISIBLE empty.text = "No drinks found" // Optional: Add placeholder text } } } // ... rest of your Activity code }
2. Use Kotlin Coroutines (Structured Concurrency)
If you prefer more control over async operations, Kotlin Coroutines are the modern standard. They let you write asynchronous code that looks synchronous, with built-in thread management.
Step 1: Update DAO to use Suspend Functions
Mark your DAO method as suspend—this tells Kotlin it's a long-running operation that shouldn't run on the main thread:
@Dao interface DrinkDao { @Query("SELECT * FROM drinks") suspend fun drinks(): MutableList<DBDrink> }
Step 2: Fetch Data with Coroutines in Your Activity
Use lifecycleScope (tied to the Activity's lifecycle) to launch a coroutine, switch to an IO thread for the database call, then switch back to the main thread for UI updates:
private fun loadDrinks() { lifecycleScope.launch { // Runs on main thread by default val drinkList = withContext(Dispatchers.IO) { // Switches to IO thread for DB call db.dao().drinks() } // Back to main thread here—safe to update UI if (drinkList.isNotEmpty()) { empty.text = null clHistory.visibility = View.VISIBLE adapter?.updateData(drinkList) } else { clHistory.visibility = View.INVISIBLE } } }
(No more manual Threads or runOnUiThread—coroutines handle thread switching for you!)
Bonus: Optimize Your Adapter
Your current updateData method uses notifyDataSetChanged(), which refreshes the entire RecyclerView even if only a few items changed. For better performance, use DiffUtil to calculate changes and update only affected items:
class MainAdapter(private val onDrinkListener: OnDrinkListener) : RecyclerView.Adapter<MainAdapter.MainViewHolder>() { private var drinks = emptyList<DBDrink>() // Use immutable list for DiffUtil private val TAG = "MainAdapter" override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder { val binding: ItemDrinkBinding = DataBindingUtil.inflate( LayoutInflater.from(parent.context), R.layout.item_drink, parent, false ) return MainViewHolder(binding, onDrinkListener) } override fun getItemCount(): Int = drinks.size override fun onBindViewHolder(holder: MainViewHolder, position: Int) { holder.bind(drinks[position]) } fun updateData(newDrinks: List<DBDrink>) { val diffResult = DiffUtil.calculateDiff(DrinkDiffCallback(drinks, newDrinks)) drinks = newDrinks diffResult.dispatchUpdatesTo(this) } inner class MainViewHolder( private val binding: ItemDrinkBinding, private val onDrinkListener: OnDrinkListener ) : RecyclerView.ViewHolder(binding.root), View.OnClickListener { fun bind(drink: DBDrink) { binding.drinkName.text = drink.strDrink binding.drinkImage.load(drink.strDrinkThumb) { transformations(RoundedCornersTransformation(25f)) } itemView.setOnClickListener(this) } override fun onClick(v: View?) { // No need for a separate idDrink list—get it directly from drinks onDrinkListener.onDrinkClick(adapterPosition, drinks[adapterPosition].idDrink) } } interface OnDrinkListener { fun onDrinkClick(position: Int, idDrink: String) // Update to take single id instead of list } // DiffUtil Callback to calculate changes private class DrinkDiffCallback( private val oldList: List<DBDrink>, private val newList: List<DBDrink> ) : DiffUtil.Callback() { override fun getOldListSize(): Int = oldList.size override fun getNewListSize(): Int = newList.size override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return oldList[oldItemPosition].idDrink == newList[newItemPosition].idDrink } override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return oldList[oldItemPosition] == newList[newItemPosition] } } }
Note: I also removed the separate idDrink list—you can directly get the ID from the drinks list in onClick, which avoids duplicate entries and keeps your data source a single source of truth.
Why Your Original Approach Isn't Ideal
- Unmanaged Threads: The
Threadyou start isn't tied to the Activity's lifecycle—if the Activity is destroyed before the thread finishes, you'll get aNullPointerExceptionor memory leak. - Nested UI Calls: You have unnecessary nested
runOnUiThreadcalls, which makes the code harder to read. - No Configuration Change Support: If the screen rotates, your data will be reloaded from scratch (unless you handle it manually), whereas ViewModel preserves the data.
Content of the question comes from Stack Exchange, question author MXF

