Skip to content

Storing Data Permanently Part 2/2 – Day 14 Android 14 Masterclass

Storing Data Permanently Part 2/2 - Day 14 Android 14 Masterclass

Storing Data Permanently Part 2/2 – Day 14 Android 14 Masterclass

Welcome to Day 14 of our Android 14 & Kotlin Masterclass. We’ll explore advanced aspects of RoomDatabase and its integration with SQLite, crucial for managing and manipulating data in Android apps. We’ll explore the essentials of CRUD operations, the role of Entity and DAO in Room, and the significance of abstract classes in Kotlin. This day is designed to deepen your understanding of data storage in Android development, preparing you for more complex topics in app creation.

 

1. Understanding CRUD Operations

RoomDatabase is part of the Android Architecture Components and provides an abstraction layer over SQLite. It simplifies database work and allows for more robust database access while harnessing the full power of SQLite.

CRUD is an acronym that stands for Create, Read, Update, and Delete. These are the four fundamental operations performed on data in any database management system, including in Android development using SQLite and RoomDatabase.

Let’s break down each operation:

  1. Create: This operation involves adding new data to the database. In the context of an app, it could be something like adding a new record (e.g., a new ‘wish’ in a wish-list app). In SQL terms, this is usually done using the INSERT statement.
    • Example: In your app, when a user adds a new wish, it triggers a CREATE operation where a new row representing that wish is added to the database.
  2. Read: This operation is used to retrieve data from the database. It can be a query for a specific item or a request for all records that meet certain criteria. In SQL, the SELECT statement is used for this purpose.
    • Example: If your app displays a list of all wishes, it uses a READ operation. The app queries the database and retrieves all the rows from the wish table.
  3. Update: This operation involves modifying existing data in the database. It is used when the information in a database needs to be changed or updated. In SQL, this is done using the UPDATE statement.
    • Example: In your app, if a user decides to edit the details of a wish, the UPDATE operation is performed where the corresponding row in the database is altered to reflect these changes.
  4. Delete: This operation removes data from the database. When an item is no longer needed or relevant, it can be deleted, which removes its corresponding row from the database table. In SQL, this is done using the DELETE statement.
    • Example: If a user decides to remove a wish from their list, the DELETE operation is triggered, and the corresponding row for that wish is removed from the database.

Importance in Android & Kotlin Development

In Android development, especially when using the Room Persistence Library, these CRUD operations are abstracted through DAOs (Data Access Objects). Developers define methods in DAOs that perform these operations, and Room generates the necessary SQL code.

This abstraction allows for more readable, maintainable, and testable code, as well as providing additional benefits like compile-time verification of SQL queries.

Understanding and efficiently implementing CRUD operations are crucial for managing the data in Android apps, as they form the backbone of most data-driven applications.

SQLite with Room: Entity and DAO in Detail

In Android development, particularly when using the Room Persistence Library, two key components come into play when interfacing with SQLite databases: Entity classes and Data Access Objects (DAOs).

Here’s a detailed look at each:

  1. Entity Class (@Entity):
    • Purpose: An Entity class in Room represents a table within the SQLite database. Each instance of an entity corresponds to a row in the table.
    • Annotation: The @Entity annotation is used to mark a class as an entity. This tells Room to create a table for this object.
    • Structure:
      • Primary Key: Each entity must define at least one primary key. This is done using the @PrimaryKey annotation. For instance, @PrimaryKey(autoGenerate = true) can be used if you want the database to auto-generate the key.
      • Columns: The fields in the entity represent the columns of the table. You can customize the column name using the @ColumnInfo(name = "custom_name") annotation. If not specified, the column name will be the same as the field name.
    • Example:In this example, Wish is an entity with a table name wish_table, containing two columns: id and wish_desc.
      @Entity(tableName = "wish_table")
      data class Wish(
          @PrimaryKey(autoGenerate = true) val id: Int,
          @ColumnInfo(name = "wish_desc") val description: String
      )
      
  2. Data Access Object (DAO):
    • Purpose: DAOs are interfaces or abstract classes where you define methods to access your database. In other words, DAOs are the main component for defining the logic of your database operations.
    • Annotation: The @Dao annotation is used to mark a class or interface as a DAO.
    • Methods: Within a DAO, you define methods to perform your database operations (CRUD operations). Room translates these methods into SQLite queries.
      • Insert: Annotated with @Insert, used for adding records to the database.
      • Query: Annotated with @Query, used for reading data. You can write SQLite queries inside the annotation.
      • Update: Annotated with @Update, used to modify existing records.
      • Delete: Annotated with @Delete, used to remove records.
    • Example:This DAO defines methods for adding, retrieving, updating, and deleting wishes in the wish_table.
      @Dao
      interface WishDao {
          @Insert
          fun addWish(wish: Wish)
      
          @Query("SELECT * FROM wish_table")
          fun getAllWishes(): List<Wish>
      
          @Update
          fun updateWish(wish: Wish)
      
          @Delete
          fun deleteWish(wish: Wish)
      }
      

Integration in Android App

In an Android application using Room, these components are integrated as follows:

  • Entities define the structure of your database tables.
  • DAOs provide the methods to interact with these tables.
  • Database Instance: Room uses these components to create a database instance which your app can use to perform actual data operations.

This combination of Entity classes and DAOs provides a robust and efficient way to interact with SQLite databases, ensuring type safety and reducing boilerplate code.

Setting Up RoomDatabase

  • RoomDatabase Setup:
    @Database(entities = [Wish::class], version = 1)
    abstract class AppDatabase : RoomDatabase() {
        abstract fun wishDao(): WishDao
    }
    

 

2. Abstract Classes in Kotlin

In Kotlin, an abstract class is a type of class that cannot be instantiated directly, meaning you cannot create an object of an abstract class.

Instead, it is meant to be a base class that other classes extend from. The key characteristics of an abstract class in Kotlin are:

  1. Abstract Methods: An abstract class can have abstract methods. These methods are declared without an implementation. The implementation of these methods must be provided by the subclasses that inherit the abstract class. This is useful for defining a common template for a group of subclasses.
  2. Non-Abstract Methods: Besides abstract methods, an abstract class can also have regular methods with implementations. This allows you to define shared behavior that subclasses can inherit or override.
  3. Instantiation: As mentioned, abstract classes cannot be instantiated on their own. They are designed to be subclassed, and their functionality is realized in their subclasses.
  4. Constructor: Like any other class, abstract classes can have constructors. These constructors are called when a subclass is instantiated.

In the context of using Room with Kotlin for Android development, the abstract class plays a crucial role, particularly when defining DAOs (Data Access Objects).

Here’s how:

  • DAO as an Abstract Class: When you define a DAO in Room, you typically declare it as an abstract class. This is because the DAO is not meant to be instantiated directly. Instead, Room takes care of creating an implementation for you.
  • Defining Database Operations: In your DAO, you define abstract methods for each database operation you want to perform (like adding, fetching, updating, or deleting data). Room generates the necessary code to perform these operations with SQLite.
  • Example:
@Dao
abstract class WishDao {
    @Insert
    abstract fun addWish(wish: Wish)

    @Query("SELECT * FROM wish_table")
    abstract fun getAllWishes(): List<Wish>

    // Other CRUD operations...
}
  • In this example, WishDao is an abstract class with abstract methods defining operations on a wish_table. Each method represents a specific database operation, and Room generates the appropriate SQL queries based on these abstract method declarations.

Using abstract classes for DAOs in Room provides a clean and concise way to define how your application interacts with the database, leveraging Kotlin’s type safety and Room’s ability to generate boilerplate code efficiently. This pattern is particularly powerful in larger applications where managing database operations and ensuring consistency can become complex.

 

3. Using Repositories and Kotlin Coroutines

In Android development with Kotlin, especially when using Room for database operations, repositories and coroutines play a crucial role in managing data operations and ensuring a smooth, responsive user interface. Let’s delve into these concepts:

  1. Repositories:
    • Role: A repository acts as a mediator between different data sources (like Room database, network, etc.) and your application’s business logic. It abstracts the source of the data from the rest of your app, providing a clean API for data access to the rest of your application.
    • Benefits: By using a repository, you can keep your data operations and your business logic separate from your UI code. This separation makes your code more modular, easier to test, and flexible in terms of changing data sources.
  2. Kotlin Coroutines:
    • Purpose: Coroutines are a Kotlin feature that simplifies asynchronous programming by allowing you to write code in a sequential manner. They are particularly useful for managing long-running tasks that could potentially block the main thread, such as database operations or network calls.
    • Dispatchers in Coroutines: Dispatchers.IO is a coroutine dispatcher that optimizes the execution of IO-intensive tasks, like reading from or writing to a database, reading files, and network operations. By using this dispatcher, such tasks are offloaded to a shared pool of threads, ensuring that the main thread, responsible for handling UI updates, remains responsive.
  3. Flow<> in Room with Kotlin:
  • What is Flow?: Flow is a type in the Kotlin Coroutines library (kotlinx.coroutines.flow) used for representing streams of data that can emit multiple values over time. Unlike a single-shot call (like a suspending function), a can emit multiple values sequentially.
  • Use in Room: When used with Room, Flow can be particularly powerful for observing changes in the database. For instance, you can return a from a DAO method, and Room will ensure that every time the data in the table changes, the Flow emits these updates automatically.
  • Example:
@Query("SELECT * FROM wish_table")
fun getAllWishes(): Flow<List<Wish>>

In this example, the method getAllWishes() returns a Flow of a list of Wish objects. Whenever the data in wish_table changes, this will emit the updated list of wishes, allowing your UI to reactively update to reflect these changes.

  1. lateinit var:
    • Purpose: The lateinit keyword in Kotlin is used to declare non-null type properties without initializing them at the point of declaration. It’s particularly useful in cases where you need to initialize a property via dependency injection, or for cases where the property needs the context which isn’t available at the time of object creation.
    • Usage: lateinit can only be used with mutable properties (var) and non-nullable types. It’s a promise that you will initialize the variable before using it, otherwise, it will throw an UninitializedPropertyAccessException.

By combining these elements—repositories for data abstraction, coroutines for asynchronous programming, and Flow for reactive data streams—you can create robust, scalable, and responsive Android applications. This approach enables you to handle data operations efficiently, maintain a responsive UI, and observe and react to data changes in real-time.

 

4. Object Graph and Dependency Injection

The concepts of an Object Graph and Dependency Injection are crucial for managing dependencies and achieving a modular, testable, and maintainable codebase. Let’s explore these concepts in detail:

  1. Object Graph:
    • Definition: An Object Graph in programming refers to a graph of objects that are interconnected and dependent on each other. In the context of Android development, it represents the entire set of dependencies that your application might need.
    • Role in Applications: The object graph is responsible for creating and providing instances of objects that are required elsewhere in your application. For example, if you have a repository that requires a database instance, the object graph would be responsible for creating and providing that database instance to the repository.
    • Management: Typically, in large applications, managing the object graph can become complex. Frameworks like Dagger or Hilt are often used in Android development to automate and simplify this process.
  2. Dependency Injection (DI):
    • Concept: Dependency Injection is a design pattern in which an object receives its dependencies from an external source rather than creating them itself. This external source is often an object graph.
    • Purpose: The main idea behind DI is to decouple the creation of an object from its usage. This makes your code more modular, easier to test, and flexible.
    • Types of DI:
      • Constructor Injection: Dependencies are provided through the constructor. This is the most common and preferred method of DI.
      • Field Injection: Dependencies are injected directly into the fields of a class.
      • Method Injection: Dependencies are provided through a method.
  3. Use in Android with Kotlin:
    • In Android development, DI is used to provide dependencies like database instances, network clients, or custom utilities to activities, fragments, or services.
    • For instance, if an activity requires a ViewModel, the ViewModel can be provided to the activity via DI, rather than the activity creating it directly. This decouples the activity from the creation logic of the ViewModel, making the code more testable and modular.
    • Example with Hilt:
@AndroidEntryPoint
class MyActivity : AppCompatActivity() {
    @Inject lateinit var viewModel: MyViewModel
    // ...
}

In this example, Hilt is used to inject the MyViewModel into MyActivity. Hilt takes care of creating and provides it to when needed.

  1. Benefits:
    • Testing: Easier to unit test as dependencies can be mocked or replaced.
    • Reusability and Maintenance: Promotes reusability and maintenance of code since dependencies are centralized.
    • Decoupling: Reduces coupling between components of an application, leading to a more flexible and scalable codebase.

In summary, understanding and implementing Object Graph and Dependency Injection in Android with Kotlin can significantly improve the design and maintainability of an application. It leads to cleaner, more testable code and allows for greater flexibility and scalability in app development.

 

5. Singleton Pattern in Detail

The Singleton pattern is a design pattern that restricts the instantiation of a class to one single instance. This is useful when exactly one object is needed to coordinate actions across the system.

The Singleton pattern is one of the simplest design patterns in software engineering, yet it plays a significant role in many applications. Here’s a detailed look at its key aspects:

  1. Basic Concept:
    • Single Instance: Ensures that a class has only one instance and provides a global point of access to it.
    • Controlled Access: Because the Singleton class encapsulates its sole instance, it can have strict control over how and when clients access it.
  2. Implementation in Kotlin:
  • Object Keyword: Kotlin provides a simple way to create singletons using the object keyword. The declaration of an object creates a class and its single instance at the same time.
  • Example:
object DatabaseHelper {
    init {
        // Initialization code here
    }

    fun getInstance(): DatabaseHelper {
        return this
    }
}

In this example, DatabaseHelper is a singleton. The object keyword ensures that only one instance of DatabaseHelper exists.

  1. Characteristics:
  • Lazy Instantiation: The instance of the Singleton class is typically created when it is first needed. This is known as lazy instantiation.
  • Thread Safety: In a multi-threaded application, care must be taken to avoid simultaneous creation of multiple instances. In Kotlin, the object declaration handles thread safety by default.
  • Global State: Singletons are often used for managing a shared resource, such as a database connection or a configuration.
  1. Use Cases:
    • Shared Resources: Managing a shared resource like a database connection pool.
    • Configuration Objects: Holding application configuration data that is accessed from multiple places.
    • Logging: Managing logging to a single file.
  2. Pros and Cons:
    • Pros:
      • Ensures controlled access to the sole instance.
      • Reduces namespace pollution by avoiding global variables.
      • Can be lazily loaded, improving performance and resource usage.
    • Cons:
      • Global instances can be problematic for unit testing.
      • Sometimes considered an anti-pattern if overused or used inappropriately, as it can introduce global state into an application.
  3. Alternatives and Considerations:
    • In some situations, using dependency injection frameworks (like Dagger or Hilt in Android) can be a more flexible alternative to singletons. These frameworks manage object lifecycles and dependencies more elegantly and are more conducive to testing.

In summary, the Singleton pattern is a widely used design pattern in software development. It’s particularly useful when you need to ensure that a class has only one instance and provide a global point of access to that instance. However, its use should be considered carefully, especially in the context of testability and maintaining a clean architecture.

6. Application Class and User Interface Enhancements

In Android development, enhancing the user interface and managing application-wide features often involves using a custom Application class and implementing various UI components and states.

Here’s a detailed look at these aspects:

  1. Application Class & Using Graph:
  • Application Class: In Android, the Application class is a base class that contains global application state. You can extend this class and implement custom application-wide features or initializations.
  • Using Object Graph for Dependency Injection: The custom Application class is an ideal place to initialize the object graph for dependency injection (using frameworks like Dagger or Hilt). This ensures that dependencies are ready to be injected whenever an activity, fragment, or service is created.
  • Example:
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        // Initialize Dependency Injection
        // Initialize other global state
    }
}
  1. SnackMessage & Snackbars:
  • Purpose: Snackbars provide lightweight feedback about an operation by showing a brief message at the bottom of the screen. They can include a user action, like an undo option.
  • Implementation: Snackbars are implemented using the Snackbar class from the Material Design library.
  • Example:
Snackbar.make(view, "Item Deleted", Snackbar.LENGTH_LONG).setAction("Undo") {
    // Handle Undo action
}.show()

In this example, a Snackbar is shown with a message and an undo action.

  1. Swipe to Delete Feature:
  • Purpose: Swipe gestures can be used to add interactive elements to lists or grids, like swiping to delete an item.
  • Implementation: This can be implemented using ItemTouchHelper attached to a RecyclerView.
  • Example:
val itemTouchHelperCallback = object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
        // Handle swipe action
    }
}
val itemTouchHelper = ItemTouchHelper(itemTouchHelperCallback)
itemTouchHelper.attachToRecyclerView(recyclerView)
  1. DismissState and AnimatedColorState:
  • DismissState: This is a state typically used in conjunction with swipe gestures to manage the state of an item being dismissed.
  • AnimatedColorState: AnimatedColorState is useful for animating color changes in response to state changes in the UI, enhancing visual feedback.
  • Implementation: These states are often used in modern Android UI development, especially with Jetpack Compose, to create responsive and visually appealing interfaces.

By combining these elements, you can greatly enhance the user experience of your Android application. The custom Application class ensures a robust foundation for managing global states and dependencies. Meanwhile, UI components like Snackbars and swipe gestures, along with responsive states, provide a dynamic and interactive user interface.

 

Conclusion: Storing Data Permanently Part 2/2 – Day 14 Android 14 Masterclass

Concluding Day 14 of our masterclass, we’ve enriched our understanding of “Storing Data Permanently Part 2/2” in Android 14 & Kotlin development. We’ve covered the core of CRUD operations in RoomDatabase, the implementation of Entity and DAO, and the use of Kotlin’s abstract classes. Additionally, we delved into repositories, Kotlin Coroutines, Dependency Injection, Singleton patterns, and UI enhancements. These concepts are crucial for developing efficient, maintainable, and high-quality Android applications, setting a solid foundation for future development endeavors.

 

If you want to skyrocket your Android career, check out our The Complete Android 14 & Kotlin Development Masterclass. Learn Android 14 App Development From Beginner to Advanced Developer.

Master Jetpack Compose to harness the cutting-edge of Android development, gain proficiency in XML — an essential skill for numerous development roles, and acquire practical expertise by constructing real-world applications.

 

Check out Day 12 of this course here.

Check out Day 13 of this course here.

Check out Day 15 & Day 16 of this course here.