Kotlin Coroutines

Coroutines are Kotlin’s way of dealing with asynchronous tasks. They can execute tasks sequentially.

Coroutines require a:

  • Job – cancellable background task with a lifecycle that culminates in its completion. Jobs can be arranged in a parent-child hierarchy so that cancellation of the parent cancels all child jobs
  • Dispatcher – sends off coroutines to run on various threads
  • Scope – combines information, including a job and dispatcher, to define the context in which a coroutine runs. Scopes keep track of coroutines

The keyword suspend is Kotlin’s way of marking a function, or function type, available to coroutines. When a coroutine calls a function marked suspend, instead of blocking until that function returns like a normal function call, it suspends execution until the result is ready then it resumes where it left off with the result. While it’s suspended waiting for a result, it unblocks the thread that it’s running on so other functions or coroutines can run. The suspend keyword doesn’t specify the thread code runs on. Suspend functions may run on a background thread or the main thread.

import ...

class MyViewModel(
        val database: MyDatabaseDao,
        application: Application) : AndroidViewModel(application) {

    private var viewModelJob = Job()

    private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob) // Set Dispatcher on the Main thread and the job

    private var myObject = MutableLiveData<MyObject?>()

    init {
        initialiseMyFunction() // When the class is instantiated call initialiseMyFunction
    }

    private fun initialiseMyFunction() {
        uiScope.launch { // This function starts by running on the Main thread as the Viewmodel usually is involved in updating the UI
            myObject.value = getObjectFromDatabase() // Then it calls the getObjectFromDatabase function
        }
    }

    private suspend fun getObjectFromDatabase(): MyObject? { // We make this a suspend functino so it won't block the UI Thread from doing work
        return withContext(Dispatchers.IO) { // This is run on the background thread as we don't eant it blocking the UI thread and messing up the user experience
            var myObject = database.getObject()
            myObject // Return the object
        }
    }

    override fun onCleared() {
        super.onCleared()
        viewModelJob.cancel() // Be sure to cancel the Job (and hence any associated coroutines) when the ViewModel is destroyed, to prevent memory leaks
    }
}

See examples in the Sleep Tracker app from the Google Udacity course: Developing Android Apps with Kotlin

FirebaseJobDispatcher

The Firebase alternative to JobScheduler – is able to schedule tasks with complex conditions and execute them when those conditions are met. Provides compatibility all the way back to API 9 (Gingerbread). Requires Google Play Services to be installed on the target phone as it depends on GooglePlayDriver.

Add dependencies to build.gradle:

implementation 'com.firebase:firebase-jobdispatcher:0.8.5'

Create a JobService class and add the 2 obligatory overrides:

public class MyJobService extends JobService {
    @Override
    public boolean onStartJob(JobParameters job) {
        // Do some work here
        return false; // Answers the question: "Is there still work going on?"
    }

    @Override
    public boolean onStopJob(JobParameters job) { // This method is called if the system has determined that you must stop execution of your job even before you've had a chance to call jobFinished
        return false; // Answers the question: "Should this job be retried?"
    }
}

As the JobService runs on the main thread, potentially slow processes should be handed off to an AsyncTask. So alternatively:

public class MyJobService extends JobService {
    private AsyncTask mNetworkTask;
    @Override
    public boolean onStartJob(final JobParameters job) {
        mNetworkTask = new AsyncTask() {
            @Override
            protected Object doInBackground(Object[] objects) {
                // Do something
                return null;
            }

            @Override
            protected void onPostExecute(Object o) { // When your AsyncTask has finished you must inform the JobService
                jobFinished(job, false); // Second parameter tells the Jobservice whether the task needs to be rescheduled (depends on RetryStrategy, set in FirebaseJobDispatcher code below)
            }
        };
        mNetworkTask.execute();
        return true; // There is still work going on
    }

    @Override
    public boolean onStopJob(JobParameters job) { // This method is called if the system has determined that you must stop execution of your job even before you've had a chance to call jobFinished
        return false; // Answers the question: "Should this job be retried?"
    }
}

Add the service to the AndroidManifest.xml:

<service
    android:exported="false"
    android:name=".MyJobService">
    <intent-filter>
        <action android:name="com.firebase.jobdispatcher.ACTION_EXECUTE"/>
    </intent-filter>
</service>

The actual code:

FirebaseJobDispatcher dispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(context)); // Create a new dispatcher using the Google Play driver
Bundle myExtrasBundle = new Bundle();
myExtrasBundle.putString("some_key", "some_value");

Job myJob = dispatcher.newJobBuilder()
    .setService(MyJobService.class) // the JobService that will be called - ESSENTIAL
    .setTag("my-unique-tag") // uniquely identifies the job - ESSENTIAL
    .setRecurring(false) // one-off job
    .setLifetime(Lifetime.UNTIL_NEXT_BOOT) // don't persist past a device reboot
    .setTrigger(Trigger.executionWindow(0, 60)) // start between 0 and 60 seconds from now
    .setReplaceCurrent(false) // don't overwrite an existing job with the same tag
    .setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL) // retry with exponential backoff
    .setConstraints( // constraints that need to be satisfied for the job to run
        Constraint.ON_UNMETERED_NETWORK, // only run on an unmetered network
        Constraint.DEVICE_CHARGING // only run when the device is charging
    )
    .setExtras(myExtrasBundle)
    .build();

dispatcher.mustSchedule(myJob); // Throws exception if fails (ScheduleFailedException)
OR
dispatcher.schedule(myJob); // Returns one of (int value) SCHEDULE_RESULT_SUCCESS, SCHEDULE_RESULT_UNKNOWN_ERROR, SCHEDULE_RESULT_NO_DRIVER_AVAILABLE, SCHEDULE_RESULT_UNSUPPORTED_TRIGGER, SCHEDULE_RESULT_BAD_SERVICE

To cancel a job:

dispatcher.cancel("my-unique-tag");

To cancel all jobs:

dispatcher.cancelAll();

ud851-Exercises-student\Lesson10-Hydration-Reminder\T10.04