Kotlin Coroutines the right way in Android

前端 未结 9 994
醉酒成梦
醉酒成梦 2020-12-02 06:51

I\'m trying to update a list inside the adapter using async, I can see there is too much boilerplate.

Is it the right way to use Kotlin Coroutines?

can this

相关标签:
9条回答
  • 2020-12-02 06:56

    After struggling with this question for days, I think the most simple and clear async-await pattern for Android activities using Kotlin is:

    override fun onCreate(savedInstanceState: Bundle?) {
        //...
        loadDataAsync(); //"Fire-and-forget"
    }
    
    fun loadDataAsync() = async(UI) {
        try {
            //Turn on busy indicator.
            val job = async(CommonPool) {
               //We're on a background thread here.
               //Execute blocking calls, such as retrofit call.execute().body() + caching.
            }
            job.await();
            //We're back on the main thread here.
            //Update UI controls such as RecyclerView adapter data.
        } 
        catch (e: Exception) {
        }
        finally {
            //Turn off busy indicator.
        }
    }
    

    The only Gradle dependencies for coroutines are: kotlin-stdlib-jre7, kotlinx-coroutines-android.

    Note: Use job.await() instead of job.join() because await() rethrows exceptions, but join() does not. If you use join() you will need to check job.isCompletedExceptionally after the job completes.

    To start concurrent retrofit calls, you can do this:

    val jobA = async(CommonPool) { /* Blocking call A */ };
    val jobB = async(CommonPool) { /* Blocking call B */ };
    jobA.await();
    jobB.await();
    

    Or:

    val jobs = arrayListOf<Deferred<Unit>>();
    jobs += async(CommonPool) { /* Blocking call A */ };
    jobs += async(CommonPool) { /* Blocking call B */ };
    jobs.forEach { it.await(); };
    
    0 讨论(0)
  • 2020-12-02 06:58

    Please find attached the implementation for a remote API call with Kotlin Coroutines & Retrofit library.

    import android.view.View
    import android.util.Log
    import androidx.lifecycle.MutableLiveData
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import com.test.nyt_most_viewed.NYTApp
    import com.test.nyt_most_viewed.data.local.PreferenceHelper
    import com.test.nyt_most_viewed.data.model.NytAPI
    import com.test.nyt_most_viewed.data.model.response.reviews.ResultsItem
    import kotlinx.coroutines.*
    import javax.inject.Inject
    
    class MoviesReviewViewModel @Inject constructor(
    private val nytAPI: NytAPI,
    private val nytApp: NYTApp,
    appPreference: PreferenceHelper
    ) : ViewModel() {
    
    val moviesReviewsResponse: MutableLiveData<List<ResultsItem>> = MutableLiveData()
    
    val message: MutableLiveData<String> = MutableLiveData()
    val loaderProgressVisibility: MutableLiveData<Int> = MutableLiveData()
    
    val coroutineJobs = mutableListOf<Job>()
    
    override fun onCleared() {
        super.onCleared()
        coroutineJobs.forEach {
            it.cancel()
        }
    }
    
    // You will call this method from your activity/Fragment
    fun getMoviesReviewWithCoroutine() {
    
        viewModelScope.launch(Dispatchers.Main + handler) {
    
            // Update your UI
            showLoadingUI()
    
            val deferredResult = async(Dispatchers.IO) {
                return@async nytAPI.getMoviesReviewWithCoroutine("full-time")
            }
    
            val moviesReviewsResponse = deferredResult.await()
            this@MoviesReviewViewModel.moviesReviewsResponse.value = moviesReviewsResponse.results
    
            // Update your UI
            resetLoadingUI()
    
        }
    }
    
    val handler = CoroutineExceptionHandler { _, exception ->
        onMoviesReviewFailure(exception)
    }
    
    /*Handle failure case*/
    private fun onMoviesReviewFailure(throwable: Throwable) {
        resetLoadingUI()
        Log.d("MOVIES-REVIEWS-ERROR", throwable.toString())
    }
    
    private fun showLoadingUI() {
        setLoaderVisibility(View.VISIBLE)
        setMessage(STATES.INITIALIZED)
    }
    
    private fun resetLoadingUI() {
        setMessage(STATES.DONE)
        setLoaderVisibility(View.GONE)
    }
    
    private fun setMessage(states: STATES) {
        message.value = states.name
    }
    
    private fun setLoaderVisibility(visibility: Int) {
        loaderProgressVisibility.value = visibility
    }
    
    enum class STATES {
    
        INITIALIZED,
        DONE
    }
    }
    
    0 讨论(0)
  • 2020-12-02 07:00

    We also have another option. if we use Anko library , then it looks like this

    doAsync { 
    
        // Call all operation  related to network or other ui blocking operations here.
        uiThread { 
            // perform all ui related operation here    
        }
    }
    

    Add dependency for Anko in your app gradle like this.

    implementation "org.jetbrains.anko:anko:0.10.5"
    
    0 讨论(0)
  • 2020-12-02 07:00

    Like sdeff said, if you use the UI context, the code inside that coroutine will run on UI thread by default. And, if you need to run an instruction on another thread you can use run(CommonPool) {}

    Furthermore, if you don't need to return nothing from the method, you can use the function launch(UI) instead of async(UI) (the former will return a Job and the latter a Deferred<Unit>).

    An example could be:

    fun loadListOfMediaInAsync() = launch(UI) {
        try {
            withContext(CommonPool) { //The coroutine is suspended until run() ends
                adapter.listOfMediaItems.addAll(resources.getAllTracks()) 
            }
            adapter.notifyDataSetChanged()
        } catch(e: Exception) {
            e.printStackTrace()
        } catch(o: OutOfMemoryError) {
            o.printStackTrace()
        } finally {
            progress.dismiss()
        }
    }
    

    If you need more help I recommend you to read the main guide of kotlinx.coroutines and, in addition, the guide of coroutines + UI

    0 讨论(0)
  • 2020-12-02 07:00

    If you want to return some thing from background thread use async

    launch(UI) {
       val result = async(CommonPool) {
          //do long running operation   
       }.await()
       //do stuff on UI thread
       view.setText(result)
    }
    

    If background thread is not returning anything

    launch(UI) {
       launch(CommonPool) {
          //do long running operation   
       }.await()
       //do stuff on UI thread
    }
    
    0 讨论(0)
  • 2020-12-02 07:01

    I think you can get rid of runOnUiThread { ... } by using UI context for Android applications instead of CommonPool.

    The UI context is provided by the kotlinx-coroutines-android module.

    0 讨论(0)
提交回复
热议问题