Quality of Life Job Scheduling (Kotlin)

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 Runnableto 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