CoolMind
CoolMind

Reputation: 28793

Wait until Kotlin coroutine finishes in onCreateView()

I have an initialization block in onCreateView, where some variables are assigned from SharedPreferences, DB or Network (currently from SharedPreferences).

I want to update views with these values in onViewCreated. But they update with empty values before a coroutine in onCreateView finishes. How to wait until the coroutine finishes in main thread?

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    ...
    GlobalScope.launch(Dispatchers.Main) {
        val job = GlobalScope.launch(Dispatchers.IO) {
            val task = async(Dispatchers.IO) {
                settingsInteractor.getStationSearchCountry().let {
                    countryName = it.name
                }
                settingsInteractor.getStationSearchRegion().let {
                    regionName = it.name
                }
            }
            task.await()
        }
        job.join()
    }
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    country.updateCaption(countryName)
    region.updateCaption(regionName)
}

UPDATE (20-05-2019)

Currently I don't use onViewCreated. I write code in onCreate and onCreateView. In onCreateView I access views this way: view.country.text = "abc" and so on.

Upvotes: 13

Views: 19284

Answers (3)

Sergio
Sergio

Reputation: 30655

In your case you don't need to use GlobalScope as a coroutine context (you can but it is not recommended as per docs). You need a local scope:

private var job: Job = Job()
override val coroutineContext: CoroutineContext
    get() = Dispatchers.Main + job

@Override
public void onDestroy() {
    super.onDestroy();
    job.cancel()
}

Also your fragment should implement CoroutineScope and to use Dispatchers.Main in Android add dependency to app's build.gradle:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.1'

The code to wait until the coroutine finishes:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    launch {
        val operation = async(Dispatchers.IO) {
            settingsInteractor.getStationSearchCountry().let {
                countryName = it.name
            }
            settingsInteractor.getStationSearchRegion().let {
                regionName = it.name
            }
        }
        operation.await() // wait for result of I/O operation without blocking the main thread

        // update views
        country.updateCaption(countryName)
        region.updateCaption(regionName)
    }
}

EDIT:

In Activity or Fragment you can use lifecycleScope instead of implementing CoroutineScope:

lifecycleScope.launch { ... }

To use lifecycleScope add next line to dependencies of the app's build.gradle file:

implementation "androidx.lifecycle:lifecycle-runtime-ktx:$LIFECYCLE_VERSION"

At the time of writing final LIFECYCLE_VERSION = "2.3.0-alpha05"

Upvotes: 5

CoolMind
CoolMind

Reputation: 28793

Use runBlocking. In this case you can block UI thread until coroutine completion. Of course, you are to be sure it will be fast enough (less than 3 sec) to not raise ANR.

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    ...
    val (countryName, regionName) = runBlocking {
        // Switch to background thread and continue blocking UI thread.
        withContext(Dispatchers.IO) {
            val countryName = settingsInteractor.getStationSearchCountry().let {
                it.name
            }
            val regionName = settingsInteractor.getStationSearchRegion().let {
                it.name
            }
            countryName to regionName
        }
    }

    // Assign data after runBlocking has finished.
    view.country_name.text = countryName
    view.region_name.text = regionName

    return view
}

I didn't test it, because use similar code in onBackPressed. When I quit a fragment (or an activity) I should write some data in DB. If running too late, onDestroy will be called and coroutines will be finished. In this case data won't be written to DB. So, I have to stop UI thread, write to DB in background thread, then return to UI. runBlocking works well.

Upvotes: -1

logcat
logcat

Reputation: 3454

It's better to use async instead of launch https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html.

Create lateinit val dataLoad: Deferred<Pair<String, String>. init it as dataLoad = GlobalScope.async(Dispatchers.Defailt) {} in onCreateView or earlier. launch new coroutine in UI scope in onViewCreated. Wait for result in that coroutine val data = dataLoad.await() and then apply data to ui

Upvotes: 6

Related Questions