To-do-List App Using Room and MVVM architecture
By: Nikitha Gullapalli
Tags: Android App Development, Mobile App Architecture, Mvvm, Database, Room

Topics covered:

Room, MVVM architecture, Executor, LiveData, ItemTouchHelper

This project is to understand Room and its implementation using MVVM app architecture.

Room is an object-mapping library that provides local data persistence with minimal boilerplate code. At compile time, it validates each query against your data schema, so broken SQL queries result in compile-time errors instead of runtime failures. Room abstracts away some of the underlying implementation details of working with raw SQL tables and queries. It also allows you to observe changes to the database’s data, including collections and join queries, exposing such changes using LiveData objects. It even explicitly defines execution constraints that address common threading issues, such as accessing storage on the main thread.

Room provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite.

To-do-list App overview

MVVM Architecture:

The below diagram shows how Room forms an abstract layer over the database. This helps ViewModel to be unaware of the activity lifecycle and helps in testing each functionality separately.

MVVM architecture and Room

There are basically 6 steps to implement Room persistence library in Android

Step 1: Add the following library and annotation processor to your app Gradle file

//liveData and Room

implementation "android.arch.lifecycle:extensions:1.1.1"

annotationProcessor "androidx.lifecycle:lifecycle-common-java8:2.2.0"

Step 2: Component 1 in room — Create an entity class:

The entity class contains all the columns in the database and should be annotated with @Entity. if no specific table name is given, the class name will be the table name, else we can specify the table name(tableName = “task”). If the class has two constructors we can ignore a constructor with the annotation @ignore. Column names can also be different from the variable name. This is done with the annotation @ColumnInfo(name=” name ”). Every database table requires a primary key, which is usually the row id. If we want the primary key to auto-increment we can use the annotation @PrimaryKey(autoGenerate = true), else just say @PrimaryKey and increment it ourselves. It is best practice to allow the library to do it to avoid errors.

import java.util.Date;

import androidx.room.ColumnInfo;

import androidx.room.Entity;

import androidx.room.Ignore;

import androidx.room.PrimaryKey;



@Entity(tableName = "task")

public class TaskEntry {



@PrimaryKey(autoGenerate = true)

private int id;

private String description;

private int priority;

@ColumnInfo(name = "updated_at")

private Date updatedAt;



@Ignore

public TaskEntry(String description, int priority, Date updatedAt) {

this.description = description;

this.priority = priority;

this.updatedAt = updatedAt;

}



public TaskEntry(int id, String description, int priority, Date updatedAt) {

this.id = id;

this.description = description;

this.priority = priority;

this.updatedAt = updatedAt;

}



//Setter and Getters

}

Step 3: Component 2 in room — Create a DAO class:

DAO class contains all the queries to be performed on the DB. It is annotated with the annotation @DAO to inform the Room library that this class is the DAO class.

import java.util.List;

import androidx.lifecycle.LiveData;

import androidx.room.Dao;

import androidx.room.Delete;

import androidx.room.Insert;

import androidx.room.OnConflictStrategy;

import androidx.room.Query;

import androidx.room.Update;



@Dao

public interface TaskDao {

@Query("SELECT * FROM task ORDER BY priority")

LiveData<List<TaskEntry>> loadAllTasks();



@Query("SELECT * FROM task WHERE id=:id")

LiveData<TaskEntry> loadTaskById(int id);



@Insert

void insertTask(TaskEntry taskEntry);



@Update(onConflict = OnConflictStrategy.REPLACE)

void updateTask(TaskEntry taskEntry);



@Delete

void deleteTask(TaskEntry taskEntry);

}

Step 4: Component 3 in room — Converter class:

This class is used to convert any data between the java objects and the database. Indicated with annotation @TypeConverter

import java.util.Date;

import androidx.room.TypeConverter;



public class DateConverter {

@TypeConverter

public static Date toDate(Long timestamp) {

return timestamp == null ? null : new Date(timestamp);

}



@TypeConverter

public static Long toTimestamp(Date date) {

return date == null ? null : date.getTime();

}

}

Step 5: Component 4 in room — Create Database class:

The database class brings all the components of Room together. This is a singleton class, hence only once instance of the class can be created. This is to avoid multiple tables from getting created with same name and messing up data storage.

import android.content.Context;

import android.util.Log;

import androidx.room.Database;

import androidx.room.Room;

import androidx.room.RoomDatabase;

import androidx.room.TypeConverters;



@Database(entities = {TaskEntry.class}, version = 1, exportSchema = false)

@TypeConverters(DateConverter.class)

public abstract class AppDatabase extends RoomDatabase { private static final String LOG_TAG =AppDatabase.class

.getSimpleName();

private static final Object LOCK = new Object();

private static final String DATABASE_NAME = "todolist";

private static AppDatabase sInstance;



public static AppDatabase getInstance(Context context) {

if (sInstance == null) {

synchronized (LOCK) {

Log.d(LOG_TAG, "Creating new database instance");

sInstance = Room.databaseBuilder(context

.getApplicationContext(),

AppDatabase.class, AppDatabase.DATABASE_NAME)

.build();

}

}

Log.d(LOG_TAG, "Getting the database instance");

return sInstance;

}



public abstract TaskDao taskDao();

}

Step 6: Create the Repository class:

Activity or fragment will call the ViewModel class. This class is responsible for caching data to avoid unnecessary DB class. For better layering of responsibilities, ViewModel class will call the Repository class which inturn makes and DB or network calls using ROOM or Retrofit. Once the data is received from the calls, the data is sent to the ViewModel class.

This helps the underlying layers like ViewModel, the repository to be independent of the activity/fragment lifecycle. Activity is subscribed to the ViewModel and every time there is a change in data in the DB all the subscribers are informed of the change when the activity/fragment is active.

public class TasksRepository {

private static final String LOG_TAG = TasksRepository.class

.getSimpleName();

private LiveData<List<TaskEntry>> tasks;

private TaskDao taskDao;

AppDatabase database;

public TasksRepository(AppDatabase database) {

this.database = database;

}



public LiveData<List<TaskEntry>> getloadAllTasks() {

tasks = database.taskDao().loadAllTasks();

return tasks;

}



public LiveData<TaskEntry> getloadTaskById(int taskId) {

return database.taskDao().loadTaskById(taskId);

}



public void deleteTasks(TaskEntry taskEntry) {

database.taskDao().deleteTask(taskEntry);

}



public void updateTaskById(TaskEntry task) {

database.taskDao().updateTask(task);

}

}

Step 7: Create the View Model class:

This class, in turn, calls the repository class and is the sub-layer between the activity and the repository.

public class MainViewModel extends AndroidViewModel {



// Constant for logging

private static final String TAG = MainViewModel.class.getSimpleName();



private LiveData<List<TaskEntry>> tasks;

private final TasksRepository tasksRepository;

public MainViewModel(Application application) {

super(application);

AppDatabase database = AppDatabase.getInstance(this.getApplication());

Log.d(TAG, "Actively retrieving the tasks from the DataBase");

//tasks = database.taskDao().loadAllTasks();

tasksRepository = new TasksRepository(database);

tasks = tasksRepository.getloadAllTasks();

}



public LiveData<List<TaskEntry>> getTasks() {

return tasks;

}



public void deleteTask(TaskEntry taskEntry) {

tasksRepository.deleteTasks(taskEntry);

}

}

Step 8: Observe ViewModel from the Activity:

If you need to send some inputs to the ViewModel you can do so using the ViewModelProvider.NewInstanceFactory. Here I do not so did not use it. However I did show the implementation for another activity, so please check the GitHub Project for more details.

viewModel = ViewModelProviders.of(this).get(MainViewModel.class);

viewModel.getTasks().observe(this, new Observer<List<TaskEntry>>() {

@Override

public void onChanged(@Nullable List<TaskEntry> taskEntries) {

Log.d(TAG, "Updating list of tasks from LiveData in ViewModel");

mAdapter.setTasks(taskEntries);

mAdapter.notifyDataSetChanged(); //optional statement. will work the same without also

}

});

Conclusion:

In conclusion Room is an excellent library and it makes life very easy. It helps in clean architecture and expanding the project in a more manageable way.

Reference:

https://android.jlelse.eu/5-steps-to-implement-room-persistence-library-in-android-47b10cd47b24