Android Kotlin: Room RecyclerView LiveData example

Introduction

In modern Android app development, building efficient and responsive user interfaces that can handle large datasets is a crucial aspect. Combining components like RecyclerView for displaying data and Room for database management is a common practice, especially when you want to ensure smooth data flow and user experience. This example demonstrates how to use Room, RecyclerView, and LiveData in Kotlin to create an application where user inputs can be dynamically stored in a local database and displayed in real-time.

This tutorial will walk through each part of the code in detail, from setting up the data model and database to integrating a view model and RecyclerView for efficient data handling. By the end of the description, you'll have a clear understanding of how these components work together to create a seamless Android application.

MainActivity.kt

In the MainActivity.kt, the app's primary logic is implemented. First, a StudentViewModel is retrieved using ViewModelProviders, which provides access to data (in this case, a list of students) that persists across activity restarts. The activity also initializes the RecyclerView, setting a LinearLayoutManager for vertical scrolling, which is essential for displaying data in a list format.

The key functionality is observing changes in the allStudents LiveData from the StudentViewModel. Whenever the data changes, the RecyclerView is updated by setting a new RecyclerViewAdapter. This ensures that the UI reflects any updates in the data, such as when new students are added. The button click listener inserts a new Student into the database asynchronously using doAsync to keep the main thread free from heavy operations.

RoomSingleton.kt

The RoomSingleton class defines the Room database configuration for the app. It ensures a single instance of the database exists during the application's lifecycle using the singleton pattern. The @Database annotation specifies that the database has one table, defined by the Student entity, and its version is set to 1. The companion object includes the getInstance() method, which creates the database if it doesn't exist and returns the instance, providing centralized access to the Room database.

By using Room.databaseBuilder, the app creates the database named "roomdb". This class is crucial for managing database connections efficiently and providing access to the studentDao() interface.

RoomEntity.kt

The Student data class defines the table schema for the studentTbl table in the Room database. Each instance of Student represents a row in the table. It has two properties: id, which serves as the primary key and is automatically generated, and name, which stores the student's UUID.

Annotations like @PrimaryKey and @ColumnInfo specify how the table is structured in the database. This mapping between Kotlin classes and database tables makes it easy to work with SQL-based databases without manually writing SQL queries.

RoomDao.kt

The StudentDao interface is the Data Access Object (DAO) for interacting with the studentTbl in the Room database. The @Dao annotation marks this interface as a Room DAO. It defines two methods: allStudents(), which retrieves all students from the table in descending order by id, and insert(), which inserts a new student into the table.

The allStudents() method returns a LiveData<List<Student>>, allowing the app to observe changes in the table and update the UI automatically without needing explicit callbacks. The insert() method is annotated with OnConflictStrategy.REPLACE, which ensures that if a student with the same id already exists, it will be replaced with the new one.

StudentViewModel.kt

The StudentViewModel class extends AndroidViewModel, providing a lifecycle-conscious component that holds the app’s data. It retrieves the list of students from the Room database and exposes it as LiveData through the allStudents property. This allows the UI to automatically observe the data and react to changes without additional configuration.

Additionally, the insert() method in the ViewModel calls the DAO's insert() method, providing a simple way for the MainActivity to add new data to the database. By managing the data in a ViewModel, the app separates UI logic from data handling, which promotes a clean architecture.

RecyclerViewAdapter.kt

The RecyclerViewAdapter class manages the display of data in the RecyclerView. It extends RecyclerView.Adapter and binds the student data to each item in the list. When the onBindViewHolder() method is called, the student’s id and name are assigned to the respective TextView elements in the custom layout (custom_view.xml).

The getItemCount() method ensures the RecyclerView knows how many items to display, which is derived from the size of the student list. This adapter allows dynamic content to be displayed in a scrollable list format, making it an essential part of the UI for this app.

XML Layouts

The activity_main.xml defines the layout for the main screen, including a button to insert data and a RecyclerView to display the list of students. The RecyclerView is constrained to take up most of the screen's height, positioned below the button.

The custom_view.xml defines the layout for individual items in the RecyclerView. It uses MaterialCardView to give each item a card-like appearance, and two TextView elements display the student's ID and name. The layout ensures that each student entry appears neatly within the list.

Summary

This example demonstrates how to build an Android app that uses Room, RecyclerView, and LiveData to create a responsive, data-driven interface. The app efficiently stores and retrieves student data using a Room database and updates the UI in real-time using LiveData. By leveraging the ViewModel architecture, the app separates UI logic from data handling, promoting a cleaner and more maintainable codebase.

The combination of Room, RecyclerView, and LiveData is a powerful pattern in Android development that allows for smooth data handling, responsive UIs, and scalable applications. Understanding how these components interact will help developers build modern Android apps that are robust and efficient.


MainActivity.kt

package com.cfsuman.jetpack

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.activity_main.*
import org.jetbrains.anko.doAsync
import java.util.*


class MainActivity : AppCompatActivity() {

    private lateinit var model: StudentViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Get the view model
        model = ViewModelProviders.of(this).get(StudentViewModel::class.java)

        // Specify layout for recycler view
        val linearLayoutManager = LinearLayoutManager(
            this, RecyclerView.VERTICAL,false)
        recyclerView.layoutManager = linearLayoutManager

        // Observe the model
        model.allStudents.observe(this, Observer{ students->
            // Data bind the recycler view
            recyclerView.adapter = RecyclerViewAdapter(students)
        })

        // Insert data into table
        btn.setOnClickListener {
            doAsync {
                model.insert(Student(null, UUID.randomUUID().toString()))
            }
        }
    }
}
RoomSingleton.kt

package com.cfsuman.jetpack

import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import android.content.Context


@Database(entities = arrayOf(Student::class), version = 1, exportSchema = false)
abstract class RoomSingleton : RoomDatabase(){
    abstract fun studentDao():StudentDao

    companion object{
        private var INSTANCE: RoomSingleton? = null
        fun getInstance(context:Context): RoomSingleton{
            if (INSTANCE == null){
                INSTANCE = Room.databaseBuilder(
                    context,
                    RoomSingleton::class.java,
                    "roomdb")
                    .build()
            }

            return INSTANCE as RoomSingleton
        }
    }
}
RoomEntity.kt

package com.cfsuman.jetpack

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

@Entity(tableName = "studentTbl")
data class Student(
    @PrimaryKey
    var id:Long?,

    @ColumnInfo(name = "uuid")
    var name: String
)
RoomDao.kt

package com.cfsuman.jetpack

import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query

@Dao
interface StudentDao{
    @Query("SELECT * FROM studentTbl ORDER BY id DESC")
    fun allStudents(): LiveData<List<Student>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(student: Student)
}
StudentViewModel.kt

package com.cfsuman.jetpack

import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import android.app.Application


class StudentViewModel(application:Application): AndroidViewModel(application){
    private val db:RoomSingleton = RoomSingleton.getInstance(application)
    internal val allStudents : LiveData<List<Student>> = db.studentDao().allStudents()

    fun insert(student:Student){
        db.studentDao().insert(student)
    }
}
RecyclerViewAdapter.kt

package com.cfsuman.jetpack

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.custom_view.view.*

class RecyclerViewAdapter(val students: List<Student>)
    : RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>(){

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int)
            : RecyclerViewAdapter.ViewHolder {
        val v: View = LayoutInflater.from(parent.context)
            .inflate(R.layout.custom_view,parent,false)
        return ViewHolder(v)
    }

    override fun onBindViewHolder(holder: RecyclerViewAdapter.ViewHolder, position: Int) {
        holder.id.text = students[position].id.toString()
        holder.name.text = students[position].name
    }

    override fun getItemCount(): Int {
        return students.size
    }

    override fun getItemId(position: Int): Long {
        return super.getItemId(position)
    }

    override fun getItemViewType(position: Int): Int {
        return super.getItemViewType(position)
    }

    class ViewHolder(itemView:View): RecyclerView.ViewHolder(itemView){
        val id = itemView.tvId
        val name = itemView.tvName
    }
}
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/rootLayout"
        tools:context=".MainActivity"
        android:background="#fdfdfc">
    <Button
            android:text="Insert Data"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/btn"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_marginStart="8dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp"/>
    <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="8dp"
            app:layout_constraintHorizontal_bias="0.0"
            android:layout_marginBottom="8dp"
            app:layout_constraintBottom_toBottomOf="parent"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/btn"/>
</androidx.constraintlayout.widget.ConstraintLayout>
custom_view.xml

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:contentPadding="10dp"
    android:layout_margin="3dp"
    app:cardElevation="2dp"
    app:cardMaxElevation="3dp">
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:id="@+id/tvId"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            tools:text="100"
            android:layout_margin="10dp"
            app:layout_constraintBottom_toBottomOf="parent"
            android:layout_marginBottom="8dp"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="8dp"/>
        <TextView
            android:id="@+id/tvName"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            tools:text="dsfdfdsfdfdf"
            android:layout_marginTop="4dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp"
            app:layout_constraintStart_toEndOf="@+id/tvId"
            android:layout_marginStart="8dp"
            app:layout_constraintHorizontal_bias="0.0"
            android:layout_margin="10dp"
        />
    </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
gradle settings

apply plugin: 'kotlin-kapt'

dependencies {
    // Material components theme
    implementation 'com.google.android.material:material:1.0.0'

    // ViewModel and LiveData
    def lifecycle_version = "2.0.0"
    implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"

    // Room support
    def room_version = "2.1.0-alpha04"
    implementation "androidx.room:room-runtime:$room_version"
    kapt 'androidx.room:room-compiler:2.1.0-alpha04'

    // Anko Commons
    implementation "org.jetbrains.anko:anko-commons:0.10.5"

    // RecyclerView
    implementation 'androidx.recyclerview:recyclerview:1.0.0'
}

Android Kotlin: Room LiveData ViewModel example

Introduction

In Android development, integrating database operations with the user interface can be challenging, especially when aiming for clean and efficient code. Kotlin's support for Android Jetpack components, such as Room, ViewModel, and LiveData, provides an excellent solution for handling database-related tasks in a lifecycle-aware manner. This article demonstrates a practical example of how to use these components together in an Android application. Specifically, it shows how to implement a simple app where users can insert data into a Room database, and observe changes using LiveData, all managed by a ViewModel.

The example covers the essential aspects of using Room for local storage, LiveData for observing data changes, and ViewModel for preserving UI-related data during configuration changes. The code includes database creation, a DAO interface for queries, and a ViewModel to manage UI data.

MainActivity.kt Overview

The MainActivity serves as the entry point of the app. It is responsible for setting up the UI and interacting with the StudentViewModel. Upon launching, the activity uses ViewModelProviders to retrieve an instance of the StudentViewModel. By doing this, the app ensures that the ViewModel survives configuration changes like screen rotations, allowing the UI to remain responsive and efficient.

Inside the onCreate method, the TextView is made scrollable using the ScrollingMovementMethod, and the app observes changes to the list of students stored in the Room database. Whenever the data in the database changes, the LiveData mechanism triggers an update to the TextView, displaying the latest list of students. The Button in the layout allows users to insert new students into the database, where each student is assigned a unique UUID.

RoomSingleton.kt and Room Database

The RoomSingleton class sets up the Room database, ensuring that there is only a single instance of the database throughout the app's lifecycle. This singleton pattern is implemented to prevent multiple instances of the database being created, which could lead to issues like data inconsistency or unnecessary memory usage.

The Room.databaseBuilder method is used to build the Room database, with StudentDao providing the interface for interacting with the studentTbl table. By abstracting database operations through a DAO, the app can perform tasks like data insertion and retrieval without tightly coupling the UI components with the database logic, thus adhering to best practices in software design.

RoomEntity.kt and RoomDao.kt

In the RoomEntity.kt file, a data class named Student is defined. This class represents the structure of the table in the Room database. Each Student object contains an id (a primary key) and a name, which in this example stores a UUID string. The @Entity annotation is used to mark the class as a table definition.

The RoomDao.kt file contains the StudentDao interface, which defines the database operations related to the studentTbl. The allStudents() method is a query that returns a LiveData object containing a list of students. This makes it possible for the UI to observe changes in the database in real-time. The insert() method inserts new student records, and its OnConflictStrategy.REPLACE ensures that in case of conflicts (like inserting a student with the same ID), the new entry will replace the old one.

StudentViewModel.kt

The StudentViewModel.kt file encapsulates the database interaction logic, providing a clean API for the MainActivity to interact with the Room database. The ViewModel holds a reference to the Room database and exposes a LiveData<List<Student>> object that can be observed by the UI.

The ViewModel class fetches the data from the database and provides methods to insert new students. By keeping this logic in the ViewModel, the app maintains a clear separation of concerns, as the UI (activity) doesn't directly interact with the database. This also ensures that the data survives configuration changes, such as screen rotations, which would otherwise destroy the activity and recreate it with a new instance.

Layout and Gradle Dependencies

The layout file activity_main.xml defines a simple UI with a Button and a TextView. The button allows the user to insert new data into the database, while the text view displays the list of students retrieved from the database. The layout ensures that the user can scroll through the list of students, even as more data is added.

In the build.gradle file, essential dependencies for ViewModel, LiveData, and Room are included. Additionally, the project uses the Anko Commons library to simplify background threading operations, such as inserting data into the database in a non-blocking manner.

Summary

This example demonstrates how to implement a simple Android app using Room, LiveData, and ViewModel to manage and display data from a local database. By separating concerns and leveraging Jetpack components, the app becomes more maintainable, responsive, and resilient to configuration changes. The use of LiveData allows the UI to automatically update when the database changes, while the ViewModel ensures that the data is retained even if the activity is destroyed and recreated.

The combination of these components results in a clean and efficient architecture, suitable for building robust Android applications that require local storage and real-time data updates.


MainActivity.kt

package com.cfsuman.jetpack

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.text.method.ScrollingMovementMethod
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import kotlinx.android.synthetic.main.activity_main.*
import org.jetbrains.anko.doAsync
import java.util.*


class MainActivity : AppCompatActivity() {

    private lateinit var model: StudentViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Make text view content scrollable
        textView.movementMethod = ScrollingMovementMethod()

        // Get the view model
        model = ViewModelProviders.of(this).get(StudentViewModel::class.java)

        // Observe the model
        model.allStudents.observe(this, Observer{ students->
            textView.text = ""
            for (student in students){
                textView.append("${student.id} | ${student.name}\n")
            }
        })


        // Insert data into table
        btn.setOnClickListener {
            doAsync {
                model.insert(Student(null, UUID.randomUUID().toString()))
               }
        }
    }
}
RoomSingleton.kt

package com.cfsuman.jetpack

import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import android.content.Context


@Database(entities = arrayOf(Student::class), version = 1, exportSchema = false)
abstract class RoomSingleton : RoomDatabase(){
    abstract fun studentDao():StudentDao

    companion object{
        private var INSTANCE: RoomSingleton? = null
        fun getInstance(context:Context): RoomSingleton{
            if (INSTANCE == null){
                INSTANCE = Room.databaseBuilder(
                    context,
                    RoomSingleton::class.java,
                    "roomdb")
                    .build()
            }

            return INSTANCE as RoomSingleton
        }
    }
}
RoomEntity.kt

package com.cfsuman.jetpack

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

@Entity(tableName = "studentTbl")
data class Student(
    @PrimaryKey
    var id:Long?,

    @ColumnInfo(name = "uuid")
    var name: String
)
RoomDao.kt

package com.cfsuman.jetpack

import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query

@Dao
interface StudentDao{
    @Query("SELECT * FROM studentTbl")
    fun allStudents(): LiveData<List<Student>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(student: Student)
}
StudentViewModel.kt

package com.cfsuman.jetpack

import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import android.app.Application


class StudentViewModel(application:Application): AndroidViewModel(application){
    private val db:RoomSingleton = RoomSingleton.getInstance(application)
    internal val allStudents : LiveData<List<Student>> = db.studentDao().allStudents()

    fun insert(student:Student){
        db.studentDao().insert(student)
    }
}
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/rootLayout"
        tools:context=".MainActivity"
        android:background="#fdfdfc">
    <Button
            android:text="Insert Data"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/btn"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_marginStart="8dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp"/>
    <TextView
            android:text=""
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/textView"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="16dp"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            app:layout_constraintTop_toBottomOf="@+id/btn"
            android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium"
            android:textColor="#3F51B5"
            tools:text="TextView"
            android:padding="16dp"
    />
</androidx.constraintlayout.widget.ConstraintLayout>
gradle settings

apply plugin: 'kotlin-kapt'

dependencies {
    // ViewModel and LiveData
    def lifecycle_version = "2.0.0"
    implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"

    // Room support
    def room_version = "2.1.0-alpha04"
    implementation "androidx.room:room-runtime:$room_version"
    kapt 'androidx.room:room-compiler:2.1.0-alpha04'

    // Anko Commons
    implementation "org.jetbrains.anko:anko-commons:0.10.5"
}

Android Kotlin: Room singleton example

Introduction

In Android development, managing a local SQLite database can be challenging without a proper framework. Fortunately, the Room persistence library simplifies this process by providing an abstraction layer over SQLite, helping developers create robust and maintainable database systems. In Kotlin, combining Room with the Singleton design pattern offers a clean and efficient way to manage a single instance of the database throughout the app's lifecycle. This example demonstrates how to use a Singleton with Room to perform basic database operations, such as inserting and fetching data from a database table.

In this tutorial, we walk through an Android Kotlin example that sets up a Room database using the Singleton pattern. The goal is to ensure only one instance of the database is used, which is crucial for avoiding resource leakage and ensuring thread safety. We'll also explain how the app interacts with this database using Data Access Objects (DAOs), Entities, and asynchronous threading with Kotlin coroutines or libraries like Anko.

MainActivity.kt

The MainActivity.kt file serves as the entry point for the application. It begins by setting up the user interface in the onCreate method, where the layout defined in activity_main.xml is loaded. The layout consists of a button (btn) for inserting data and a scrollable TextView (textView) for displaying the inserted records. One of the key components of this file is initializing the Room database.

The Room database instance is created via the RoomSingleton.getInstance(applicationContext) call, ensuring that only one instance of the database is used throughout the application's lifecycle. A button click listener is set up to trigger the database operation. Upon clicking the button, data insertion and retrieval occur asynchronously using Anko's doAsync function, which handles background tasks. The inserted data is retrieved and displayed in the TextView after the operation completes on the UI thread using uiThread.

RoomSingleton.kt

The RoomSingleton.kt file implements the Singleton pattern for the Room database. This design pattern is essential in scenarios where multiple parts of an application require access to the database, but only one instance should be created to prevent resource overuse and conflicts. The RoomSingleton class is annotated with @Database, which defines the entities and the version of the database.

The Singleton is implemented using the getInstance method, which checks if an instance of the database already exists. If not, it initializes a new instance using Room.databaseBuilder and assigns it to the INSTANCE variable. This ensures that all calls to the database use the same instance, thus providing consistency and efficient resource management across the application.

RoomEntity.kt

The RoomEntity.kt file defines the Student data class, which is the entity that maps to the database table studentTbl. Each entity in Room represents a table within the database. In this case, the Student entity has two fields: id, which serves as the primary key, and name, which holds a unique identifier (UUID) generated when inserting a new student.

Annotations such as @Entity and @PrimaryKey specify how this class corresponds to the table schema in the SQLite database. Room automatically maps this class to the underlying database table and handles the creation of SQL statements for CRUD operations.

RoomDao.kt

The RoomDao.kt file defines the DAO (Data Access Object) for interacting with the studentTbl. The StudentDao interface contains methods for querying and inserting data into the database. The @Dao annotation marks this interface as a DAO component for Room.

The allStudents() method uses an SQL query to select all records from the studentTbl, returning them as a list of Student objects. The insert() method inserts a new Student object into the database, replacing any conflicting entries with the same primary key (in this case, the id field). Room generates the required SQL code to perform these operations behind the scenes, allowing the developer to interact with the database in a more abstracted and Kotlin-friendly way.

activity_main.xml

The activity_main.xml layout file defines the user interface for the main activity. It contains a button labeled "Insert Data" and a TextView to display the inserted data. The TextView is configured to be scrollable, allowing it to display multiple lines of text without overflowing off the screen.

ConstraintLayout is used to arrange the Button and TextView within the screen. The button is positioned at the top of the layout, while the TextView is placed below it to display the records after the button is clicked.

Gradle Settings

The build.gradle file contains essential dependencies required for this project. It includes the Room library for database management and Anko for simplified asynchronous threading. The Anko library, though deprecated in later versions, was a popular choice for Kotlin developers due to its concise syntax for handling background tasks and UI updates.

In this example, the Room version used is 2.1.0-alpha04, and the kapt plugin is applied for Room annotation processing. The kapt tool generates the necessary code for Room during compilation.

Summary

In summary, this Android Kotlin example demonstrates how to use the Room persistence library with the Singleton pattern to efficiently manage a local SQLite database. The application allows the user to insert random student data into a database and display it in a scrollable TextView. By using Room, developers can work with databases in a more structured and maintainable way, and the Singleton pattern ensures that only one instance of the database is created.

This approach not only improves application performance and resource management but also promotes a clean architecture. With tools like Anko or Kotlin coroutines, developers can manage asynchronous tasks with ease, ensuring the user interface remains responsive while database operations are performed in the background.


MainActivity.kt

package com.cfsuman.jetpack

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.text.method.ScrollingMovementMethod
import kotlinx.android.synthetic.main.activity_main.*
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import java.util.*


class MainActivity : AppCompatActivity() {

    private lateinit var mDb: RoomSingleton

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Initialize the room database
        mDb = RoomSingleton.getInstance(applicationContext)

        // Make text view content scrollable
        textView.movementMethod = ScrollingMovementMethod()

        // Insert data into table
        btn.setOnClickListener {
            textView.text = ""

            doAsync {
                mDb.studentDao().insert(Student(null,UUID.randomUUID().toString()))
                val list = mDb.studentDao().allStudents()

                uiThread {
                    // Show the records in text view
                    for (Student in list){
                        textView.append("${Student.id} : ${Student.name} \n")
                    }
                }
            }
        }
    }
}
RoomSingleton.kt

package com.cfsuman.jetpack

import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import android.content.Context


@Database(entities = arrayOf(Student::class), version = 1, exportSchema = false)
abstract class RoomSingleton : RoomDatabase(){
    abstract fun studentDao():StudentDao

    companion object{
        private var INSTANCE: RoomSingleton? = null
        fun getInstance(context:Context): RoomSingleton{
            if (INSTANCE == null){
                INSTANCE = Room.databaseBuilder(
                    context,
                    RoomSingleton::class.java,
                    "roomdb")
                    .build()
            }

            return INSTANCE as RoomSingleton
        }
    }
}
RoomEntity.kt

package com.cfsuman.jetpack

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

@Entity(tableName = "studentTbl")
data class Student(
    @PrimaryKey
    var id:Long?,

    @ColumnInfo(name = "uuid")
    var name: String
)
RoomDao.kt

package com.cfsuman.jetpack

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query

@Dao
interface StudentDao{
    @Query("SELECT * FROM studentTbl")
    fun allStudents():List<Student>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(student: Student)
}
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/rootLayout"
        tools:context=".MainActivity"
        android:background="#fdfdfc">
    <Button
            android:text="Insert Data"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/btn"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_marginStart="8dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp"/>
    <TextView
            android:text=""
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/textView"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="16dp"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            app:layout_constraintTop_toBottomOf="@+id/btn"
            android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium"
            android:textColor="#3F51B5"
            tools:text="TextView"
            android:padding="16dp"
    />
</androidx.constraintlayout.widget.ConstraintLayout>
gradle settings

apply plugin: 'kotlin-kapt'

dependencies {
    // Room support
    def room_version = "2.1.0-alpha04"
    implementation "androidx.room:room-runtime:$room_version"
    kapt 'androidx.room:room-compiler:2.1.0-alpha04'

    // Anko Commons
    implementation "org.jetbrains.anko:anko-commons:0.10.5"
}

android kotlin - Volley UTF-8 encoding example

MainActivity.kt

package com.cfsuman.kotlintutorials

import android.app.Activity
import android.os.Bundle
import android.text.method.ScrollingMovementMethod
import android.widget.*
import com.android.volley.Request


class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // get the widgets reference from XML layout
        val button = findViewById<Button>(R.id.button)
        val textView = findViewById<TextView>(R.id.textView)

        // Make text view content scrollable
        textView.movementMethod = ScrollingMovementMethod()

        // Feed Url
        val url = "https://www.kalerkantho.com/rss.xml"


        // Run volley
        button.setOnClickListener {
            // Disable the button itself
            it.isEnabled = false

            val stringRequest = VolleyUTF8EncodingStringRequest(
                Request.Method.GET,url,
                { response ->
                    textView.text = response
                }, { error ->
                    textView.text = error.toString()
                })

            // Add the volley string request to the request queue
            VolleySingleton.getInstance(this)
                .addToRequestQueue(stringRequest)
        }
    }
}
VolleyUTF8EncodingStringRequest.kt

package com.cfsuman.kotlintutorials

import com.android.volley.NetworkResponse
import com.android.volley.ParseError
import com.android.volley.Request
import com.android.volley.Response
import com.android.volley.toolbox.HttpHeaderParser
import java.io.UnsupportedEncodingException


// Volley custom string request with utf-8 encoding
class VolleyUTF8EncodingStringRequest(
    method: Int,
    url: String,
    private val mListener: Response.Listener<String>,
    errorListener: Response.ErrorListener
) : Request<String>(method, url, errorListener) {

    override fun deliverResponse(response: String) {
        mListener.onResponse(response)
    }

    override fun parseNetworkResponse(
        response: NetworkResponse): Response<String> {
        var parsed = ""

        val encoding = charset(
            HttpHeaderParser.parseCharset(response.headers))

        try {
            parsed = String(response.data, encoding)
            val bytes = parsed.toByteArray(encoding)
            parsed = String(bytes, charset("UTF-8"))

            return Response.success(
                parsed,
                HttpHeaderParser.parseCacheHeaders(response)
            )
        } catch (e: UnsupportedEncodingException) {
            return Response.error(ParseError(e))
        }
    }
}
VolleySingleton.kt

package com.cfsuman.kotlintutorials

import android.content.Context
import com.android.volley.Request
import com.android.volley.RequestQueue
import com.android.volley.toolbox.Volley

class VolleySingleton constructor(context: Context) {
    companion object {
        @Volatile
        private var INSTANCE: VolleySingleton? = null
        fun getInstance(context: Context) =
            INSTANCE ?: synchronized(this) {
                INSTANCE ?: VolleySingleton(context).also {
                    INSTANCE = it
                }
            }
    }
    private val requestQueue: RequestQueue by lazy {
        // applicationContext is key, it keeps you from leaking the
        // Activity or BroadcastReceiver if someone passes one in.
        Volley.newRequestQueue(context.applicationContext)
    }
    fun <T> addToRequestQueue(req: Request<T>) {
        requestQueue.add(req)
    }
}
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#DCDCDC"
    android:padding="24dp">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Run Volley"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="16dp"
        android:fontFamily="sans-serif"
        android:textSize="24sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button"
        tools:text="TextView" />

</androidx.constraintlayout.widget.ConstraintLayout>
gradle dependencies

// Volley network library
implementation 'com.android.volley:volley:1.2.1'

android kotlin - Volley post request with parameters

MainActivity.kt

package com.cfsuman.kotlintutorials

import android.app.Activity
import android.os.Bundle
import android.text.method.ScrollingMovementMethod
import android.widget.*
import com.android.volley.DefaultRetryPolicy
import com.android.volley.Request
import com.android.volley.toolbox.JsonObjectRequest
import org.json.JSONObject


class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // get the widgets reference from XML layout
        val button = findViewById<Button>(R.id.button)
        val textView = findViewById<TextView>(R.id.textView)

        // Make text view content scrollable
        textView.movementMethod = ScrollingMovementMethod()


        // Run volley
        button.setOnClickListener {
            // Disable the button itself
            it.isEnabled = false

            val url = "https://postman-echo.com/post"
            textView.text = ""

            // Post parameters
            // Form fields and values
            val params = HashMap<String,String>()
            params["foo1"] = "bar1"
            params["foo2"] = "bar2"
            val jsonObject = JSONObject(params as Map<*, *>?)

            // Volley post request with parameters
            val request = JsonObjectRequest(
                Request.Method.POST,url,jsonObject,
                { response ->
                    // Process the json
                    try {
                        textView.text = "Response: $response"
                    } catch (e: Exception) {
                        textView.text = "Exception: $e"
                    }

                }, {
                    // Error in request
                    textView.text = "Volley error: $it"
                })


            // Volley request policy, only one time request to
            // avoid duplicate transaction
            request.retryPolicy = DefaultRetryPolicy(
                DefaultRetryPolicy.DEFAULT_TIMEOUT_MS,
                // 0 means no retry
                0, // DefaultRetryPolicy.DEFAULT_MAX_RETRIES = 2
                1f // DefaultRetryPolicy.DEFAULT_BACKOFF_MULT
            )

            // Add the volley post request to the request queue
            VolleySingleton.getInstance(this)
                .addToRequestQueue(request)
        }
    }
}
VolleySingleton.kt

package com.cfsuman.kotlintutorials

import android.content.Context
import com.android.volley.Request
import com.android.volley.RequestQueue
import com.android.volley.toolbox.Volley

class VolleySingleton constructor(context: Context) {
    companion object {
        @Volatile
        private var INSTANCE: VolleySingleton? = null
        fun getInstance(context: Context) =
            INSTANCE ?: synchronized(this) {
                INSTANCE ?: VolleySingleton(context).also {
                    INSTANCE = it
                }
            }
    }
    private val requestQueue: RequestQueue by lazy {
        // applicationContext is key, it keeps you from leaking the
        // Activity or BroadcastReceiver if someone passes one in.
        Volley.newRequestQueue(context.applicationContext)
    }
    fun <T> addToRequestQueue(req: Request<T>) {
        requestQueue.add(req)
    }
}
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#DCDCDC"
    android:padding="24dp">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Run Volley"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="16dp"
        android:fontFamily="sans-serif"
        android:textSize="24sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button"
        tools:text="TextView" />

</androidx.constraintlayout.widget.ConstraintLayout>
gradle dependencies

// Volley network library
implementation 'com.android.volley:volley:1.2.1'

android kotlin - Volley basic authentication example

MainActivity.kt

package com.cfsuman.kotlintutorials

import android.app.Activity
import android.os.Bundle
import android.widget.*
import com.android.volley.Request


class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // get the widgets reference from XML layout
        val button = findViewById<Button>(R.id.button)
        val textView = findViewById<TextView>(R.id.textView)


        // Run volley
        button.setOnClickListener {
            // Disable the button itself
            it.isEnabled = false

            //val credentials = "username:$password"
            val credentials = "postman:password"
            val url = "https://postman-echo.com/basic-auth"
            textView.text = ""

            // Make a volley custom json object request
            // with basic authentication
            val request = CustomJsonObjectRequestBasicAuth(
                Request.Method.GET, url,null,
                { response->
                    try {
                        // Parse the json object here
                        textView.text = "Response : $response"
                    }catch (e:Exception){
                        e.printStackTrace()
                        textView.text = "Parse exception : $e"
                    }
                }, {
                    textView.text = "Volley error: $it}"
                },credentials
            )

            // Add the volley request to request queue
            VolleySingleton.getInstance(this)
                .addToRequestQueue(request)
        }
    }
}
CustomJsonObjectRequestBasicAuth.kt

package com.cfsuman.kotlintutorials

import android.util.Base64
import com.android.volley.AuthFailureError
import com.android.volley.Response
import com.android.volley.toolbox.JsonObjectRequest
import org.json.JSONObject


// Class to make a volley json object request
// with basic authentication
class CustomJsonObjectRequestBasicAuth(
    method:Int, url: String,
    jsonObject: JSONObject?,
    listener: Response.Listener<JSONObject>,
    errorListener: Response.ErrorListener,
    credentials:String
)
    : JsonObjectRequest(
    method,url, jsonObject, listener, errorListener) {

    private var mCredentials:String = credentials

    @Throws(AuthFailureError::class)
    override fun getHeaders(): Map<String, String> {
        val headers = HashMap<String, String>()
        headers["Content-Type"] = "application/json"
        //val credentials:String = "username:password"
        val auth = "Basic " + Base64.encodeToString(
            mCredentials.toByteArray(),
            Base64.NO_WRAP)
        headers["Authorization"] = auth
        return headers
    }
}
VolleySingleton.kt

package com.cfsuman.kotlintutorials

import android.content.Context
import com.android.volley.Request
import com.android.volley.RequestQueue
import com.android.volley.toolbox.Volley

class VolleySingleton constructor(context: Context) {
    companion object {
        @Volatile
        private var INSTANCE: VolleySingleton? = null
        fun getInstance(context: Context) =
            INSTANCE ?: synchronized(this) {
                INSTANCE ?: VolleySingleton(context).also {
                    INSTANCE = it
                }
            }
    }
    private val requestQueue: RequestQueue by lazy {
        // applicationContext is key, it keeps you from leaking the
        // Activity or BroadcastReceiver if someone passes one in.
        Volley.newRequestQueue(context.applicationContext)
    }
    fun <T> addToRequestQueue(req: Request<T>) {
        requestQueue.add(req)
    }
}
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#DCDCDC"
    android:padding="24dp">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Run Volley"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="16dp"
        android:fontFamily="sans-serif"
        android:textSize="24sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button"
        tools:text="TextView" />

</androidx.constraintlayout.widget.ConstraintLayout>
gradle dependencies

// Volley network library
implementation 'com.android.volley:volley:1.2.1'

android kotlin - Volley example

MainActivity.kt

package com.cfsuman.kotlintutorials

import android.app.Activity
import android.os.Bundle
import android.text.method.ScrollingMovementMethod
import android.util.Log
import android.widget.*
import com.android.volley.Request
import com.android.volley.toolbox.JsonObjectRequest
import org.json.JSONException


class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // get the widgets reference from XML layout
        val button = findViewById<Button>(R.id.button)
        val textView = findViewById<TextView>(R.id.textView)

        // Make text view text scrollable
        textView.movementMethod = ScrollingMovementMethod()

        // Data URL
        val url = "https://pastebin.com/raw/2bW31yqa"


        // Run volley
        button.setOnClickListener {
            // disable the button itself
            it.isEnabled = false

            textView.text = ""

            val jsonObjectRequest = JsonObjectRequest(
                Request.Method.GET, url, null,
                { response ->
                    // Process the JSON
                    try {
                        // Get the JSON array
                        val array = response.getJSONArray("students")

                        // Loop through the array elements
                        for (i in 0 until array.length()) {
                            // Get current json object
                            val student = array.getJSONObject(i)

                            // Get the current student (json object) data
                            val firstName = student.getString("firstname")
                            val lastName = student.getString("lastname")
                            val age = student.getString("age")

                            // Display the formatted json data in text view
                            textView.append("$firstName $lastName\nage : $age")
                            textView.append("\n\n")
                        }
                    } catch (e: JSONException) {
                        e.printStackTrace()
                    }
                }, { error ->
                    Log.d("error", error.toString())
                }
            )

            // Access the RequestQueue through singleton class.
            VolleySingleton.getInstance(this)
                .addToRequestQueue(jsonObjectRequest)
        }
    }
}
VolleySingleton.kt

package com.cfsuman.kotlintutorials

import android.content.Context
import com.android.volley.Request
import com.android.volley.RequestQueue
import com.android.volley.toolbox.Volley

class VolleySingleton constructor(context: Context) {
    companion object {
        @Volatile
        private var INSTANCE: VolleySingleton? = null
        fun getInstance(context: Context) =
            INSTANCE ?: synchronized(this) {
                INSTANCE ?: VolleySingleton(context).also {
                    INSTANCE = it
                }
            }
    }
    private val requestQueue: RequestQueue by lazy {
        // applicationContext is key, it keeps you from leaking the
        // Activity or BroadcastReceiver if someone passes one in.
        Volley.newRequestQueue(context.applicationContext)
    }
    fun <T> addToRequestQueue(req: Request<T>) {
        requestQueue.add(req)
    }
}
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#DCDCDC"
    android:padding="24dp">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Run Volley"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="16dp"
        android:fontFamily="sans-serif"
        android:textSize="24sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button"
        tools:text="TextView" />

</androidx.constraintlayout.widget.ConstraintLayout>
gradle dependencies

// Volley network library
implementation 'com.android.volley:volley:1.2.1'

Android Kotlin: WorkManager constraints example

Introduction

In this article, we explore an Android Kotlin example using WorkManager to handle background tasks with specific constraints. WorkManager is part of Jetpack libraries and provides a simple yet flexible API for scheduling deferrable, guaranteed tasks that will run even if the app exits or the device restarts. In our example, we use WorkManager to download an image from a URL with certain constraints, such as the device being on charge and having a network connection.

This breakdown will guide you through the key components of this example, highlighting the WorkManager’s constraints, handling background tasks with worker classes, and the integration of UI elements to display the download status and the downloaded image.

MainActivity Overview

The main activity (MainActivity.kt) is the entry point for the application and contains the logic for triggering the download process. When the user clicks the "Download Image" button, a WorkRequest is created with constraints. These constraints ensure that the download only starts when the device is connected to a network and charging. This is achieved using a Constraints.Builder object, which is then attached to the WorkRequest.

The constraints defined here include setRequiresCharging(true), which ensures that the device is charging before the task starts, and setRequiredNetworkType(NetworkType.CONNECTED), which ensures a network connection is available. Additional constraints, like requiring the battery to not be low, can be added as needed but are commented out in this example for simplicity.

Setting Up WorkManager

To start the download, we enqueue a OneTimeWorkRequest using the WorkManager class. This request is linked to the DownloadImageWorker class, which handles the actual downloading in the background. We also pass input data, including the URL of the image to download, using the Data.Builder().

Once the work is enqueued, we observe its status using getWorkInfoByIdLiveData(), which allows real-time updates on the task’s progress. This status is reflected in the UI through a TextView, showing different states such as "Download enqueued," "Download running," and finally either "Download successful" or "Failed to download," depending on the outcome.

Downloading the Image with Worker Class

The DownloadImageWorker class is responsible for performing the background task. It extends the Worker class, and the doWork() method is overridden to define the actual task logic. The image is downloaded from the provided URL using an HTTP connection, and the input stream is converted into a Bitmap object.

The downloaded image is then saved to the device's internal storage using a custom extension function, saveToInternalStorage(). This function saves the bitmap to a private directory and returns the URI of the saved image. If the download is successful, Result.success() is returned along with the URI, which is passed back to MainActivity to display the image. In case of failure, Result.failure() is returned.

Saving the Image to Internal Storage

The extension function saveToInternalStorage() handles saving the downloaded image in the internal storage of the device. It creates a directory inside the app's internal storage and saves the image as a PNG file. This function is useful because it abstracts away the complexities of file handling, returning the URI of the saved file, which can be used later to display the image in an ImageView.

Additionally, the helper function stringToURL() safely converts a string to a URL object, catching any potential MalformedURLException that may occur during the conversion.

XML Layout and UI Interaction

The layout file (activity_main.xml) defines the user interface, which includes a Button, a TextView, and an ImageView. When the user presses the "Download Image" button, the download process begins, and the TextView displays the status of the download. Upon successful completion, the downloaded image is displayed in the ImageView using the URI retrieved from the DownloadImageWorker.

The layout is simple and designed to focus on the core functionality: downloading an image and updating the UI based on the background task's status.

Conclusion

This Android Kotlin example demonstrates how to use WorkManager with constraints to handle background tasks efficiently. The app ensures that the download process only starts when specific conditions, like charging and network connectivity, are met, improving battery life and user experience. By observing the task’s progress with live data, the app updates the UI in real-time, providing feedback to the user about the task's status.

WorkManager offers a robust solution for scheduling deferrable background tasks, making it ideal for operations like downloading files, uploading data, or processing large datasets without interrupting the user experience. This example is a starting point for integrating more complex background tasks into your Android applications.


MainActivity.kt

package com.cfsuman.jetpack

import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import kotlinx.android.synthetic.main.activity_main.*
import android.graphics.BitmapFactory
import java.io.BufferedInputStream
import java.io.IOException
import java.net.HttpURLConnection
import android.graphics.Bitmap
import android.net.Uri
import androidx.lifecycle.Observer
import androidx.work.*


class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Download image
        btnDownload.setOnClickListener {
            // Create a Constraints object that defines when the task should run
            val downloadConstraints = Constraints.Builder()
                // Device need to charging for the WorkRequest to run.
                .setRequiresCharging(true)
                // Any working network connection is required for this work.
                .setRequiredNetworkType(NetworkType.CONNECTED)
                //.setRequiresBatteryNotLow(true)
                // Many other constraints are available, see the
                // Constraints.Builder reference
                .build()

            // Define the input data for work manager
            val data = Data.Builder()
            data.putString("imageUrl",
                "https://www.freeimageslive.com/galleries/buildings/structures/pics/canal100-0416.jpg")

            // Create an one time work request
            val downloadImageWork = OneTimeWorkRequest
                .Builder(DownloadImageWorker::class.java)
                .setInputData(data.build())
                .setConstraints(downloadConstraints)
                .build()

            // Enqueue the work
            WorkManager.getInstance().enqueue((downloadImageWork))

            // Get the work status using live data
            WorkManager.getInstance().getWorkInfoByIdLiveData(downloadImageWork.id)
                .observe(this, Observer { workInfo ->

                    // Toast the work state
                    toast(workInfo.state.name)

                    if (workInfo != null) {
                        if (workInfo.state == WorkInfo.State.ENQUEUED) {
                            // Show the work state in text view
                            textView.text = "Download enqueued."
                        } else if (workInfo.state == WorkInfo.State.BLOCKED) {
                            textView.text = "Download blocked."
                        } else if (workInfo.state == WorkInfo.State.RUNNING) {
                            textView.text = "Download running."
                        }
                    }

                    // When work finished
                    if (workInfo != null && workInfo.state.isFinished) {
                        if (workInfo.state == WorkInfo.State.SUCCEEDED) {
                            textView.text = "Download successful."

                            // Get the output data
                            val successOutputData = workInfo.outputData
                            val uriText = successOutputData.getString("uriString")

                            // If uri is not null then show it
                            uriText?.apply {
                                // If download finished successfully then show the downloaded image in image view
                                imageView.setImageURI(Uri.parse(uriText))
                                textView.text = uriText
                            }
                        } else if (workInfo.state == WorkInfo.State.FAILED) {
                            textView.text = "Failed to download."
                        } else if (workInfo.state == WorkInfo.State.CANCELLED) {
                            textView.text = "Work request cancelled."
                        }
                    }
                })
        }
    }
}


// Worker class to download image
class DownloadImageWorker(context:Context, params: WorkerParameters): Worker(context,params){
    override fun doWork(): Result {
        // Get the input data
        val urlString = inputData.getString("imageUrl")
        val url = stringToURL(urlString)

        // IMPORTANT - Put internet permission on manifest file
        var connection: HttpURLConnection? = null

        try {
            // Initialize a new http url connection
            connection = url?.openConnection() as HttpURLConnection

            // Connect the http url connection
            connection.connect()

            // Get the input stream from http url connection
            val inputStream = connection.getInputStream()

            // Initialize a new BufferedInputStream from InputStream
            val bufferedInputStream = BufferedInputStream(inputStream)

            // Convert BufferedInputStream to Bitmap object

            // Return the downloaded bitmap
            val bmp:Bitmap? = BitmapFactory.decodeStream(bufferedInputStream)
            val uri:Uri? = bmp?.saveToInternalStorage(applicationContext)

            Log.d("download","success")
            // Return the success with output data
            return Result.success(createOutputData(uri))

        } catch (e: IOException) {
            e.printStackTrace()
            Log.d("download",e.toString())

        } finally {
            // Disconnect the http url connection
            connection?.disconnect()
        }
        Log.d("download","failed")
        return Result.failure(createOutputData(null))
    }


    // Method to create output data
    private fun createOutputData(uri: Uri?): Data {
        return Data.Builder()
            .putString("uriString", uri.toString())
            .build()
    }
}
xBitmap.kt

package com.cfsuman.jetpack

import android.content.Context
import android.content.ContextWrapper
import android.graphics.Bitmap
import android.net.Uri
import android.widget.Toast
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.net.MalformedURLException
import java.net.URL
import java.util.*


// Extension function to save bitmap in internal storage
fun Bitmap.saveToInternalStorage(context: Context): Uri {
    // Get the context wrapper instance
    val wrapper = ContextWrapper(context)

    // Initializing a new file
    // The bellow line return a directory in internal storage
    var file = wrapper.getDir("images", Context.MODE_PRIVATE)


    // Create a file to save the image
    file = File(file, "${UUID.randomUUID()}.png")

    try {
        // Get the file output stream
        val stream: OutputStream = FileOutputStream(file)

        // Compress bitmap
        this.compress(Bitmap.CompressFormat.PNG, 100, stream)

        // Flush the stream
        stream.flush()

        // Close stream
        stream.close()
    } catch (e: IOException){ // Catch the exception
        e.printStackTrace()
    }

    // Return the saved image uri
    return Uri.parse(file.absolutePath)
}


// Custom method to convert string to url
fun stringToURL(urlString: String?): URL? {
    urlString?.let {
        try {
            return URL(urlString)
        } catch (e: MalformedURLException) {
            e.printStackTrace()
        }
    }

    return null
}


fun Context.toast(message:String)=
    Toast.makeText(this,message, Toast.LENGTH_SHORT).show()
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/rootLayout"
        tools:context=".MainActivity"
        android:background="#fdfdfc">
    <Button
            android:text="Download Image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/btnDownload"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_marginStart="8dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp"/>
    <ImageView
            android:layout_width="0dp"
            android:layout_height="0dp"
            tools:srcCompat="@tools:sample/avatars"
            android:id="@+id/imageView"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="8dp"
            app:layout_constraintHorizontal_bias="0.0"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/textView"
            android:layout_marginBottom="8dp"
            app:layout_constraintBottom_toBottomOf="parent"/>
    <TextView
            android:text=""
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/textView"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="16dp"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            app:layout_constraintTop_toBottomOf="@+id/btnDownload"
            android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium"
            android:textColor="#3F51B5"
            tools:text="Work Status"
            android:layout_marginBottom="16dp"
            app:layout_constraintBottom_toTopOf="@+id/imageView"/>
</androidx.constraintlayout.widget.ConstraintLayout>
gradle dependencies

// WorkManager
def work_version = "1.0.0-beta03"
implementation "android.arch.work:work-runtime:$work_version"

Android Kotlin: WorkManager input output data example

Introduction

WorkManager is a library in Android Jetpack that allows developers to schedule deferrable, asynchronous tasks that are guaranteed to execute even if the app exits or the device is restarted. This makes it highly suitable for background tasks that require guaranteed execution, such as downloading images, syncing data, or sending logs. In this article, we’ll explore an example of using WorkManager in Kotlin to download an image from the internet, manage input and output data, and display the result on the screen. This example showcases how WorkManager simplifies task scheduling and ensures task completion even in challenging circumstances.

In addition to providing an overview of the WorkManager implementation, this breakdown will delve into key components such as input data, output data, observing task states, and handling background processes efficiently. By the end of this walkthrough, you'll have a good understanding of how to implement WorkManager for tasks requiring data transfer and persistent task execution in Android.

MainActivity Overview

The heart of this example lies in MainActivity.kt, where the interaction with the user is handled. The layout features a button (btnDownload) that initiates the process, a TextView to show the status of the download, and an ImageView to display the downloaded image.

When the user clicks the "Download Image" button, it triggers a WorkManager task. The first step is defining the input data needed for the task. In this case, the input is a URL string (imageUrl) for the image to be downloaded. This input data is bundled into a Data.Builder object and passed to the WorkManager task. The task itself is represented by a OneTimeWorkRequest, which is designed to run only once.

Once the WorkRequest is created, it is enqueued using WorkManager.getInstance().enqueue(). This schedules the task to be executed by WorkManager. At the same time, LiveData is used to observe the status of the task through getWorkInfoByIdLiveData(). The state of the task, whether enqueued, running, or finished, is displayed in the TextView. Additionally, a toast notification is triggered to reflect the current state.

WorkManager and Worker Class

The image download process is handled in a background thread by the DownloadImageWorker class, which extends Worker. The doWork() method contains the core logic for downloading the image and saving it to internal storage.

The doWork() method begins by retrieving the imageUrl passed via the input data. This URL is then converted into a HttpURLConnection object, which establishes a connection to the server. Once the connection is made, the input stream of data is retrieved and converted into a Bitmap using BitmapFactory.decodeStream(). This Bitmap object represents the downloaded image.

If the image is successfully downloaded, it is saved to the internal storage of the app using the custom saveToInternalStorage() extension function. The saved file's URI is then returned as part of the output data, indicating a successful task completion. If the download fails for any reason, an error is logged, and a failure result is returned.

Input and Output Data

Input and output data are crucial parts of WorkManager tasks. In this example, input data is used to pass the URL of the image to the worker. The Data.Builder class allows you to pass key-value pairs to the worker, making it easy to handle dynamic data.

The output data is generated in the doWork() method after the image is successfully downloaded and saved. The createOutputData() method creates a Data object containing the URI of the saved image. This output data is then accessed in the MainActivity using LiveData. Once the worker has finished and if the task is successful, the ImageView displays the downloaded image using the URI provided in the output data.

Handling Task States

The application uses LiveData to monitor the state of the WorkManager task. WorkManager provides several states, including ENQUEUED, RUNNING, SUCCEEDED, FAILED, and CANCELLED. These states help in providing feedback to the user during various stages of task execution.

For example, when the task is enqueued, the TextView updates to show "Download enqueued." If the task is blocked or in progress, corresponding messages are displayed. Finally, when the task completes, either successfully or with failure, the UI is updated accordingly. If the task succeeds, the image is shown in the ImageView, and if it fails, an error message is displayed.

Bitmap Saving and Utilities

The custom extension function saveToInternalStorage() in xBitmap.kt handles the task of saving the downloaded bitmap to the device’s internal storage. It creates a unique file name using UUID.randomUUID(), compresses the bitmap, and writes it to an output stream. The function returns the URI of the saved file, which is then passed back to the main activity via output data.

Additionally, the helper function stringToURL() is used to convert the URL string into a proper URL object. This is a simple utility to handle the potential MalformedURLException that could arise during string-to-URL conversion.

Summary

This Android Kotlin example demonstrates how WorkManager can be used to manage background tasks such as downloading images, handling input and output data, and updating the UI based on task states. The separation of concerns is clear, with the MainActivity managing UI updates and user interactions, while the DownloadImageWorker handles the background task of image downloading and saving.

By using WorkManager, the application ensures that the task will complete, even if the app is closed or the device is restarted, making it a robust solution for deferred tasks in Android development. This example provides a foundation for building more complex background tasks using WorkManager in Android.


MainActivity.kt

package com.cfsuman.jetpack

import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import kotlinx.android.synthetic.main.activity_main.*
import android.graphics.BitmapFactory
import java.io.BufferedInputStream
import java.io.IOException
import java.net.HttpURLConnection
import android.graphics.Bitmap
import android.net.Uri
import androidx.lifecycle.Observer
import androidx.work.*


class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Download image
        btnDownload.setOnClickListener {
            // Define the input data for work manager
            val data = Data.Builder()
            data.putString("imageUrl",
                "https://www.freeimageslive.com/galleries/food/breakfast/pics/muffin1964.jpg")

            // Create an one time work request
            val downloadImageWork = OneTimeWorkRequest
                .Builder(DownloadImageWorker::class.java)
                .setInputData(data.build())
                .build()

            // Enqueue the work
            WorkManager.getInstance().enqueue((downloadImageWork))

            // Get the work status using live data
            WorkManager.getInstance().getWorkInfoByIdLiveData(downloadImageWork.id)
                .observe(this, Observer { workInfo ->

                    // Toast the work state
                    toast(workInfo.state.name)

                    if (workInfo != null) {
                        if (workInfo.state == WorkInfo.State.ENQUEUED) {
                            // Show the work state in text view
                            textView.text = "Download enqueued."
                        } else if (workInfo.state == WorkInfo.State.BLOCKED) {
                            textView.text = "Download blocked."
                        } else if (workInfo.state == WorkInfo.State.RUNNING) {
                            textView.text = "Download running."
                        }
                    }

                    // When work finished
                    if (workInfo != null && workInfo.state.isFinished) {
                        if (workInfo.state == WorkInfo.State.SUCCEEDED) {
                            textView.text = "Download successful."

                            // Get the output data
                            val successOutputData = workInfo.outputData
                            val uriText = successOutputData.getString("uriString")

                            // If uri is not null then show it
                            uriText?.apply {
                                // If download finished successfully then show the downloaded image in image view
                                imageView.setImageURI(Uri.parse(uriText))
                                textView.text = uriText
                            }
                        } else if (workInfo.state == WorkInfo.State.FAILED) {
                            textView.text = "Failed to download."
                        } else if (workInfo.state == WorkInfo.State.CANCELLED) {
                            textView.text = "Work request cancelled."
                        }
                    }
                })
        }
    }
}


// Worker class to download image
class DownloadImageWorker(context:Context, params: WorkerParameters): Worker(context,params){
    override fun doWork(): Result {
        // Get the input data
        val urlString = inputData.getString("imageUrl")
        val url = stringToURL(urlString)

        // IMPORTANT - Put internet permission on manifest file
        var connection: HttpURLConnection? = null

        try {
            // Initialize a new http url connection
            connection = url?.openConnection() as HttpURLConnection

            // Connect the http url connection
            connection.connect()

            // Get the input stream from http url connection
            val inputStream = connection.getInputStream()

            // Initialize a new BufferedInputStream from InputStream
            val bufferedInputStream = BufferedInputStream(inputStream)

            // Convert BufferedInputStream to Bitmap object

            // Return the downloaded bitmap
            val bmp:Bitmap? = BitmapFactory.decodeStream(bufferedInputStream)
            val uri:Uri? = bmp?.saveToInternalStorage(applicationContext)

            Log.d("download","success")
            // Return the success with output data
            return Result.success(createOutputData(uri))

        } catch (e: IOException) {
            e.printStackTrace()
            Log.d("download",e.toString())

        } finally {
            // Disconnect the http url connection
            connection?.disconnect()
        }
        Log.d("download","failed")
        return Result.failure(createOutputData(null))
    }


    // Method to create output data
    private fun createOutputData(uri: Uri?): Data {
        return Data.Builder()
            .putString("uriString", uri.toString())
            .build()
    }
}
xBitmap.kt

package com.cfsuman.jetpack

import android.content.Context
import android.content.ContextWrapper
import android.graphics.Bitmap
import android.net.Uri
import android.widget.Toast
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.net.MalformedURLException
import java.net.URL
import java.util.*


// Extension function to save bitmap in internal storage
fun Bitmap.saveToInternalStorage(context: Context): Uri {
    // Get the context wrapper instance
    val wrapper = ContextWrapper(context)

    // Initializing a new file
    // The bellow line return a directory in internal storage
    var file = wrapper.getDir("images", Context.MODE_PRIVATE)


    // Create a file to save the image
    file = File(file, "${UUID.randomUUID()}.png")

    try {
        // Get the file output stream
        val stream: OutputStream = FileOutputStream(file)

        // Compress bitmap
        this.compress(Bitmap.CompressFormat.PNG, 100, stream)

        // Flush the stream
        stream.flush()

        // Close stream
        stream.close()
    } catch (e: IOException){ // Catch the exception
        e.printStackTrace()
    }

    // Return the saved image uri
    return Uri.parse(file.absolutePath)
}


// Custom method to convert string to url
fun stringToURL(urlString: String?): URL? {
    urlString?.let {
        try {
            return URL(urlString)
        } catch (e: MalformedURLException) {
            e.printStackTrace()
        }
    }

    return null
}


fun Context.toast(message:String)=
    Toast.makeText(this,message, Toast.LENGTH_SHORT).show()
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/rootLayout"
        tools:context=".MainActivity"
        android:background="#fdfdfc">
    <Button
            android:text="Download Image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/btnDownload"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_marginStart="8dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp"/>
    <ImageView
            android:layout_width="0dp"
            android:layout_height="0dp"
            tools:srcCompat="@tools:sample/avatars"
            android:id="@+id/imageView"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="8dp"
            app:layout_constraintHorizontal_bias="0.0"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/textView"
            android:layout_marginBottom="8dp"
            app:layout_constraintBottom_toBottomOf="parent"/>
    <TextView
            android:text=""
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/textView"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="16dp"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            app:layout_constraintTop_toBottomOf="@+id/btnDownload"
            android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium"
            android:textColor="#3F51B5"
            tools:text="Work Status"
            android:layout_marginBottom="16dp"
            app:layout_constraintBottom_toTopOf="@+id/imageView"/>
</androidx.constraintlayout.widget.ConstraintLayout>
gradle dependencies

// WorkManager
def work_version = "1.0.0-beta03"
implementation "android.arch.work:work-runtime:$work_version"