Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Launch coroutine from click event in fragment

What is the proper way to launch a coroutine from a click event that is defined in a fragment? From my understanding, GlobalScope.launch is used if you want to launch a coroutine that is supposed to remain in memory for the entire lifecycle of the app. But since a fragment usually has a shorter lifecycle than the app, GlobalScope.launch probably isn't the proper way. I assume that if I used GlobalScope.launch, it might keep the fragment from being garbage collected?

I really only need to launch the coroutine from a click event handler, which means that there is no parent functions that I would be calling from.

like image 241
Johann Avatar asked Jan 06 '20 08:01

Johann


2 Answers

You need a job to handle the coroutine cancelation to prevent leaks:

//inside Fragment
val job = Job()
val uiScope = CoroutineScope(Dispatchers.Main + job)


//late in the button click

button.setOnClickListener{
  uiScope.launch(Dispatchers.IO){
    //asyncOperation
    withContext(Dispatchers.Main){
     //ui operation
   }

  }
}

//later in on destroy:

override fun onDestroy(){
  job.cancel()
  super.onDestroy()
}

You can also use LifecycleScope extension from Google:

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        lifecycleScope.launch {
            val params = TextViewCompat.getTextMetricsParams(textView)
            val precomputedText = withContext(Dispatchers.Default) {
                PrecomputedTextCompat.create(longTextContent, params)
            }
            TextViewCompat.setPrecomputedText(textView, precomputedText)
        }
    }
}

Edit, in case you would re-fire another operation. Just use the same scope:

//let's assume you have a second button

button2.setOnClickListener{
  uiScope.launch(Dispatchers.IO){
    //perform second operation
  }
}
like image 125
coroutineDispatcher Avatar answered Nov 06 '22 23:11

coroutineDispatcher


GlobalScope.launch is used if you want to launch a coroutine that is supposed to remain in memory for the entire lifecycle of the app.

This isn't necessarily so, it could be used for any coroutine that isn't coupled to an activity or phase of the app that the user could navigate away from. For example, you might launch a task that sends a one-way message to your server. It will probably finish quite soon, and you want it to finish whatever the user does later on.

I assume that if I used GlobalScope.launch, it might keep the fragment from being garbage collected?

Only if the coroutine retains a reference to the fragment or a part of it, and only if it has the potential to run for a long time.

Specifically, the typical thing you do in an on-click event is perform some action that involves your back end (i.e., networking) and then updates the UI. Clearly this can take a long time (especially in case of bad network) and it retains a reference to the UI element it's going to touch later on. This should be bound to the lifecycle of the fragment.

What is the proper way to launch a coroutine from a click event that is defined in a fragment?

The easiest way is like this, following the official documentation:

class MyFragment : Fragment, CoroutineScope by MainScope {

    override fun onDestroy() {
        cancel() // extension on CoroutineScope
    }

    ... rest of your fragment code ...
}

This captures the idiom that you previously had to write by hand (as seen in the other answer here).

like image 2
Marko Topolnik Avatar answered Nov 06 '22 23:11

Marko Topolnik