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

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="16dp">
<EditText android:id="@+id/editTextGroceryItem" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Enter grocery item"/>
<Button android:id="@+id/buttonAdd" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="ADD ITEM" android:layout_gravity="center_horizontal"/>
<TextView android:id="@+id/textViewGroceryItems" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" android:textAppearance="?android:attr/textAppearanceMedium"/>
<!-- Optional ScrollView in case of many items --> <ScrollView android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1">
<TextView android:id="@+id/textViewGroceryList" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" android:text="Grocery List:" android:textAppearance="?android:attr/textAppearanceMedium"/> </ScrollView>
</LinearLayout>
megaphone

GroceryItemDao.kt


package com.example.grocerylist.dao

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import com.example.grocerylist.data.GroceryItem
import com.example.grocerylist.model.GroceryItem

@Dao
interface GroceryItemDao {
@Insert
suspend fun insert(item: GroceryItem)

@Query("SELECT * FROM grocery_items")
suspend fun getAllItems(): List<GroceryItem>
}


megaphone

AppDatabase.kt

package com.example.grocerylist.model

import androidx.room.Database
import androidx.room.RoomDatabase
import com.example.grocerylist.dao.GroceryItemDao

// Specify your entities and set the version number. Whenever you change the schema, you'll need to update the version number and define a migration strategy.
@Database(entities = [GroceryItem::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
// Provide an abstract "getter" method for each DAO.
abstract fun groceryItemDao(): GroceryItemDao
}


megaphone

GroceryItem.kt

package com.example.grocerylist.model

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "grocery_items")
data class GroceryItem(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val name: String
)


megaphone

Lecture on Room Android App

Welcome to our lecture on the Android Room Persistence Library, a vital component of the Android Jetpack suite. Today, we'll walk through the foundational aspects of Room, exploring how it enhances app development by providing an abstraction layer over SQLite.
This session will demystify the code we've written for our Grocery List app, diving into the technical details and the integration with Android libraries.

Introduction to Room

Room is a database object mapping library that makes it easier to handle databases in Android.
It takes care of mundane tasks that you'd have to do with SQLite, such as creating tables and writing queries.
Room provides compile-time checks of SQLite statements and can return query results as LiveData or Flow, which helps you build robust, high-quality apps.

The Three Main Components of Room

Room operates on three major components:
Entity
DAO (Data Access Object)
Database

Entity

Entities represent the tables in your app's database. An entity class is a model @annotated with @Entity, where each instance corresponds to a row in the table.
Example from GroceryItem.kt:
@Entity(tableName = "grocery_items")

data class GroceryItem(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val name: String
)

Here, GroceryItem defines the structure of a table within the database with two columns: an ID and a name.

DAO

The DAO (Data Access Object) is an interface that defines the methods for accessing your database.
With annotations like @Insert, @Delete, @Update, and @Query, you declare how you'd like to interact with the entity.
Example from GroceryItemDao.kt:
@Dao
interface GroceryItemDao {
@Insert
suspend fun insert(item: GroceryItem)

@Query("SELECT * FROM grocery_items")
suspend fun getAllItems(): List<GroceryItem>
}
GroceryItemDao declares how we insert new items and retrieve all items from our grocery_items table.
The use of suspend keywords indicates that these operations can be called from a coroutine.

Database

This is an abstract class that extends RoomDatabase and ties entities and DAOs together.
Example from AppDatabase.kt:

@Database(entities = [GroceryItem::class], version = 1, exportSchema = false)
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.