I recently came across a code snippet that used Runnable with AsyncTask, which I was not familiar with previously.
AsyncTask.execute{
/* Some code to run in Background
* ...
* ...
*/
runOnUiThread{
//run on main thread, just like onPostExecute
}
}
I would like to know how does this compare with following way where we create an AsyncTask class?
class MyAsyncTask : AsyncTask<Unit, Unit, String>() {
override fun doInBackground(vararg params: Unit): String {...}
override fun onPostExecute(result: String) {...}
}
Are there any performance or other downsides of the first method?
I don't think it has any thing to do with performance. They are just different ways that you could use to implement this action. If I was writing this code, I would create a class and implement it there.
Related
I want to load some data inside activity after the button is clicked. I came up with the following solution and it works as I expect. But I just started learning kotlin coroutines and I want someone else to comment on my code. For example, is it okay that I update the UI using lifecycleScope.launch? I could probably use withContext(Dispatchers.Main) instead but is there a difference?
Is my implementation good in general? Is there something that could be optimzed/refactored?
I understand that it's better to use ViewModel and make API calls there but in this case I want all action to happen inside the activity.
class MainActivity : AppCompatActivity() {
var apiCallScope: CoroutineScope? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<View>(R.id.btn_load_content).setOnClickListener {
// Cancel previous API call triggered by the click.
// I don't want to have multiple API calls executing at the same time.
apiCallScope?.cancel()
showProgress(true)
apiCallScope = CoroutineScope(Dispatchers.IO)
apiCallScope!!.launch {
// Execute Retrofit API call
val content = api.loadContent().await()
// Update UI with the content from API call on main thread
lifecycleScope.launch {
showProgress(false)
drawContent(content)
}
}
}
}
override fun onDestroy() {
super.onDestroy()
apiCallScope?.cancel()
}
private fun showProgress(show: Boolean) {
// TODO implement
}
private fun drawContent(content: String) {
// TODO implement
}
}
It's preferable to use ViewModel to make such types of operations and not perform them inside Activity, especially in the onCreate method.
ViewModel gives you the viewModelScope property, any coroutine launched in this scope is automatically canceled if the ViewModel is cleared to avoid consuming resources.
With the below code I'm getting the following error: "Suspend function 'getSomethingFromAPI' should be called only from a coroutine or another suspend function.", which is current. getSomethingFromAPI is indeed a suspend function of the ViewModel.
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProvider(this).get(CallVM::class.java)
viewModel.applyLaunch {
this.getSomethingFromAPI()
}
}
fun <T: ViewModel> T.applyLaunch(block: T.() -> Unit)
= apply { viewModelScope.launch(Dispatchers.IO) { block() } }
As you can see though, in the applelaunch function I am executing getSomethingFromAPI inside of a coroutine (launch), but this information is lost. Is there any way to preserve it and keep T as ViewModel at the same time?
To be more specific, is it possible to have a shortcut function that implements two first lines of the below code?
viewModel.apply {
viewModelScope.launch(Dispatchers.IO) {
getSomethingFromAPI()
}
getSomethingFromAPI above sees both 'this' (ViewModel and coroutine).
I know it's not something important to have, but it might be good to know for creating DSL.
You are getting this error because you are trying to call a suspend function in a non-suspend lambda. Make lambda in applyLaunch suspend block: suspend T.() -> Unit
I have an Inherited class which all of my Activities inherit. In that Base/Parent Activity class I would like to create a Handler and Runnable Object that runs every 5 seconds in all Child Activities without having to add code in each Child Activity. I have it working, where the Runnable Object is in the Inherited Activity but I can't seem to get around having to add code for the Handler in each Child Activity.
open class BaseActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
val handlerBaseActivity = Handler()
val runnable = object : Runnable {
override fun run() {
Log.d("icepts","runnable from base...")
}
}
class MainActivity : BaseActivity () {
val handlerLocalActivity = Handler()
override fun onCreate(savedInstanceState: Bundle?) {
...
handlerLocalActivity.postDelayed(runnable, 100)
}
override fun onPause() {
super.onPause()
handlerBaseActivity.removeCallbacks(runnable)
}
Anyone have any ideas that they'd be interested in sharing? Thanks so much!
You can use Activity's lifecycle in the BaseActivity as well so that every child Activity does exactly the same work in their lifecycle callback. You need to post your Runable in the onResume() or onCreate() of the BaseActivity and remove it in onPause() or onDestroy(). Please see the example below,
BaseActivity
class BaseActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
val handlerBaseActivity = Handler()
val runnable = object : Runnable {
override fun run() {
Log.d("icepts","runnable from base...")
// TODO perform your repetitive operation
// call the handler again after 5 sec to perform above operation again
handlerBaseActivity.postDelayed(this, 5000)
}
}
override fun onResume() {
super.onResume()
handlerBaseActivity.postDelayed(runnable, 100)
}
override fun onPause() {
super.onPause()
handlerBaseActivity.removeCallbacks(runnable)
}
}
After that, you do not have to call postDelayed() function manually in each Activity that inherits BaseActivity. So, to make MainActivity do the same operation that is written in run() function in BaseActivity you just simply inherit BaseActivity.
class MainActivity : BaseActivity () {
// no need to declare local handler
override fun onCreate(savedInstanceState: Bundle?) {
...
// No need to call postDelayed() of localHandler as it's already called in BaseActiviy
}
// No need to remove callbacks in onPause() as it's already defined in BaseActivity
}
Update
I want to clear your confusion of the following comment,
If I put the removeCallBacks in the onPause of the BaseActivity then
the runnable wouldn't run at all because the onPause of the
BaseActivity gets executed before the Child Activity starts.
If you also have BaseActivity registered in the AndroidMannifest and it runs as an independent Activity, removing the runnable from it will not prevent any child from executing their runnable at all. Basically, all the childs will have their own instance of runnables and handlers that will be tied to their own lifecycles.
You can use a Thread to perform this task. I am aware that in Kotlin you have Coroutines which are cheaper to instantiate and easier to manage, so you might want to go that path. Make sure you create your handler outside of this thread, that way when you perform UI changes you don't get errors, however, if you want to perform background tasks then don't use a handler at all.
You can keep a reference to this thread so that you can pause it, by setting running to false and interrupting it, then setting it to null, for good measure, when you completely stop using it.
fun loop() {
handler.post(runnable)
//or
runOnUIThread(runnable)
//background task
}
Thread(Runnable{
while(running){
loop()
try{
Thread.sleep(1000 * 5)
}catch(e: InterruptedException){
//some weird stuff happened
}
}
}).start()
I'm using kotlin for android and I'm trying to create a generic Worker class in which I can pass a lambda which can be called from doWork() method.
class BaseWorker(val context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
override fun doWork(): Result {
//Passed from the activity while creating the work request
someLambda()
return Result.success()
}
}
The problem is I am not instantiating the BaseWorker class by calling the constructor.
Is it possible to pass the Lambda using setInputData() of OneTimeWorkRequestBuilder class.
I referred How to pass the worker parameters to WorkManager class where the constructor of the class is being called which I think is not the right way.
The Data class in WorkManager it's only intended for base types and their arrays. You cannot use it to pass a lamba.
A possible solution is to customize WorkManager's initialization, as explained in the documentation, and use a custom WorkerFactory to add a parameter to the constructor that you can use to retrieve the lambda. Keep in mind that you are configuring WorkManager just once, at initialization time. This means that you can pass directly the lambda as the additional parameter, but it will not be possible to customize it for each WorkRequest.
Depending on what you want to achieve exactly, something similar can be used as a starting point:
// provide custom configuration
val config = Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.INFO)
.setWorkerFactory(MyWorkerFactory(lambda))
.build()
//initialize WorkManager
WorkManager.initialize(this, config)
val workManager = WorkManager.getInstance()
And then have your WorkerFactory:
class MyWorkerFactory(private val lambda: Unit) : WorkerFactory() {
override fun createWorker(appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters): MyWorker {
return MyWorker(appContext, workerParameters, lambda)
}
}
You can then have your worker that use the new constructor:
class MyWorker(val context: Context, workerParams: WorkerParameters, private val lambda: Unit) : Worker(context, workerParams) {
override fun doWork(): Result {
//Passed from the WorkManager's configuration
lambda()
return Result.success()
}
}
Remember to disable the default WorkManager initialization adding to the AndroidManifest.xml:
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove" />
I think it's worth revisiting your question to see what you are trying to do and why it's fundamentally flawed. You are trying to pass a lambda from an Activity to a worker, which is something that runs in the background, even if the Activity is no longer around. This doesn't make any sense. Please don't do this - it's only going to result in memory leaks, crashes, and/or strange errors that will be hard for you to track down. Remember that workers need to be able to be created from scratch when the OS tells your app to run them.
Actually, you CAN do it ONLY using what WorkManager provides to us. IMO changing the WorkManager initialization is too complex/risk for a simple thing like this as suggested in the other answer.
WorkRequests accept ByteArray input, which can be any object, right? So, create a serialized object wrapping the lambda function that will be later called
Define a serializable wrapper class passing the lambda(logRequestTime): LambdaSerializable
package com.febaisi.lambdawithworkers
import java.io.Serializable
class LambdaSerializable(val logRequestTime: () -> (Unit)): Serializable {
}
Convert it to ByteArray and put it in the input object.
val lambdaObject = LambdaSerializable { //Lambda function
Log.e("Lambda", "Request time was -> $requestTime")
}
val input = Data.Builder()
.putByteArray(SimpleWorker.LAMBDA_OBJECT, serializeObject((lambdaObject as Object)))
.build()
val simpleWorker = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
.setInputData(input)
.build()
WorkManager.getInstance(applicationContext).enqueueUniqueWork("lambda_worker", ExistingWorkPolicy.KEEP, simpleWorker)
Call it from the worker. SimpleWorker
class SimpleWorker(context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) {
companion object {
val LAMBDA_OBJECT = "LAMBDA_OBJECT"
}
override fun doWork(): Result {
Log.e("Lambda", "Executing worker - Sleeping for 5 seconds - Compare request vs current time")
val lambdaSerializable = inputData.getByteArray(LAMBDA_OBJECT)?.let{ getByteInput(it) }
runBlocking {
delay(5000)
val sdf = SimpleDateFormat("yyyyMMdd__HH:mm:ss", Locale.getDefault())
Log.e("Lambda", "Current time is -> ${sdf.format(Date())}")
(lambdaSerializable as LambdaSerializable).logRequestTime() // High level - Calling Lambda
}
return Result.success()
}
}
FULL EXAMPLE HERE: https://github.com/febaisi/LambdaWithWorkers/
Check logs to see lambda function being called.
let's say i have an activity with instance variable loadedMovie and a method that executes AsyncTask which is in another file
class MainActivity:AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
var loadedMovie: Movie? = null
....
fun loadMovie() {
val task = LoadMovieTask(this)
task.execute()
}
}
separate AsyncTask
class LoadMovieTask(val ctx: Activity) : AsyncTask<Void, Void, Void>() {
var movie: Movie? = null
override fun onPreExecute() {
....
}
// loading information from network
override fun doInBackground(vararg params: Void?): Void? {
movie = load()
return null
}
// here i modify views with help of kotlin android extensions
override fun onPostExecute(result: Void?) {
....
}
}
problem is: somehow i can't modify loadedMovie neither from doInBackground (which is ok, because it runs on separate thread) and onPostExecute (which is not ok)
i just type ctx.loadedMovie in onPostExecute and it's not there.. maybe i don't understand something? or maybe there is another way to do it that i'm not aware of
Use this
class LoadMovieTask(val ctx: MainActivity)
instead of
class LoadMovieTask(val ctx: Activity)
MainActivity has the method and not the Android's Activity class itself. So even though you need the context, since you are trying to access the method specific to MainActivity, it is required to pass that