Should I cancel coroutine in android activity? - android

I started coroutine here to handle retrofit call without ViewModel directly in the activity:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_change_pass)
job = Job()
coroutineScope = CoroutineScope(Dispatchers.Main)
}
retrofit call:
private fun changePassCall(user: User) {
coroutineScope.launch {
var changePassDeferred = UserApiObj.retrofitServiceCoroutine.changePass(user, bearerToken)
try {
var response = changePassDeferred?.await()
Toast.makeText(this#ChangePassActivity, "Pass changed", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
progressDialog.dismiss()
}
}
}
I cancel it here:
override fun onDestroy() {
super.onDestroy()
job.cancel()
}

you could use lifecycleScope to launch a coroutine and you dont have to create a job or cancel it anymore.

Related

How to use Coroutine in singleton properly

I want to create a singleton with Coroutine to load image from network. I have done implement the singleton and can load network image into imageView. Here is my singleton class.
class Singleton(context: Context) {
private val TAG = "Singleton"
private val scope =
CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineExceptionHandler { _, exception ->
Log.e(TAG, "Caught $exception")
})
private var job:Job? = null
companion object {
private var INSTANCE: Singleton? = null
#Synchronized
fun with(context: Context): Singleton {
require(context != null) {
"ImageLoader:with - Context should not be null."
}
return INSTANCE ?: Singleton(context).also {
INSTANCE = it
Log.d("ImageLoader", "First Init")
}
}
}
private fun onAttachStateChange(imageView: ImageView, job: Job) {
imageView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) {
}
override fun onViewDetachedFromWindow(v: View?) {
job.cancel()
}
})
}
fun loadImage(url: String, imageView: ImageView) {
job = scope.launch {
try {
updateData(URL(url), imageView)
} catch (e: CancellationException) {
Log.d(TAG, "work cancelled!")
}
}.also {
onAttachStateChange(imageView, it)
}
}
suspend fun updateData(url: URL, imageView: ImageView) = run {
fetchImage(url)?.apply { imageView.setImageBitmap(this) }
?: imageView.setImageResource(R.drawable.ic_launcher_background)
}
fun stopUpdate() {
scope.cancel()
}
private suspend fun fetchImage(url: URL): Bitmap? {
return withContext(Dispatchers.IO) {
try {
val connection = url.openConnection() as HttpURLConnection
val bufferedInputStream = BufferedInputStream(connection.inputStream)
BitmapFactory.decodeStream(bufferedInputStream)
} catch (e: Exception) {
Log.e("TAG", e.toString())
null
}
}
}
}
My problem is when I cancel my coroutine scope in onDestroy() at ActivityB and than use my singleton again in ActivityA it won't do anything cause the scope have been cancel(). So is there any way to use Coroutine in singleton properly with scope.cancel() when activity is onDestroy(). Here is a demo:
class MainActivityA : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_activity)
Singleton.with(this).updateData(url, imageView)
}
}
class MainActivityB : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_activity)
}
override fun onDestroy() {
super.onDestroy()
// Do not need to call scope.cancel(). Cause when the view is
// detached it will cancel the job.
// Singleton.with(this).stopUpdate()
}
}
Edited
I have come up with an idea and have added into Singleton class. Using view.onAttachStateChange to detect whether the view is still attached to the window. If is detached then we can cancel the job. Is this a good way to doing so?
Singleton by definition lives forever, so I'm not really sure it makes sense to cancel its scope. What if you would need to use your singleton from multiple components at the same time? They would cancel jobs of other components.
To make sure you don't leak jobs of destroyed components, you can either create a child job per component and put all tasks under it or just do not define a custom scope at all and reuse the coroutine context of the caller.

Is Coroutine job auto cancelled upon exiting Activity?

I have the below code of a slow loading image
class MainActivity : AppCompatActivity() {
private lateinit var job: Job
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val imageLoader = ImageLoader.Builder(this)
.componentRegistry { add(SvgDecoder(this#MainActivity)) }
.build()
job = MainScope().launch {
try {
val request = ImageRequest.Builder(this#MainActivity)
.data("https://restcountries.eu/data/afg.svg")
.build()
val drawable = imageLoader.execute(request).drawable
Log.d("TrackLog", "Loaded")
findViewById<ImageView>(R.id.my_view).setImageDrawable(drawable)
} catch (e: CancellationException) {
Log.d("TrackLog", "Cancelled job")
}
}
}
override fun onDestroy() {
super.onDestroy()
// job.cancel()
}
}
If I exit the activity before the image loaded completed, I thought I should manually perform job.cancel() to get the coroutine canceled.
However, even when I commented out the job.cancel(), the job still get canceled when I exit MainActivity.
This is also true when I use either GlobalScope or even use a global variable scope and job.
val myScope = CoroutineScope(Dispatchers.IO)
private lateinit var job: Job
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val imageLoader = ImageLoader.Builder(this)
.componentRegistry { add(SvgDecoder(this#MainActivity)) }
.build()
job = myScope.launch {
try {
val request = ImageRequest.Builder(this#MainActivity)
.data("https://restcountries.eu/data/afg.svg")
.build()
val drawable = imageLoader.execute(request).drawable
Log.d("TrackLog", "Loaded")
findViewById<ImageView>(R.id.my_view).setImageDrawable(drawable)
} catch (e: CancellationException) {
Log.d("TrackLog", "Cancelled job")
}
}
}
override fun onDestroy() {
super.onDestroy()
// job.cancel()
}
}
I'm puzzled how did the job get canceled when we exit the Activity even when I don't call job.cancel().
Apparently, because my request is made of this#MainActivity
val request = ImageRequest.Builder(this#MainActivity)
.data("https://restcountries.eu/data/afg.svg")
.build()
hence, when exiting, the this#MainActivity is killed, hence the request also got terminated and perhaps canceled?
If we use baseContext
val request = ImageRequest.Builder(baseContext)
.data("https://restcountries.eu/data/afg.svg")
.build()
then we have to manually cancel the job during onDestroy
Therefore it is always safer to use lifecycleScope

Kotlin wait for async with coroutine

I would like to open a new activity when phoneViewModel and ScanViewModel are instantiated. They are instantiated by calling an async function InitialRead(). I'm logging each step, atm they are logged as done3 => done2 => done1
I would like to have them in this order:
done1 => done2 => done3
I have following code:
class MainBusinessActivity : AppCompatActivity() {
private lateinit var scanViewModel: ScanViewModel
private lateinit var phoneViewModel: PhoneViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_business)
}
private fun startEntitySetListActivity() = GlobalScope.async {
val sapServiceManager = (application as SAPWizardApplication).sapServiceManager
sapServiceManager?.openODataStore {
phoneViewModel = ViewModelProvider(this#MainBusinessActivity).get(PhoneViewModel::class.java).also {it.initialRead{Log.e("done", "done1")}}
scanViewModel = ViewModelProvider(this#MainBusinessActivity).get(ScanViewModel::class.java).also {it.initialRead{Log.e("done", "done2")}}
}
}
override fun onResume() {
super.onResume()
//startEntitySetListActivity()
runBlocking {
startEntitySetListActivity().await()
val intent = Intent(this#MainBusinessActivity, HomeActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
Log.e("done", "done3")
startActivity(intent)
}
}
}
What am I doing wrong? Can someone correct my code?
Never use runBlocking in an Android app. runBlocking completely defeats the purpose of using coroutines, and can lead to an ANR. You also probably should never use GlobalScope, which leads to UI leaks. You might possibly need it for some kind of long-running task that doesn't make sense to put in a service but doesn't have dependency on any UI components, but I can't think of any examples
You also shouldn't be instantiating your ViewModels in the background. That should be done in onCreate().
Make this function a suspend function, and it can break down the two tasks in the background simultaneously before returning.
Start your coroutine with lifecycleScope.
Assuming sapServiceManager?.openODataStore is an asynchronous task that takes a callback, you will need to wrap it in suspendCoroutine.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_business)
phoneViewModel = ViewModelProvider(this#MainBusinessActivity).get(PhoneViewModel::class.java)
scanViewModel = ViewModelProvider(this#MainBusinessActivity).get(ScanViewModel::class.java)
}
private suspend fun startEntitySetListActivity() = coroutineScope {
val sapServiceManager = (application as SAPWizardApplication).sapServiceManager
sapServiceManager ?: return
suspendCoroutine<Unit> { continuation ->
sapServiceManager.openODataStore { continuation.resume(Unit) }
}
listOf(
launch {
phoneViewModel.initialRead{Log.e("done", "done1")}
},
launch {
scanViewModel.initialRead{Log.e("done", "done2")}
}
).joinAll()
}
override fun onResume() {
super.onResume()
lifecycleScope.launch {
startEntitySetListActivity()
val intent = Intent(this#MainBusinessActivity, HomeActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
Log.e("done", "done3")
startActivity(intent)
}
}

onResume does not worked in viewmodel

my data is fetched only when it is created...im using viewmodel...when press back button it doesnt update the previous data..onresume is not working in this...
i refered this but none of those helped--> Reacting to activity lifecycle in ViewModel
i need help
thanks in advance
activity:--
class MyAccount : BaseClassActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.myaccount)
var mActionBarToolbar = findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbartable);
setSupportActionBar(mActionBarToolbar);
setEnabledTitle()
val resetbutton=findViewById<Button>(R.id.resetpwd)
resetbutton.setOnClickListener {
val i=Intent(applicationContext,
ResetPasswordActivity::class.java)
startActivity(i)
}
val editbutton=findViewById<Button>(R.id.editdetail)
editbutton.setOnClickListener {
val i=Intent(applicationContext, EditProfile::class.java)
startActivity(i)
}
hello()
}
override fun onResume() {
super.onResume()
hello()
}
fun hello(){
val first_name = findViewById<TextView>(R.id.firstname)
val last_name = findViewById<TextView>(R.id.lastname)
val emailuser = findViewById<TextView>(R.id.emailuser)
val phone_no = findViewById<TextView>(R.id.phone_no)
val birthday = findViewById<TextView>(R.id.birthday)
val image=findViewById<ImageView>(R.id.imageprofile)
val model = ViewModelProvider(this)[MyAccountViewModel::class.java]
model.viewmodel?.observe(this, object : Observer<My_account_base_response> {
override fun onChanged(t: My_account_base_response?) {
first_name.setText(t?.data?.user_data?.first_name)
last_name.setText(t?.data?.user_data?.last_name)
emailuser.setText(t?.data?.user_data?.email)
phone_no.setText(t?.data?.user_data?.phone_no).toString()
birthday.setText(t?.data?.user_data?.dob).toString()
Glide.with(applicationContext).load(t?.data?.user_data?.profile_pic)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.ic_launcher_foreground)
.into(image)
}
})
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
NavUtils.navigateUpFromSameTask(this)
true
}
else -> super.onOptionsItemSelected(item)
}
}}
viewmodel:--
class MyAccountViewModel(context: Application) :AndroidViewModel(context),LifecycleObserver{
private var MyAccountViewModels: MutableLiveData<My_account_base_response>? = null
val viewmodel: MutableLiveData<My_account_base_response>?
get() {
if (MyAccountViewModels == null) {
MyAccountViewModels = MutableLiveData<My_account_base_response>()
loadviewmodel()
}
return MyAccountViewModels
}
private fun loadviewmodel(){
val token :String = SharedPrefManager.getInstance(getApplication()).user.access_token.toString()
RetrofitClient.instance.fetchUser(token)
.enqueue(object : Callback<My_account_base_response> {
override fun onFailure(call: Call<My_account_base_response>, t: Throwable) {
Log.d("res", "" + t)
}
override fun onResponse(
call: Call<My_account_base_response>,
response: Response<My_account_base_response>
) {
var res = response
if (res.body()?.status == 200) {
MyAccountViewModels!!.value = response.body()
} else {
try {
val jObjError =
JSONObject(response.errorBody()!!.string())
Toast.makeText(getApplication(),
jObjError.getString("user_msg"),
Toast.LENGTH_LONG).show()
} catch (e: Exception) {
Log.e("errorrr", e.message)
}
}
}
})
}}
There are bunch of things wrong here, so let me provide you refactored code and explanation as much as I would be able to..
Activity:
class MyAccount : BaseClassActivity() {
private val mActionBarToolbar by lazy { findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbartable) }
private val resetbutton by lazy { findViewById<Button>(R.id.resetpwd) }
private val editbutton by lazy { findViewById<Button>(R.id.editdetail) }
private val first_name by lazy { findViewById<TextView>(R.id.firstname) }
private val last_name by lazy { findViewById<TextView>(R.id.lastname) }
private val emailuser by lazy { findViewById<TextView>(R.id.emailuser) }
private val phone_no by lazy { findViewById<TextView>(R.id.phone_no) }
private val birthday by lazy { findViewById<TextView>(R.id.birthday) }
private val image by lazy { findViewById<ImageView>(R.id.imageprofile) }
lateinit var model: MyAccountViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.myaccount)
setSupportActionBar(mActionBarToolbar)
setEnabledTitle()
model = ViewModelProvider(this)[MyAccountViewModel::class.java]
resetbutton.setOnClickListener {
val i = Intent(applicationContext, ResetPasswordActivity::class.java)
startActivity(i)
}
editbutton.setOnClickListener {
val i = Intent(applicationContext, EditProfile::class.java)
startActivity(i)
}
model.accountResponseData.observe(this, object : Observer<My_account_base_response> {
override fun onChanged(t: My_account_base_response?) {
first_name.setText(t?.data?.user_data?.first_name)
last_name.setText(t?.data?.user_data?.last_name)
emailuser.setText(t?.data?.user_data?.email)
phone_no.setText(t?.data?.user_data?.phone_no).toString()
birthday.setText(t?.data?.user_data?.dob).toString()
Glide.with(applicationContext)
.load(t?.data?.user_data?.profile_pic)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.ic_launcher_foreground)
.into(image)
}
})
}
override fun onResume() {
super.onResume()
model.loadAccountData()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
NavUtils.navigateUpFromSameTask(this)
true
}
else -> super.onOptionsItemSelected(item)
}
}
}
Few notes on your activity class:
You don't need to findViewById everytime, just do it once during onCreate or do it lazily. (FYI consider using kotlin synthetics or view binding or data binding)
Initialize your viewModel during onCreate method only. (That's the best way to do it)
Also observer your LiveData from ViewModel once, it should be also from the onCreate as it's the entry point to the activity and apart from config changes this method called only once. So, it's safe to observe it over there rather than during onResume which will be called multiple times during activity lifecycle. (The main issue your code wasn't working, so as a fix you only call your API method from ViewModel during resume)
ViewModel:
class MyAccountViewModel(context: Application) : AndroidViewModel(context) {
private val _accountResponseData = MutableLiveData<My_account_base_response?>()
val accountResponseData: MutableLiveData<My_account_base_response?>
get() = _accountResponseData
init {
loadAccountData()
}
fun loadAccountData() {
val token: String = SharedPrefManager.getInstance(getApplication()).user.access_token.toString()
RetrofitClient.instance.fetchUser(token)
.enqueue(object : Callback<My_account_base_response> {
override fun onFailure(call: Call<My_account_base_response>, t: Throwable) {
Log.d("res", "" + t)
_accountResponseData.value = null
}
override fun onResponse(
call: Call<My_account_base_response>,
response: Response<My_account_base_response>
) {
var res = response
if (res.body()?.status == 200) {
_accountResponseData.value = response.body()
} else {
try {
val jObjError =
JSONObject(response.errorBody()!!.string())
Toast.makeText(
getApplication(),
jObjError.getString("user_msg"),
Toast.LENGTH_LONG
).show()
} catch (e: Exception) {
Log.e("errorrr", e.message)
}
}
}
})
}
}
Don't make initial API call along with LiveData creation, it's okay to do in most of cases but if you're updating LiveData on response of that call then it's good to make it separately like during init block.
It's good practice not to allow Ui (Activity/Fragments) to modify LiveDatas of ViewModel directly. So, that's good sign you're following such pattern by having private MutableLiveData exposed as public LiveData, but do it correctly as suggested.
Side note: Your view model doesn't need to be LifecycleObserver. LifecycleObserver is used for some custom class/component which needs to be managed by their self by silently observing/depending on activity lifecycle independently. That's not the use case of ViewModel.
The only thing that I found why your code wasn't working correctly is because you were creating & observing ViewModel & LiveData over & over again as new objects from onResume method where you called hello() method.
Let me know if something don't make sense or missing.

CoroutineScope - CompletableDeferred cancellation

I have two questions about this topic. I will use these in android with use case classes and i try to implement an architecture similar to this https://www.youtube.com/watch?v=Sy6ZdgqrQp0 but i need some answers.
1) I have a deferred with async builder and when i cancel job then the
other chains cancelled too. This code prints "Call cancelled". But i am not sure that if i am doing correct.
fun main(args: Array<String>) = runBlocking<Unit> {
val job = GlobalScope.launch {
println(getUser())
}
job.cancelAndJoin()
}
suspend fun getUser() = getUserDeferred().await()
suspend fun getUserDeferred() = coroutineScope {
val request = Request.Builder()
.url("https://jsonplaceholder.typicode.com/users")
.build()
val call = OkHttpClient().newCall(request)
val deferred = async(Dispatchers.IO) {
val body = call.execute()
body.body()?.string() ?: ""
}
deferred.invokeOnCompletion {
if (deferred.isCancelled) {
println("Call cancelled")
call.cancel()
}
}
deferred
}
2) I can't find a way to cancel this one. I want to use this in retrofit2 call adapter, is there any better way to handle this case.
fun main(args: Array<String>) = runBlocking<Unit> {
val job = GlobalScope.launch {
println(getUser1())
}
job.cancelAndJoin()
}
suspend fun getUser1() = getUser1Deferred().await()
fun getUser1Deferred(): Deferred<String> {
val request = Request.Builder()
.url("https://jsonplaceholder.typicode.com/users")
.build()
val call = OkHttpClient().newCall(request)
val deferred = CompletableDeferred<String>()
call.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
deferred.complete("Error")
}
override fun onResponse(call: Call, response: Response) {
deferred.complete(response.body()?.string() ?: "Error")
}
})
deferred.invokeOnCompletion {
if (deferred.isCancelled) {
println("Call cancelled")
call.cancel()
}
}
return deferred
}
You should avoid the first approach because it blocks a thread in a thread pool. Using the second approach you can propagate cancellation both ways. If you cancel the Deferred it will cancel the call, and if the call fails, it will cancel the Deferred with the exception it got.
fun getUserAsync(): Deferred<String> {
val call = OkHttpClient().newCall(Request.Builder()
.url("https://jsonplaceholder.typicode.com/users")
.build())
val deferred = CompletableDeferred<String>().apply {
invokeOnCompletion {
if (isCancelled) {
call.cancel()
}
}
}
call.enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
deferred.complete(response.body()?.string() ?: "Error")
}
override fun onFailure(call: Call, e: IOException) {
deferred.cancel(e)
}
})
return deferred
}
However, going the Deferred route is probably a red herring. If you are cancelling it, the underlying reason is that you're bailing out of the whole task you're doing. You should instead cancel the whole coroutine it runs in. If you properly implement structured concurrency, everything will happen automatically if your activity gets destroyed.
So my recommendation is to use this code:
suspend fun getUser() = suspendCancellableCoroutine<String> { cont ->
val call = OkHttpClient().newCall(Request.Builder()
.url("https://jsonplaceholder.typicode.com/users")
.build())
cont.invokeOnCancellation {
call.cancel()
}
call.enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
cont.resume(response.body()?.string() ?: "Error")
}
override fun onFailure(call: Call, e: IOException) {
cont.resumeWithException(e)
}
})
}
If you absolutely need the Deferred because you're running it concurrently in the background, it's easy to do using the above:
val userDeferred = this.async { getUser() }
Where I assume this is your activity, which is also a CoroutineScope.
The reason the 2nd case is not cancelling is because you are using CompletableDeferred. It isn't launched as a coroutine so isn't a child of your parent coroutine. So if you cancel the parent it will not cancel the deferred.
It works in the first case because async starts a new child coroutine which is linked to the parent. when you cancel either one they both get cancelled.
In order to link the Deferred to your parent Job you would need a reference to it and use invokeOnCompletion
var deferred : Deferred<Void>? = null
launch {
deferred = retroService.someDeferredCall()
deferred.await()
}.invokeOnCompletion {
//job was cancelled. Probably activity closing.
if(it is CancellationException) {
deferred?.let { it.cancel() }
}
}
Not terribly pretty but should get the job done.

Categories

Resources