Welcome back to our ongoing Quality of Life series! If you missed the previous post where we extended native types to simplify the boilerplate code for common tasks, I strongly suggest reading it. If you are already familiar with how Kotlin extensions work and you are looking for a way to use job scheduling in a functional manner, keep reading!
As always, the code and a working example app is available at the end of this post.
What do you mean by “job scheduling”?
A job is a piece of code representing a specific task, which is scheduled to be executed at a specific time, once or repeatedly, with or without a delay. In the good old days of Java, we tended to implement these types of jobs with interfacing custom classes as Runnables. Runnables are great, but they are too lengthy to write for simple tasks. A single line of business code requires five lines of boilerplate. Kotlin’s creators realized that wrapping a single method in an interface is the most widely used workaround method in Java for implementing anonymous functions found in other languages such as Javascript, Python and C++11., They introduced all the components missing from an actual functional programming language into Kotlin in order to try to get rid of the workaround. The result is Higher Order Functions and Lambdas.
Higher Order Functions are functions that can be passed as arguments to other functions. In Java, we can only pass objects as arguments using other methods; but using Kotlin extends this capability. Since Kotlin is a strongly typed language like Java, having the ability to pass functions as arguments requires them to have their own type.
For example a boolean has the type Boolean
in Kotlin. A function that accepts two booleans and returns a boolean has the type (Boolean, Boolean)->Boolean
. A function without arguments has the type ()->ReturnType
where ReturnType
can be any type. A function that returns nothing has the ReturnType
, Unit
. These capabilities allow us to express any kind of function in a type safe way.
// Higher order function example fun callFiveTimes(funcToCall: ()->Unit) { funcToCall() funcToCall() funcToCall() funcToCall() funcToCall() } // Higher order function example fun iterateTenTimes(funcToCall: (Int)->Unit) { for (i in 1..10) funcToCall(i) } // Some random free functions fun foo() { print("Hello") } fun bar(i: Int) { print(i) } // Test code fun test() { // Call with free functions callFiveTimes(foo) iterateTenTimes(bar) //////////////////////////// // Call with anonymous functions callFiveTimes({ print("Hola!") }) iterateTenTimes({ a:Int -> print(a) }) //////////////////////////// // Call with anonymous shorthand syntax // Refer to docs to learn how to use these sugars. callFiveTimes { print("Hola!") } iterateTenTimes { a -> print(a) } //////////////////////////// }
Lambda is just a fancy name for anonymous (unnamed) functions.
Job Handling the Old Way
Android’s built-in tools for handling jobs are encapsulated in the Handler class. This class, when instantiated, acts as a channel between your calling context and the current thread’s scheduler. You can send Runnable
instances to methods like post()
, postDelayed()
to schedule your jobs. Android also provides AyncTask.execute()
which accept a runnable for background scheduling in case you have a long running job.
Job Handling for Hipsters
For simplicity’s sake, we are going to implement 5 different cases for scheduling, which are each customizable if required.
These cases are:
- Periodic scheduling: Run the code every x milliseconds.
- Aperiodic scheduling: Run the code after x milliseconds.
- Observe for first occurrence: Run the code once the predicate is true.
- Observe for each occurrence: Run the code every time the predicate is true.
- Background scheduling: Run the code in another thread immediately.
We will allow schedules to be canceled if needed. Doing this will gracefully avoid crashes if a scheduled job throws exceptions.
Let’s define the imports and type aliases first:
import android.os.AsyncTask import android.util.Log interface ScheduledJob { fun cancel() } interface RunnableScheduledJob: ScheduledJob, Runnable {} typealias Lambda = ()->Unit typealias Predicate = ()->Boolean
We extended Runnable
to conform with Android’s native libraries. This also gives us the ability to extend existing behavior for jobs, such as allowing cancels. These ScheduledJob
objects will be the return values of our schedule methods. Calling RunnableScheduledJob.run()
as if it’s a Runnable
will execute the job and cause the schedule to be canceled in order to avoid executing the same job twice.
All of our utilities will be implemented as function extensions of Lambda
type.
Periodic Scheduling: Run the code every x milliseconds
Periodic jobs are executed repeatedly until canceled.
@JvmOverloads fun Lambda.runWithPeriod(period: Long = 1000, onCancel: () -> Unit = {}): ScheduledJob { val observer = android.os.Handler() val self = this val job = object : ScheduledJob, Runnable { private var canceled = false override fun run() { synchronized(this) { if (canceled) return self() observer.postDelayed(this, period) } } override fun cancel() { synchronized(this) { canceled = true observer.post(onCancel) } } } observer.post(job) return job }
Calling run()
on the returned ScheduledJob
is not allowed in order not to mess with the period. If provided, onCancel
callback will be called on the handler thread as soon as the cancel happens.
Aperiodic scheduling: Run the code after x milliseconds
Aperiodic jobs are executed once some amount of time passes.
@JvmOverloads fun Lambda.runWithDelay(delay: Long = 1000, onCancel: () -> Unit = {}): RunnableScheduledJob { val observer = android.os.Handler() val self = this val job = object : RunnableScheduledJob { private var canceled = false private var done = false override fun run() { synchronized(this) { if (canceled || done) return done = true self() } } override fun cancel() { synchronized(this) { canceled = true observer.post(onCancel) } } } observer.postDelayed(job, delay) return job }
Calling run()
on the returned RunnableScheduledJob
is allowed. It will simply execute the job prematurely and disable the execution after the specified delay.
Observe for first occurrence: Run the code once the predicate is true
This type of scheduling allows you to implement a logic like “Wait until X happens, then execute”. The event to wait for is checked with a given predicate function frequently. Since there is no guarantee that X will happen, we will allow the caller to specify a timeout duration for schedule to stop.
@JvmOverloads fun Lambda.observeOnce(predicate: Predicate, checkPeriod: Long = 40, timeout: Long = 1000): RunnableScheduledJob { val observer = android.os.Handler() val self = this // Enable this to detect neverending observers // final Exception e = new RuntimeException("WTF"); val job = object : RunnableScheduledJob { private var canceled = false private var done = false private var timeElapsed = 0 override fun run() { synchronized(this) { if (timeElapsed + checkPeriod > timeout) cancel() timeElapsed += 40 if (canceled) return var readyToProceed: Boolean? = null try { readyToProceed = predicate() } catch (e: Exception) { Log.v("Util", "Observing", e) observer.postDelayed(this, checkPeriod) return } if (readyToProceed && !done) { Log.v("Util", "Observed") done = true self() } else if (!done) { Log.v("Util", "Observing") observer.postDelayed(this, checkPeriod) } } } override fun cancel() { synchronized(this) { canceled = true } } } observer.post(job) return job }
Calling run()
on the returned RunnableScheduledJob
is allowed. It will check the predicate immediately and act accordingly if it is true.
Observe for each occurrence: Run the code every time the predicate is true
This type of scheduling allows you to implement a logic like “Unless canceled, every time it is necessary, execute”. The permission to execute frequently with a given predicate function. We will allow the caller to specify a timeout duration for schedule to stop.
@JvmOverloads fun Lambda.observe(predicate: Predicate, checkPeriod: Long = 40, timeout: Long = 1000): ScheduledJob { val observer = android.os.Handler() val self = this val job = object : RunnableScheduledJob { private var canceled = false private var timeElapsed = 0L override fun run() { synchronized(this) { if (timeElapsed + checkPeriod > timeout) cancel() timeElapsed += checkPeriod if (canceled) return var readyToProceed: Boolean? = null try { readyToProceed = predicate() } catch (e: Exception) { Log.v("Util", "Observing"); observer.postDelayed(this, checkPeriod) return } if (readyToProceed) { Log.v("Util", "Observed"); self() observer.postDelayed(this, checkPeriod) } else { Log.v("Util", "Observing"); observer.postDelayed(this, checkPeriod) } } } override fun cancel() { synchronized(this) { canceled = true } } } observer.post(job) return job }
Calling run()
on the returned ScheduledJob
is not allowed.
Background Scheduling: Run the code in another thread immediately
Some jobs will require a longer amount of time to complete their logic. These jobs will block the main thread and cause the user interface to freeze. Android forces applications which lock the main thread for more than a few seconds to quit. This doesn’t mean we are not allowed to run long running tasks entirely – a thread pool of background threads is always available to us. The code executed in these threads is free from UI.
Below we implement a Lambda
extension to invoke the lambda in a background thread.
fun Lambda.invokeInBackground(): RunnableScheduledJob { val self = this val job = object : RunnableScheduledJob { private var canceled = false private var done = false override fun run() { synchronized(this) { if (canceled || done) return done = true self() } } override fun cancel() { synchronized(this) { canceled = true } } } AsyncTask.execute(job) return job }
Calling run()
on the returned RunnableScheduledJob
is allowed. It will run the job in the current thread instead of a background thread and cancel the schedule in the background entirely to avoid conflicts due to multiple executions.
Usage
Since we implemented all our schedulers as Lambda
extensions, we can directly call them on anonymous function blocks. Pretty neat!
val random = Random() // This function will be called every 1 second until it is canceled. val repeatingJob = { "Hello World always.".LogD() }.runWithPeriod(1000) // repeatingJob.cancel() // This function will be called after 5 seconds passes if remains uncancelled. val oneTimeJob = { "Hello World with delay.".LogD() }.runWithDelay(5000) // oneTimeJob.cancel() // This function will be called every 40 milliseconds if and only if the predicate is true. val repeatedCheck = { "Sometimes I win randomly!".LogD() }.observe({ random.nextBoolean() }) // repeatedCheck.cancel() // This function will be called only once and only when the first time predicate returns true. val onceCheck = { "When I win, it ends! But I don't know when since it's random.".LogD() }.observeOnce({ random.nextBoolean() }) // onceCheck.cancel() // This function will be called in another thread to avoid locking the UI. ({ "Hello in another thread!".LogD() }).invokeInBackground() // onceCheck.cancel()
Let us know what kind of quality of life improvements you want to see next. Until then, peace 🙂
Working Code
All of the code in this article as well as an example project can be found here: https://github.com/testfairy-blog/KotlinQOL
This is the same Github repository we provided in the previous post. The file you are looking for is here.
Credits
-
Photo by Matthew Smith on Unsplash