Share
Explore

Room App Grocery List v3

image.png

Zip File of Class Project:
This code was developed using:
Android Studio Iguana | 2023.2.1 Patch 2 Build #AI-232.10300.40.2321.11668458, built on April 3, 2024 Runtime version: 17.0.10+7-b1000.50 amd64 VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o. Windows 11.0 GC: G1 Young Generation, G1 Old Generation Memory: 4096M Cores: 20 Registry: ide.experimental.ui=true
Non-Bundled Plugins: com.tabnine.TabNine (1.74.0) Docker (232.10300.41)
megaphone

Using the libraries:

kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
Deep dive into the Kotlin coroutines and their application in an Android app utilizing the Room library for data persistence.
Coroutines are a fundamental part of modern Android development, enabling us to perform asynchronous tasks succinctly and efficiently.
How are coroutines used in our Grocery List app, specifically focusing on the Kotlin Coroutine Dispatchers and context-switching mechanisms provided by the launch and withContext functions.

Kotlin Coroutines

Coroutines are a concurrency design pattern used to simplify asynchronous programming by turning asynchronous callbacks into sequential code.
Kotlin's implementation of coroutines introduces a new way of writing asynchronous, non-blocking code.

Dispatchers and Threads

Coroutines run within a context defined by a CoroutineDispatcher.
Dispatchers determine what thread or threads the corresponding coroutine uses for its execution.
Dispatchers.Main: This dispatcher is confined to the Main thread of the Android application.
It's primarily used for interacting with the UI and performing quick work.
Dispatchers.IO: Optimized for disk and network I/O off the main thread.
For instance, reading or writing from the database, reading or writing files, and running network operations.
Dispatchers.Default: Optimized for CPU-intensive work off the main thread, such as sorting a list and parsing JSON.

Applying Coroutines in the Grocery List App

Initialization with launch

When we want to perform an operation that should not block the UI, such as inserting an item into the Room database, we use the launch function from the lifecycleScope of an Activity or Fragment.

lifecycleScope is bound to the lifecycle of the Activity or Fragment and automatically cancels the operation if the lifecycle is destroyed.

lifecycleScope.launch(Dispatchers.IO) {
database.groceryItemDao().insert(GroceryItem(name = groceryName))
// Other database operations
}

In the above code, lifecycleScope.launch(Dispatchers.IO) starts a coroutine in the I/O dispatcher, perfect for our database call which performs I/O operations under the hood.

Switching Contexts with withContext

Sometimes we start an operation in one context, like the I/O context for database access, but then need to switch back to the Main thread to update the UI with the results.

lifecycleScope.launch(Dispatchers.IO) {
// Perform database operations
val items = database.groceryItemDao().getAllItems()
withContext(Dispatchers.Main) {
// Update the UI
}
}

Here, withContext(Dispatchers.Main) switches the context of the coroutine to the main thread.
It's a suspending function that changes the coroutine's dispatcher while preserving its original state and call stack.

Best Practices

Use Dispatchers.Main sparingly, only for operations that update the UI or need to execute quickly on the main thread.
Offload blocking I/O operations to Dispatchers.IO.
Be mindful of the lifecycle of coroutines in UI controllers.
Using lifecycleScope is preferred because it ties the coroutine's lifecycle to the Activity or Fragment.
Consider using structured concurrency to create a new scope when the lifetime of the coroutine is not tied to the UI or the application's lifetime.

Conclusion

In summary, coroutines are an incredibly powerful tool for handling asynchronous tasks in Kotlin, especially when combined with Room's database operations.
Coroutines allow for writing clean, concise, and straightforward code, eliminating callback hell and making error handling easier.
Our Grocery List app serves as a practical example of using Kotlin coroutines with Room. We use launch to initiate non-blocking database operations and withContext to switch back to the main thread for UI updates.
In our upcoming lab, we'll build on this knowledge and experiment with coroutines further, tackling common asynchronous patterns and learning how to manage complex sequences of asynchronous tasks with ease.


image.png
megaphone

Project Assets

Project Level Build Gradle File
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.jetbrainsKotlinAndroid) apply false
}

App Model Build Gradle File
plugins {
id("com.android.application")
kotlin("android")
kotlin("kapt")
}

android {
namespace = "com.example.grocerylist"
compileSdk = 34

defaultConfig {
applicationId = "com.example.grocerylist"
minSdk = 21
targetSdk = 34
versionCode = 1
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

buildFeatures {
viewBinding = true
}

buildTypes {
getByName("release") {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = "1.8"
}
}

dependencies {
implementation("androidx.core:core-ktx:1.7.0")
implementation("androidx.appcompat:appcompat:1.4.1")
implementation("com.google.android.material:material:1.5.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")

// Room components
implementation("androidx.room:room-runtime:2.4.2")
kapt("androidx.room:room-compiler:2.4.2")
// Kotlin Extensions and Coroutines support for Room
implementation("androidx.room:room-ktx:2.4.2")
// Kotlin coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")
}

Your build.gradle.kts file for the app module for a Kotlin-based Android app using Room and View Binding contains the necessary plugins for Kotlin and Kapt (Kotlin annotation processing), sets up the Android SDK versions, and declares the dependencies for Room and coroutines, among others.
Here's a brief overview of the key parts of your configuration:
namespace: Declares a namespace for resources, ensuring resource names are unique.
compileSdk: Specifies the API level of the Android SDK that you compile the app against.
defaultConfig: Contains default settings and entries for your app.
viewBinding: Enables View Binding, allowing you to more easily interact with views.
buildTypes: Defines the build types, like 'release'. You have ProGuard enabled for code shrinking and optimization.
compileOptions & kotlinOptions: Configures the project to use Java 8 and sets the JVM target for Kotlin to 1.8, which is necessary for certain language features like lambda expressions.
The dependencies section includes:
Core KTX, AppCompat, Material Design, and ConstraintLayout for fundamental app functionality and UI.
JUnit for testing, along with the AndroidX Test library and Espresso for UI testing.
Room library components for database operations.
Kotlin coroutines for managing background tasks with a cleaner API and improved performance.
Remember to check the versions of dependencies to ensure they are up to date and compatible with each other. Outdated or incompatible versions can sometimes cause build failures or runtime issues. It's also good practice to check for newer versions of these dependencies, as they might include important bug fixes and performance improvements. You can find the latest versions of these libraries on the .
Before running the app, sync the project with the updated Gradle files by clicking "Sync Now" in Android Studio. If everything is correctly set up, you should be able to build and run your app without issues.


megaphone

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.grocerylist"> <!-- Make sure to put your actual package name here -->

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:targetApi="31">

<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Other activities or components if any -->
</application>

</manifest>

megaphone

MainActivity.kt

package com.example.grocerylist

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.room.Room
import com.example.grocerylist.databinding.ActivityMainBinding
import com.example.grocerylist.model.AppDatabase
import com.example.grocerylist.model.GroceryItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var database: AppDatabase

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

// Initialize the database
database = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "grocery-database"
).build()

// Set up the button click listener
binding.buttonAdd.setOnClickListener {
val groceryName = binding.editTextGroceryItem.text.toString()
if (groceryName.isNotEmpty()) {
addGroceryItem(groceryName)
binding.editTextGroceryItem.text.clear()
}
}

// Load grocery items
loadGroceryItems()
}

private fun addGroceryItem(groceryName: String) {
// Launch a coroutine in the IO context
lifecycleScope.launch(Dispatchers.IO) {
database.groceryItemDao().insert(GroceryItem(name = groceryName))
loadGroceryItems() // Refresh the list after insertion
}
}

private fun loadGroceryItems() {
// Launch a coroutine in the IO context
lifecycleScope.launch(Dispatchers.IO) {
val items = database.groceryItemDao().getAllItems()
// Switch to the Main context to update the UI
withContext(Dispatchers.Main) {
displayGroceryItems(items)
}
}
}

private fun displayGroceryItems(items: List<GroceryItem>) {
binding.textViewGroceryItems.text = items.joinToString(separator = "\n") { it.name }
}
}


megaphone

activity_main.xml

Want to print your doc?
This is not the way.
Try clicking the ⋯ next to your doc name or using a keyboard shortcut (
CtrlP
) instead.