Understanding Kotlin Coroutine While Learning Android Development

in GEMS5 years ago

My journey to developing a real android project has really been a fun one since I began studying with Google codelabs tutorials. Just a few weeks back, I shared what I did on
Kotlin Database creation here. Kindly do check it out if you haven't. At the ending of that post, I promised to make a new post on the following points;

  • How threads work in Android.
  • How to use Kotlin's coroutines to move database operations away from the main thread.
  • How to display formatted data in a TextView

In today's post, I will be explaining how I used Kotlin Coroutine in the sample android project I am working with.


Creating A ViewModel & ViewModelFactory

Just because I'm working on a SleepTracking App which will definitely require data to be passed from one fragment to the other, also, to simplify this development, its codes will need to use the MVVM architecture.

The first step needed here is to create a veiwModel for the SleepTracker app . Below is the code where a variable is declared to access the SleepDatabaseDao we created in our previous lesson.

class SleepTrackerViewModel(
       val database: SleepDatabaseDao,
       application: Application) : AndroidViewModel(application) {
}

In a later time, we'll complete the above code by writing functions to perform clicks and text views for the app. Before then, let's create SleepTrackerViewModelFactory which will handle arguments that would be transferred between the available fragments. This is a boilerplate code and can be reused by just editing a few lines.

class SleepTrackerViewModelFactory(
       private val dataSource: SleepDatabaseDao,
       private val application: Application) : ViewModelProvider.Factory {
   @Suppress("unchecked_cast")
   override fun <T : ViewModel?> create(modelClass: Class<T>): T {
       if (modelClass.isAssignableFrom(SleepTrackerViewModel::class.java)) {
           return SleepTrackerViewModel(dataSource, application) as T
       }
       throw IllegalArgumentException("Unknown ViewModel class")
   }
}

Now, we are going to update SleepTrackerViewModel that we created earlier with the following codes;

  • A value that throws illegalexprtion if the value is null
    val application = requireNotNull(this.activity).application
  • A dataSource
    val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  • An instance of the videwModelFactory
    val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)
  • A reference to the SleepTrackerViewModel
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)


Here's how the final code for SleepTrackViewModelFactory will look like;

carbon (4).png

Adding DataBinding

Now, we want the SleepTrackerFragment to connect the ViewModel with the UI. let's add the following code to the fragment_sleep_tracker.xml.


<data>
   <variable
       name="sleepTrackerViewModel"
       type="com.example.android.trackmysleepquality.sleeptracker.SleepTrackerViewModel" />
</data>

In SleepTrackerFragment, we should set it as the lifecycle owner of the binding with this code;

binding.setLifecycleOwner(this)

Let's now assign the sleepTrackerViewModel binding variable to the sleepTrackerViewModel.

binding.sleepTrackerViewModel = sleepTrackerViewModel


Coroutines

The main focus in this post is to look at what Coroutine is and how to make use of it to simlify algorithm programming in Kotlin.

To my understanding, Coroutines is a mechanism in programing that programmers use while they want to make a function(s) in their codes to work asynchronously. With that said, you can now understand that Coroutine helps to suspend computation without delaying another function from running. A program can have several suspended function without blocking the main thread.

To use coroutines in Kotlin, you need three things:

  • A job
  • A dispatcher
  • A scope

Below are the explaintion from the document i'm learning from;

Job: Basically, a job is anything that can be canceled. Every coroutine has a job, and you can use the job to cancel the coroutine. Jobs can be arranged into parent-child hierarchies. Canceling a parent job immediately cancels all the job's children, which is a lot more convenient than canceling each coroutine manually.

Dispatcher: The dispatcher sends off coroutines to run on various threads. For example, Dispatcher.Main runs tasks on the main thread, and Dispatcher.IO offloads blocking I/O tasks to a shared pool of threads.

Scope: A coroutine's scope defines the context in which the coroutine runs. A scope combines information about a coroutine's job and dispatcher. Scopes keep track of coroutines. When you launch a coroutine, it's "in a scope," which means that you've indicated which scope will keep track of the coroutine.

Now, let us use Coroutine in our SleepTracker app.

As earlier stated, coroutines are Kotlin mechanism. Its dependencies would be added to our app module before we'll be able to use it.

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"


All of our coroutine codes will be stored in SleepTrackerViewModel So, let's begin.

From the explanation above, a job is basically anything that can be canceled. so let us create an instance of job().

var viewModelJob = Job()

That is, whenever viewModelJob is canceled, the whole thread running on SleepTrackerViewModel will be canceled. This is very handy because you don't have to cancel them one after the other. Now let us override OnCleared and then call viewModelJob.cancle();

override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}

We now need a scope where the coroutine will run on. This thread will then need to know the newly created job() instance. Dispatchers.Main This means that this coroutine will run on the main thread. We also have Dispatchers.IO for input and output from the Database.

private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)

Let us declare a variable to get toNight in MutableLiveData form.

private var tonight = MutableLiveData<SleepNight?>()

Create an Init block and then call initializeTonight() which will be declared next.

init {
   initializeTonight()
}

Define an initializeTonight function to get toNight value from dataBase. You should note that this function getTonightFromDatabase() hasn't been created and that will be our next task.

private fun initializeTonight() {
   uiScope.launch {
       tonight.value = getTonightFromDatabase()
   }
}

Creating a suspendgetTonightFromDatabase()

private suspend fun getTonightFromDatabase(): SleepNight? {
   return withContext(Dispatchers.IO) {
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       night
   }
}

From the above code, we are simply stating that if the Endtime from the latest night in the database is not the same as the Startime, then make night be null.

The code written in the above function is a long-running task and it's meant to be written in a suspended function. That's a coroutine rule.


Creating A Start Button Function

To successfully make the start button clickable, we'll need to write a onStartTracking() function in SleepTrackerViewModel.

 fun onStartSleepTracking() {
        uiScope.launch {
            val newNight = SleepNight()
            insert(newNight)
            toNight.value = getTonightFromDatabase()
        }
    }

Since we've got our xml file ready for data binding, we will now add the below code inside fragment_sleep_tracker.xml layout file to complete our Start button function.

android:onClick="@{() -> sleepTrackerViewModel.onStartTracking()}"

Displaying Results after start button is clicked

Our start button is functioning but needs to display the toNight on the screen. For this to be possible, we need to transform the display date using Transformations.map()

Let us declare a variable to access all nights from database
private val nights = database.getAllNights()

Now let's transform the Nights value to use nightsStrings file from our application resources.

val nightsString = Transformations.map(nights) { nights ->
   formatNights(nights, application.resources)
}
"@{sleepTrackerViewModel.nightsString}"


Now, let's build and run our application.

image.png

My next challenge

How to update an existing sleep-quality record in the database.
How to use LiveData to track button states.
How to display a snackbar in response to an event.

Sort:  

This post is really resourceful, love how you explained all those terms. It's nice to be challenging yourself to do more.

Thanks for sharing this post.

Your post has been curated with @gitplait community account because this is the kind of publications we like to see in our community.

Join our Community on Hive and Chat with us on Discord.

[Gitplait-Team]

That's really cool, bro @rufans

Keep it up!