Using WorkManager in Android
Tags: android, computerscience, kotlin, programming
Published: 2021-08-07 16:09:13

*In case it helped :)*
<a href="https://www.buymeacoffee.com/aseemwangoo" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Pass Me A Coffee!!" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>

<!-- wp:paragraph -->
<p>We will cover briefly:</p>
<!-- /wp:paragraph -->

<!-- wp:list {"ordered":true} -->
<ol><li>Custom WorkManager</li><li>OneTimeWorkRequest</li><li>PeriodicWorkRequest</li><li>Write Tests for Workers (step2 and step3)</li></ol>
<!-- /wp:list -->

{% youtube 1KxQ_eeLeOw %}

<!-- wp:heading {"level":3} -->
<h3>Custom WorkManager</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>There is detailed description <a href="https://developer.android.com/topic/libraries/architecture/workmanager" rel="noreferrer noopener" target="_blank">here</a> for WorkManager</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>As per the docs</p>
<!-- /wp:paragraph -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>By default, WorkManager configures itself automatically when your app starts. If you require more control of how WorkManager manages and schedules work, you can customise the WorkManager configuration.</p></blockquote>
<!-- /wp:quote -->

<!-- wp:list -->
<ul><li>Install the dependencies inside <code>app.gradle</code>&nbsp;</li></ul>
<!-- /wp:list -->

<!-- wp:code -->
<pre class="wp-block-code"><code>def work_version = "2.5.0"
implementation "androidx.work:work-runtime-ktx:$work_version"
androidTestImplementation "androidx.work:work-testing:$work_version"</code></pre>
<!-- /wp:code -->

<!-- wp:list -->
<ul><li>Remove the default initialiser from <code>AndroidManifest.xml</code>&nbsp;</li></ul>
<!-- /wp:list -->

<!-- wp:code -->
<pre class="wp-block-code"><code>&lt;provider
   android:name="androidx.work.impl.WorkManagerInitializer"
   android:authorities="${applicationId}.workmanager-init"
   tools:node="remove" /></code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Create your application class and define your own custom configuration.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>class TodoApplication() : Application(), Configuration.Provider {
    override fun onCreate() {
      super.onCreate()
      // LATER PUT THE PERIODIC WORK REQUEST HERE
    }
    override fun getWorkManagerConfiguration(): Configuration {
 
      return if (BuildConfig.DEBUG) {
         Configuration.Builder().setMinimumLoggingLevel(Log.DEBUG).build()
      } 
      else {
         Configuration.Builder().setMinimumLoggingLevel(Log.ERROR).build()
      }
    }
}</code></pre>
<!-- /wp:code -->

<!-- wp:separator {"className":"is-style-dots"} -->
<hr class="wp-block-separator is-style-dots"/>
<!-- /wp:separator -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>Here, we use <strong>Configuration.Provider</strong> to extend our TodoApplication<strong>, </strong>and override the <strong>getWorkManagerConfiguration</strong></p></blockquote>
<!-- /wp:quote -->

<!-- wp:paragraph -->
<p>In our case, we just configure the logging level. For the complete list of customisations available, see the <code><a href="https://developer.android.com/reference/androidx/work/Configuration.Builder" rel="noreferrer noopener" target="_blank"><em>Configuration.Builder()</em></a></code></p>
<!-- /wp:paragraph -->

<!-- wp:separator {"className":"is-style-dots"} -->
<hr class="wp-block-separator is-style-dots"/>
<!-- /wp:separator -->

<!-- wp:heading {"level":3} -->
<h3>OneTimeWorkRequest</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>This is how our folder structure looks like</p>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center"} -->
<div class="wp-block-image"><figure class="aligncenter"><img src="https://cdn-images-1.medium.com/max/1600/1*G2B3z87Fc8VOtTIy3hC03Q.png" alt="Workers in the App"/><figcaption>Workers in the&nbsp;App</figcaption></figure></div>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>For creating any worker, we need the following:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>Worker (this section)&nbsp;: Actual work you want to perform in the background</li><li>WorkRequest (created in the viewmodel section): This represents a request to do some work.</li><li>WorkManager (created above): Schedules your <code>WorkRequest</code> and makes it run</li></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>We will start with <strong>OnDemandBackupWorker</strong>, which basically aims to save the data onto some backend, (for our demo, we fake in the network call)</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>This is then followed by <strong>FileWorker</strong>, which creates a file onto the device and appends the timestamp into the newly created file</p>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center"} -->
<div class="wp-block-image"><figure class="aligncenter"><img src="https://cdn-images-1.medium.com/max/1600/1*qIQ1C9WNMTttHjEJjLprCw.png" alt="FileWorker Output"/><figcaption>FileWorker Output</figcaption></figure></div>
<!-- /wp:image -->

<!-- wp:heading {"level":4} -->
<h4>OnDemandBackupWorker</h4>
<!-- /wp:heading -->

<!-- wp:list -->
<ul><li>This class extends from the <a href="https://developer.android.com/reference/kotlin/androidx/work/CoroutineWorker" rel="noreferrer noopener" target="_blank">CoroutineWorker</a> (provides interop with Kotlin Coroutines)</li><li>We override the doWork function for our suspending work</li></ul>
<!-- /wp:list -->

<!-- wp:code -->
<pre class="wp-block-code"><code>override suspend fun doWork(): Result {
    val appContext = applicationContext
    showNotifications("Backing up the data", appContext)
    return try {
       val res = dummyWork()
       val outputData = workDataOf(KEY_ONDEMANDWORKER_RESP to res)
       Result.success(outputData)
    } catch (throwable: Throwable) {
       Timber.e(throwable)
       Result.failure()
    }
}
private suspend fun dummyWork(): String {
     // Faking the network call
     sleep()
     return "Completed successfully!"
}</code></pre>
<!-- /wp:code -->

<!-- wp:separator {"className":"is-style-dots"} -->
<hr class="wp-block-separator is-style-dots"/>
<!-- /wp:separator -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>Here, we create a dummyWork function, (which puts the thread to sleep) and returns string result.</p></blockquote>
<!-- /wp:quote -->

<!-- wp:list -->
<ul><li>The result is then put inside <code>workDataOf</code> (this converts list of pairs to Data object) using a key (which should be a string)</li><li>This result/output is then passed onto Worker’s <code>Result.success</code> that indicates the work completed successfully</li><li>In case of any error, we call Worker’s <code>Result.failure</code>&nbsp;</li></ul>
<!-- /wp:list -->

<!-- wp:separator {"className":"is-style-dots"} -->
<hr class="wp-block-separator is-style-dots"/>
<!-- /wp:separator -->

<!-- wp:heading {"level":4} -->
<h4>FileWorker</h4>
<!-- /wp:heading -->

<!-- wp:list -->
<ul><li>This class extends from the <a href="https://developer.android.com/reference/androidx/work/Worker" rel="noreferrer noopener" target="_blank">Worker</a> (performs work synchronously on a background thread)</li><li>We override the doWork function for our synchronous work</li></ul>
<!-- /wp:list -->

<!-- wp:code -->
<pre class="wp-block-code"><code>override fun doWork(): Result {
return try {
   val content="Backed up on ${dateFormatter.format(Date())}"
   val outputUri = saveToFile(appContext, content)
   val data=workDataOf(KEY_FILEWORKER_RESP to outputUri.toString())
   Result.success(data)
   
 } catch (throwable: Throwable) {
     Timber.e(throwable)
     Result.failure()
   }
}</code></pre>
<!-- /wp:code -->

<!-- wp:separator {"className":"is-style-dots"} -->
<hr class="wp-block-separator is-style-dots"/>
<!-- /wp:separator -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>Here, we create a file using saveToFile, and put in the current timestamp as text into that file</p></blockquote>
<!-- /wp:quote -->

<!-- wp:list -->
<ul><li>The result is then put inside <code>workDataOf</code> and like before, we pass it into the Worker’s <code>Result.success</code></li></ul>
<!-- /wp:list -->

<!-- wp:separator {"className":"is-style-dots"} -->
<hr class="wp-block-separator is-style-dots"/>
<!-- /wp:separator -->

<!-- wp:heading {"level":4} -->
<h4>OnDemandBackupViewModel</h4>
<!-- /wp:heading -->

<!-- wp:list -->
<ul><li>This class is responsible for creating the work request and extends from the <a href="https://developer.android.com/reference/androidx/lifecycle/AndroidViewModel" rel="noreferrer noopener" target="_blank">AndroidViewModel</a>.</li><li>We define two functions (<em>beginBackup and cancelBackup</em>) inside this class</li></ul>
<!-- /wp:list -->

<!-- wp:code -->
<pre class="wp-block-code"><code>internal fun beginBackup() {
   val constraints = Constraints.Builder()
     .setRequiresStorageNotLow(true)
     .setRequiresBatteryNotLow(true)
     .setRequiredNetworkType(NetworkType.CONNECTED)
     .build()
var continuation = workManager.beginUniqueWork(
       ONDEMAND_BACKUP_WORK_NAME,
       ExistingWorkPolicy.KEEP,
    OneTimeWorkRequest.from(OnDemandBackupWorker::class.java)
   )
// BACKUP WORKER
   val backupBuilder = OneTimeWorkRequestBuilder&lt;OnDemandBackupWorker>()
        
  
  backupBuilder.addTag(TAG_BACKUP)
  backupBuilder.setConstraints(constraints)
  continuation = continuation.then(backupBuilder.build())
// SAVE FILE WORKER
  val saveInFile = OneTimeWorkRequest.Builder(FileWorker::class.java)
      .setConstraints(constraints)
      .addTag(TAG_FILE)
      .build()
  continuation = continuation.then(saveInFile)
  continuation.enqueue()
}

internal fun cancelBackup() {
   workManager.cancelUniqueWork(ONDEMAND_BACKUP_WORK_NAME)
}</code></pre>
<!-- /wp:code -->

<!-- wp:separator {"className":"is-style-dots"} -->
<hr class="wp-block-separator is-style-dots"/>
<!-- /wp:separator -->

<!-- wp:list -->
<ul><li>Specify the constraints under which the worker is supposed to run, (in our case we specify storage, battery and internet)</li><li>We get an instance of workmanager inside our viewmodel class. Using this instance, we call the <code>beginUniqueWork</code>&nbsp;</li></ul>
<!-- /wp:list -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>Since, we need to chain our work requests, hence we use <code>beginUniqueWork</code></p></blockquote>
<!-- /wp:quote -->

<!-- wp:list -->
<ul><li>We give our worker a unique name (basically a string), specify the <code><a href="http://d.android.com/reference/androidx/work/ExistingWorkPolicy" rel="noreferrer noopener" target="_blank">ExistingWorkPolicy</a></code> with options as<code><a href="http://d.android.com/reference/androidx/work/ExistingWorkPolicy#keep" rel="noreferrer noopener" target="_blank">KEEP</a></code> and create a OneTimeWorkRequest from OnDemandBackupWorker class</li><li>The output from <code>beginUniqueWork</code> is a WorkContinuation.</li><li>Next, we create the WorkRequest Builder, using <code>OneTimeWorkRequestBuilder</code>&nbsp;. We add the tag for this work, which is used to later identify the progress for this work</li><li>This request is added to the <code>WorkContinuation</code>&nbsp;</li><li>We repeat the last two steps for our FileWorker</li><li>Finally, our chain of work request chain needs to be added to the queue (in order to run on the background thread). This is done using <code>enqueue</code>&nbsp;</li></ul>
<!-- /wp:list -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>Note: For cancelling a work request, we simply call the <code>cancelUniqueWork</code> with the tag (used for creating the work)&nbsp;</p></blockquote>
<!-- /wp:quote -->

<!-- wp:separator {"className":"is-style-dots"} -->
<hr class="wp-block-separator is-style-dots"/>
<!-- /wp:separator -->

<!-- wp:heading {"level":4} -->
<h4>Tracking Progress of WorkRequest</h4>
<!-- /wp:heading -->

<!-- wp:list -->
<ul><li>Since we added tags for our Worker, we can utilise them to get the status of any <code>WorkRequest</code>&nbsp;</li><li>It returns a <code>LiveData</code> that holds a <code><a href="http://d.android.com/reference/androidx/work/WorkInfo" rel="noreferrer noopener" target="_blank">WorkInfo</a></code> object. <code>WorkInfo</code> is an object that contains details about the current state of a <code>WorkRequest</code>&nbsp;</li></ul>
<!-- /wp:list -->

<!-- wp:code -->
<pre class="wp-block-code"><code>internal val backupDataInfo: LiveData&lt;List&lt;WorkInfo>> = workManager
        .getWorkInfosByTagLiveData(TAG_BACKUP)</code></pre>
<!-- /wp:code -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>Here, <code>TAG_BACKUP</code> is our tag, specified previously</p></blockquote>
<!-- /wp:quote -->

<!-- wp:separator {"className":"is-style-dots"} -->
<hr class="wp-block-separator is-style-dots"/>
<!-- /wp:separator -->

<!-- wp:heading {"level":3} -->
<h3>PeriodicWorkRequest</h3>
<!-- /wp:heading -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>Note: PeriodicBackupWorker is the same like FileWorker (only difference is the file content)</p></blockquote>
<!-- /wp:quote -->

<!-- wp:list -->
<ul><li>This work executes multiple times until it is cancelled, with the first execution happening immediately or as soon as the given <code><a href="https://developer.android.com/reference/androidx/work/Constraints" rel="noreferrer noopener" target="_blank">Constraints</a></code> are met.</li><li>The next execution will happen during the period interval.</li><li>Go to our Application class and instantiate the <code><a href="https://developer.android.com/reference/androidx/work/PeriodicWorkRequest.Builder" rel="noreferrer noopener" target="_blank">PeriodicWorkRequest.Builder</a></code></li></ul>
<!-- /wp:list -->

<!-- wp:code -->
<pre class="wp-block-code"><code>val constraints = Constraints.Builder()
    .setRequiresStorageNotLow(true)
    .setRequiresBatteryNotLow(true)
    .setRequiredNetworkType(NetworkType.UNMETERED)
    .build()
val periodicBackup = PeriodicWorkRequestBuilder&lt;PeriodicBackupWorker>(1, TimeUnit.DAYS)
    .addTag(TAG_PERIODIC_BACKUP)
    .setConstraints(constraints)
    .build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
     PERIODIC_BACKUP_WORK_NAME,
     ExistingPeriodicWorkPolicy.KEEP,
     periodicBackup
 )</code></pre>
<!-- /wp:code -->

<!-- wp:separator {"className":"is-style-dots"} -->
<hr class="wp-block-separator is-style-dots"/>
<!-- /wp:separator -->

<!-- wp:list -->
<ul><li>We specify the constraints as needed</li><li>Using <code>PeriodicWorkRequestBuilder</code>, we create our <code>PeriodicWorkRequest</code>&nbsp;</li><li>The time interval is specified as once per day.</li></ul>
<!-- /wp:list -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>Periodic work has a minimum interval of 15 minutes. Also if your periodic work has constraints, it will not execute until the constraints are met, even if the delay between periods has been met.</p></blockquote>
<!-- /wp:quote -->

<!-- wp:list -->
<ul><li>We enqueue our <code>PeriodicWorkRequest</code> using <code>enqueueUniquePeriodicWork</code>, keeping the work policy as <code>Keep</code>&nbsp;</li></ul>
<!-- /wp:list -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>The normal lifecycle of a PeriodicWorkRequest is <code>ENQUEUED -&gt; RUNNING -&gt; ENQUEUED</code></p></blockquote>
<!-- /wp:quote -->

<!-- wp:separator {"className":"is-style-dots"} -->
<hr class="wp-block-separator is-style-dots"/>
<!-- /wp:separator -->

<!-- wp:heading {"level":3} -->
<h3>Write Tests for&nbsp;Workers</h3>
<!-- /wp:heading -->

<!-- wp:image {"align":"center"} -->
<div class="wp-block-image"><figure class="aligncenter"><img src="https://cdn-images-1.medium.com/max/1600/0*C1g7vmhrbR2S8WM_.gif" alt=""/></figure></div>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>We will now write tests for our workers: <code>OnDemandBackupWorker</code> and <code>PeriodicBackupWorker</code>&nbsp;</p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":4} -->
<h4><code>OnDemandBackupWorkerTest</code></h4>
<!-- /wp:heading -->

<!-- wp:list -->
<ul><li>Create a test class <a href="https://github.com/AseemWangoo/hands_on_kotlin/blob/master/app/src/androidTest/java/com/aseemwangoo/handsonkotlin/worker/OnDemandBackupWorkerTest.kt" rel="noreferrer noopener" target="_blank">OnDemandBackupWorkerTest</a></li><li>We annotate our class with <code>AndroidJUnit4</code> This is the thing that will drive the tests for a single class.</li></ul>
<!-- /wp:list -->

<!-- wp:code -->
<pre class="wp-block-code"><code>@RunWith(AndroidJUnit4::class)
class OnDemandBackupWorkerTest {
 private lateinit var context: Context
 private lateinit var executor: Executor
 @Before
 fun setUp() {
     context = ApplicationProvider.getApplicationContext()
     executor = Executors.newSingleThreadExecutor()
 }
 @Test
 fun testOnDemandBackupWorker() {
    val worker = TestListenableWorkerBuilder&lt;OnDemandBackupWorker> (context).build()
    
   runBlocking {
     val result = worker.doWork()
     assertTrue(result is ListenableWorker.Result.Success)
   }
 }
}</code></pre>
<!-- /wp:code -->

<!-- wp:separator {"className":"is-style-dots"} -->
<hr class="wp-block-separator is-style-dots"/>
<!-- /wp:separator -->

<!-- wp:list -->
<ul><li>We initialise the context and executor inside our <code>setUp</code> and write a single test <code>testOnDemandBackupWorker</code>&nbsp;</li><li>Since, our OnDemandBackupWorker is a CoroutineWorker, we make use of <code>TestListenableWorkerBuilder</code> which basically builds instances of <code><a href="https://developer.android.com/reference/androidx/work/ListenableWorker" rel="noreferrer noopener" target="_blank">ListenableWorker</a></code> used for testing.</li><li>Next, we call the <code>doWork</code> inside <a href="https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html" rel="noreferrer noopener" target="_blank">runBlocking</a>, (it executes the test synchronously on the main thread, which we want for testing)</li></ul>
<!-- /wp:list -->

<!-- wp:image {"align":"center"} -->
<div class="wp-block-image"><figure class="aligncenter"><img src="https://cdn-images-1.medium.com/max/1600/1*d3Pj16AbbPVVb-L8s2zd7Q.png" alt="OnDemandBackupWorkerTest"/><figcaption>OnDemandBackupWorkerTest</figcaption></figure></div>
<!-- /wp:image -->

<!-- wp:separator {"className":"is-style-dots"} -->
<hr class="wp-block-separator is-style-dots"/>
<!-- /wp:separator -->

<!-- wp:heading {"level":4} -->
<h4><code>PeriodicBackupWorkerTest</code></h4>
<!-- /wp:heading -->

<!-- wp:list -->
<ul><li>Create a test class <a href="https://github.com/AseemWangoo/hands_on_kotlin/blob/master/app/src/androidTest/java/com/aseemwangoo/handsonkotlin/worker/PeriodicBackupWorkerTest.kt" rel="noreferrer noopener" target="_blank">PeriodicBackupWorkerTest</a></li><li>Same as before, we initialise in <code>setUp</code> method and write 2 tests</li></ul>
<!-- /wp:list -->

<!-- wp:code -->
<pre class="wp-block-code"><code>// TEST 1
@Test
fun testPeriodicBackUpWorker() {
   val worker = TestWorkerBuilder&lt;PeriodicBackupWorker>(
        context = context,
        executor = executor
    ).build()
   val result = worker.doWork()
   assertTrue(result is ListenableWorker.Result.Success)
}</code></pre>
<!-- /wp:code -->

<!-- wp:separator {"className":"is-style-dots"} -->
<hr class="wp-block-separator is-style-dots"/>
<!-- /wp:separator -->

<!-- wp:list -->
<ul><li>Since, our PeriodicBackupWorker is a Worker, we make use of <code>TestWorkerBuilder</code> which basically builds instances of <code><a href="https://developer.android.com/reference/androidx/work/Worker" rel="noreferrer noopener" target="_blank">Worker</a></code> used for testing.</li><li>Next, we call the <code>doWork</code> and assert for the <code>Result.success</code>&nbsp;</li></ul>
<!-- /wp:list -->

<!-- wp:separator {"className":"is-style-dots"} -->
<hr class="wp-block-separator is-style-dots"/>
<!-- /wp:separator -->

<!-- wp:list -->
<ul><li>For the second test, we check if the periodic work state is <code>ENQUEUED</code></li></ul>
<!-- /wp:list -->

<!-- wp:code -->
<pre class="wp-block-code"><code>// TEST 2
@Test
fun testIfPeriodicBackupRunning() {
 WorkManagerTestInitHelper.initializeTestWorkManager(context)
 
 val testDriver = WorkManagerTestInitHelper.getTestDriver(context)
 val workManager = WorkManager.getInstance(context)
 val constraints = Constraints.Builder()
     .setRequiresStorageNotLow(true)
     .setRequiresBatteryNotLow(true)
     .setRequiredNetworkType(NetworkType.UNMETERED)
     .build()
 val request =
     PeriodicWorkRequestBuilder&lt;PeriodicBackupWorker> (repeatInterval=24, TimeUnit.HOURS)
         .setConstraints(constraints)
         .build()
 workManager.enqueue(request).result.get()
 with(testDriver) {
      this?.setPeriodDelayMet(request.id)
      this?.setAllConstraintsMet(request.id)
 }
 val workInfo = workManager.getWorkInfoById(request.id).get()
 assertEquals(workInfo.state, WorkInfo.State.ENQUEUED)
}</code></pre>
<!-- /wp:code -->

<!-- wp:list -->
<ul><li>We make use of <code>WorkManagerTestInitHelper</code> which helps initialise <code>WorkManager</code> for testing.</li><li>Set the constraints, create a PeriodicWorkRequest and enqueue it using the workManager instance</li><li>Next, we make use of <code>testDriver</code> to make the constraints meet and assert if the status is <code>ENQUEUED</code></li></ul>
<!-- /wp:list -->

<!-- wp:image {"align":"center"} -->
<div class="wp-block-image"><figure class="aligncenter"><img src="https://cdn-images-1.medium.com/max/1600/1*DIfcJr9nsoWlX2O0bU_O5Q.png" alt="PeriodicBackupWorkerTest"/><figcaption><code>PeriodicBackupWorkerTest</code></figcaption></figure></div>
<!-- /wp:image -->

{% youtube 1KxQ_eeLeOw %}

<!-- wp:paragraph -->
<p><a rel="noreferrer noopener" href="https://github.com/AseemWangoo/hands_on_kotlin" target="_blank"><em>Source code.</em></a>

*In case it helped :)*
<a href="https://www.buymeacoffee.com/aseemwangoo" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Pass Me A Coffee!!" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>