How to use Coroutine in singleton properly - android

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.

Related

android.util.AndroidRuntimeException: Animators may only be run on Looper threads

I am getting an error in my project. What is the problem? can you help me?
android.util.AndroidRuntimeException: Animators may only be run on Looper threads
at com.nisaefendioglu.movieapp.ui.DetailMovieActivity$addToFav$1.invokeSuspend(DetailMovieActivity.kt:65)
MyCode :
DetailMovieActivity:
class DetailMovieActivity : AppCompatActivity() {
private lateinit var binding: ActivityDetailMovieBinding
var b:Bundle?=null
private lateinit var appDb : MovieDatabase
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding= ActivityDetailMovieBinding.inflate(layoutInflater);
appDb= MovieDatabase.getDatabase(this);
setContentView(binding.root)
b=intent.extras
val i=b?.getString("imdbid")
val apikey="93b3e8f8"
ApiClient.instances.getDetailMovie(i,apikey).enqueue(object :Callback<MovieDetailData> {
override fun onResponse(
call: Call<MovieDetailData>,
response: Response<MovieDetailData>
) {
binding.tvType.text = response.body()?.Release
binding.tvPlot.text=response.body()?.plot
Glide.with(this#DetailMovieActivity).load(response.body()?.poster)
.into(binding.imgPoster)
binding.imgToolbarBtnFav.setOnClickListener(){
addToFav(response.body());
}
}
override fun onFailure(call: Call<MovieDetailData>, t: Throwable) {
TODO("Not yet implemented")
}
})
binding.imgToolbarBtnBack.setOnClickListener {
finish()
}
}
private fun addToFav(body: MovieDetailData?) {
GlobalScope.launch(Dispatchers.IO) {
if (body?.let { appDb.movieDao().getById(it.Title)} !=null ) {
binding.imgToolbarBtnFav.setBackgroundResource(R.drawable.favorite_bg);
return#launch;
}else{
binding.imgToolbarBtnFav.setBackgroundResource(R.drawable.favorite_bg);
body?.let { appDb.movieDao().insert(it) }
}
}
}
}
MovieDatabase:
#Database(entities = [MovieDetailData::class],version = 2, exportSchema = false)
abstract class MovieDatabase: RoomDatabase() {
abstract fun movieDao() : MovieDao
companion object{
#Volatile
private var INSTANCE : MovieDatabase? = null
fun getDatabase(context: Context): MovieDatabase {
val tempInstance = INSTANCE
if(tempInstance != null){
return tempInstance
}
synchronized(this){
val instance = Room.databaseBuilder(
context.applicationContext,
MovieDatabase::class.java,
"movies2"
).build()
INSTANCE = instance
return instance
}
}
}
}
Hello, I am getting an error in my project. What is the problem? can you help me?
Hello, I am getting an error in my project. What is the problem? can you help me?
Hello, I am getting an error in my project. What is the problem? can you help me?
This is the problem: .launch(Dispatchers.IO)
Dispatchers.IO is a thread pool that is completely independent from Android's Looper system that various APIs like Glide use to run callbacks in asynchronous functions. Also, many Android View-related classes must be called on the Android main thread (which also has a Looper).
When in an Activity, you should use lifecycleScope to launch your coroutines, and you should not change the dispatcher since it appropriately uses Dispatchers.Main by default.
private fun addToFav(body: MovieDetailData?) {
lifecycleScope.launch {
if (body?.let { appDb.movieDao().getById(it.Title)} != null) {
binding.imgToolbarBtnFav.setBackgroundResource(R.drawable.favorite_bg) //TODO?
}else{
binding.imgToolbarBtnFav.setBackgroundResource(R.drawable.favorite_bg)
body?.let { appDb.movieDao().insert(it) }
}
}
}
You should only use Dispatchers.IO when you are calling blocking functions.
Suggestion: I don't think you should make body nullable in this function since it cannot do anything useful with a null body. The null checks make the code more confusing. You should push the null check to the caller. Then this function can be simplified.
you cannot use background thread to work with UI.
here is solution
private fun addToFav(body: MovieDetailData?) {
lifecycleScope.launch {
if (body?.let { appDb.movieDao().getById(it.Title)} !=null ) {
binding.imgToolbarBtnFav.setBackgroundResource(R.drawable.favorite_bg);
return#launch;
}else{
binding.imgToolbarBtnFav.setBackgroundResource(R.drawable.favorite_bg);
body?.let { appDb.movieDao().insert(it) }
}
}
}

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

Proper way to unregister a callback from an Application class

I have implemented a custom Application class in my app which handles updating the app theme before the app start up.
I also registered a network callback to set a variable each time there is a connection change.
My application class is as such:
Application.kt
package com.th3pl4gu3.mes.ui
.....
class MesApplication : Application() {
companion object {
#Volatile
private var INSTANCE: MesApplication? = null
fun getInstance() =
INSTANCE ?: synchronized(this) {
INSTANCE
?: MesApplication().also { INSTANCE = it }
}
}
override fun onCreate() {
super.onCreate()
// Assigns 'this' to the singleton object
INSTANCE = this
// Updates the application's theme
updateAppTheme()
// Start a network callback to monitor internet connection
startNetworkCallback()
}
private fun startNetworkCallback(){
try{
val cm = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val builder = NetworkRequest.Builder()
cm.registerNetworkCallback(builder.build(), object: ConnectivityManager.NetworkCallback(){
override fun onAvailable(network: Network) {
super.onAvailable(network)
Log.v("INTERNET_TEST", "AC: Network Available")
Global.isNetworkConnected = true
}
override fun onLost(network: Network) {
super.onLost(network)
Log.v("INTERNET_TEST", "AC: Network Lost")
Global.isNetworkConnected = false
}
})
Global.isNetworkConnected = false
}catch (e: Exception){
Global.isNetworkConnected = false
}
}
}
However, from the docs, they recommend to unregister this callback but the Application class lifecycle doesn't have any onPause or onDestroy function.
Is there any proper way to unregister this callback to not cause any memory leaks?
Also feel free to suggest any alternatives in case I am coding this wrong
In this case , you can use ActivityLifecycleCallbacks, to detect are any Activity of your is in Foreground?
ActivityLiveCycleListener
class ActivityLiveCycleListener(private val appStateListener: AppStateListener) : Application.ActivityLifecycleCallbacks {
companion object {
var foregroundActivities = 0
}
override fun onActivityPaused(p0: Activity) {
}
override fun onActivityStarted(p0: Activity) {
if(foregroundActivities == 0){
appStateListener.onAppForeGround()
}
foregroundActivities++
}
override fun onActivityDestroyed(p0: Activity) {
}
override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) {
}
override fun onActivityStopped(p0: Activity) {
foregroundActivities--
if(foregroundActivities == 0){
appStateListener.onAppBackground()
}
}
override fun onActivityCreated(p0: Activity, p1: Bundle?) {
}
override fun onActivityResumed(p0: Activity) {
}
}
And your interface can have two methods to indicate background/foreground state
interface AppStateListener{
fun onAppForeGround()
fun onAppBackground()
}
Now in Application onCreate(), register to ActivityLifeCycleListener
override fun onCreate(){
registerActivityLifecycleCallbacks(ActivityLiveCycleListener(object : AppStateListener{
override fun onAppForeGround() {
//start network listener
}
override fun onAppBackground() {
//remove network listener
}
}))
}

How do I notify Workmanager task completed to Service?

My Worker(for API call) starts from Service and I want to completion event send into Service class.
What should be best approach?
Calling from service:
PeriodicWorkRequest request = new PeriodicWorkRequest
.Builder(Worker.class, Constants.REPEAT_INTERVAL, TimeUnit.MINUTES)
.addTag(TAG)
.setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
.build();
WorkManager.getInstance(getApplicationContext()).enqueue(request);
Calling from WorkManager:
override fun doWork(): Result {
// API call
return Result.success()
}
Okay so what I would do is I would create common object for both Worker and Service class and utilize Observer pattern. This WorkObserver object would behave as a proxy between Service and Worker. Using Koin for example, it would look something like that:
class MyWorker: Worker(), KoinComponent {
private val workObserver: WorkObserver by inject()
override fun doWork(): Result {
val result = api.call().execute()
if(result.isSuccessful) {
workObserver.notifySuccess()
return Result.success()
} else {
workObserver.notifyError()
return Result.failure()
}
}
}
class MyService: Service(), KoinComponent {
private val workObserver: WorkObserver by inject()
override fun onCreate() {
workObserver.setOnResultListener { result ->
if(result) {
//do something
} else {
// do something else
}
}
override fun onDestroy() {
workObserver.setOnResultListener(null)
}
}
class WorkObserver {
private var onResultListener: ((Result) -> Unit)? = null
fun setOnResultListener(listener: ((Result) -> Unit)?) {
this.onResultListener = listener
}
fun notifySuccess() {
this.onResultListener?.invoke(true)
}
fun notifyError() {
this.onResultListener?.invoke(false)
}
}
Of course you can use other DI tools for that, you can have a list of listeners and remove particular ones, you can pass any other object through the listener in WorkObserver with the payload you need. I just created a simple boolean passing
For that simple case however if you don't want to use DI, simple Object would do the work. However when your codebase grows and you are dealing with multithreading issues, or even accessing this object in other parts of the application it may lead to problems. I am using it only to pass information between objects, I don't recommend using it for storing data etc:
class MyWorker: Worker() {
override fun doWork(): Result {
val result = api.call().execute()
if(result.isSuccessful) {
WorkObserver.notifySuccess()
return Result.success()
} else {
WorkObserver.notifyError()
return Result.failure()
}
}
}
class MyService: Service() {
override fun onCreate() {
WorkObserver.setOnResultListener { result ->
if(result) {
//do something
} else {
// do something else
}
}
override fun onDestroy() {
WorkObserver.setOnResultListener(null)
}
}
object WorkObserver {
private var onResultListener: ((Result) -> Unit)? = null
fun setOnResultListener(listener: ((Result) -> Unit)?) {
this.onResultListener = listener
}
fun notifySuccess() {
this.onResultListener?.invoke(true)
}
fun notifyError() {
this.onResultListener?.invoke(false)
}
}

Should I cancel coroutine in android activity?

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.

Categories

Resources