Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Update UI async call with coroutines

I've got to update the UI with an async call to the Room Database, but when I do I've got this error : android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

// FavoritesPresenter.kt

GlobalScope.launch {
    favoritesView.showFavorites(ProductProvider.getAllProducts() as ArrayList<Product>)
}

// ProductProvider.kt

fun getAllProducts() : MutableList<Product> {
    return dao.getAllProducts()
}

// ProductDao.kt

@Query("SELECT * FROM product")
fun getAllProducts(): MutableList<Product>

What I need is to update my UI though my ProductProvider, as I'll use for all my entities I need a reliable solution.

like image 547
Biscuit Avatar asked Feb 13 '19 13:02

Biscuit


3 Answers

You should fetch from Room using an IO Coroutine, and switch to a Main (UI) Coroutine to update the view.

Try:

GlobalScope.launch(Dispatchers.IO) {
            val products = ProductProvider.getAllProducts() as ArrayList<Product>
            withContext(Dispatchers.Main) {
                favoritesView.showFavorites(products)
            }
        }

Make sure to have the Android Coroutine library installed so that Main Dispatcher correctly recognises the Android Main Thread.

api "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"
like image 171
TomH Avatar answered Sep 16 '22 13:09

TomH


Room 2.1 (currently in alpha) adds support for Kotlin coroutines. You can do the following:

  1. Mark functions in ProductDao and ProductProvider as suspend:

    // ProductDao.kt
    @Query("SELECT * FROM product")
    suspend fun getAllProducts(): List<Product>
    
    // ProductProvider.kt
    suspend fun getAllProducts(): List<Product> = dao.getAllProducts()
    
  2. Create local scope for a coroutine in FavoritesPresenter:

    class FavoritesPresenter {
        private var favoritesView: FavoritesView? = null
        private val provider: ProductProvider = ...// initialize it somehow
        private var job: Job = Job()
        private val scope = CoroutineScope(job + Dispatchers.Main)
    
        fun getProducts() {
            scope.launch {
                favoritesView?.showFavorites(provider.getAllProducts())
            }
        }
    
        fun attachView(view: FavoritesView) {
            this.favoritesView = view
        }
    
        fun detachView() {
            job.cancel() // cancel the job when Activity or Fragment is destroyed
            favoritesView = null
        }
    
        interface FavoritesView {
            fun showFavorites(products: List<Product>)
        }
    }
    
  3. Use FavoritesPresenter in Activity or Fragment:

    class MainActivity : AppCompatActivity(), FavoritesPresenter.FavoritesView {
        lateinit var presenter: FavoritesPresenter
        override fun onCreate(savedInstanceState: Bundle?) {
           super.onCreate(savedInstanceState)
           // ...
           presenter = FavoritesPresenter()
           presenter.attachView(this)
           presenter.getProducts()
    
        }
    
        override fun onDestroy() {
            presenter.detachView()
            super.onDestroy()
        }
    
        override fun showFavorites(products: List<Product>) {
            // use products to update UI
        }
    }
    

To use Dispatchers.Main import:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
like image 39
Sergey Avatar answered Sep 17 '22 13:09

Sergey


It would be better not use GlobalScope, instead use your own CoroutineContext, for example:

class YourActivity : CoroutineScope {
    private lateinit var job: Job

    // context for io thread
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.IO + job

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    job = Job()
  }

    fun toDoSmth() {
        launch {
            // task, do smth in io thread
            withContext(Dispatchers.Main) {
              // do smth in main thread after task is finished
            }                  
        }
    }

   override fun onDestroy() {
      job.cancel()
      super.onDestroy()
   }
}
like image 34
Artem Botnev Avatar answered Sep 18 '22 13:09

Artem Botnev