12

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.

Sergio
  • 27,326
  • 8
  • 128
  • 149
CoolMind
  • 26,736
  • 15
  • 188
  • 224

3 Answers3

6

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

logcat
  • 3,435
  • 1
  • 29
  • 44
5

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"

Sergio
  • 27,326
  • 8
  • 128
  • 149
  • Thanks! I tested this code. Maybe it's similar to @ArtemBotnev answer, but you hinted me how to make a coroutine context. Before I didn't know how to create it. – CoolMind Dec 07 '18 at 09:35
  • By now it has been well established that you should _not_ use `async-await` for non-concurrent code. You should write `withContext(Dispatchers.IO)`. This is especially relevant due to a very different and non-intuitive way that `async` treats exceptions that aren't handled within its body. – Marko Topolnik Dec 07 '18 at 20:02
  • Should we cancel a job on fragment recreate? For instance, if some request has uploaded a bitmap and we rotate a screen, should we can cancel it? – CoolMind Dec 10 '18 at 07:47
  • 1
    You can cancel it. When fragment is recreated it should launch coroutine again. You can use `setRetainInstance(true)` method to prevent recreation of the fragment. – Sergio Dec 10 '18 at 08:14
  • @Sergey, I had a problem with recreating fragments' jobs in `ViewPager` and changed this code, see https://stackoverflow.com/a/53117790/2914140. В общем, корутины не запускались в новых фрагментах, пока не начал создавать job по-новой. – CoolMind Dec 28 '18 at 09:40
-1

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.

CoolMind
  • 26,736
  • 15
  • 188
  • 224