Working with SQLite directly in Android means a lot of boilerplate: SQLiteOpenHelper subclasses, ContentValues, cursor parsing, and no compile-time verification that your queries are valid. Room is Google's answer to that — a lightweight ORM built on top of SQLite that handles the repetitive work, verifies your SQL at compile time, and keeps your database layer clean and testable.
It has three main pieces: Entity (a table), DAO (data access methods), and Database (the entry point). Here's how they fit together in a simple Todo app.
## Entity
An entity is a plain Java class annotated with @Entity. Each field maps to a column; @PrimaryKey marks the row identifier; @ColumnInfo lets you specify the column name independently of the field name.
@Entity(tableName = "todo")
public class Todo {
@PrimaryKey
private long _id;
@ColumnInfo(name = "title")
private String todoTitle;
@ColumnInfo(name = "text")
private String todoText;
@ColumnInfo(name = "time")
private long timeStamp;
public long get_id() { return _id; }
public void set_id(long _id) { this._id = _id; }
public String getTodoTitle() { return todoTitle; }
public void setTodoTitle(String todoTitle) { this.todoTitle = todoTitle; }
public String getTodoText() { return todoText; }
public void setTodoText(String todoText) { this.todoText = todoText; }
public long getTimeStamp() { return timeStamp; }
public void setTimeStamp(long timeStamp) { this.timeStamp = timeStamp; }
}
## DAO
The DAO is an interface annotated with @Dao. Room generates the implementation at compile time. Each method is annotated with the operation it performs — @Query for reads, @Insert, @Update, and @Delete for writes.
@Dao
public interface AppDao {
@Query("Select * from " + AppConstants.DB_NAME + " order by time desc")
List<Todo> getAllTodos();
@Query("Select * from " + AppConstants.DB_NAME + " where _id = :id order by time desc")
Todo getTodo(String id);
@Insert
void insertTodo(Todo todo);
@Update
void updateTodo(Todo todo);
@Delete
void deleteTodo(Todo todo);
}
Tip
The @Query annotation is verified at compile time — a typo in a column name or table name is a build error, not a runtime crash.
## Database
The database class extends RoomDatabase and declares which entities it includes and the current schema version. It exposes abstract methods that return your DAO interfaces.
@Database(entities = {Todo.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract AppDao appDao();
}
## Singleton Access
Building a RoomDatabase is expensive, so wrap it in a singleton:
public class AppDBHandler {
private AppDao appdao;
private static AppDBHandler ourInstance;
public static AppDBHandler getInstance(Context context) {
if (ourInstance == null) {
ourInstance = new AppDBHandler(context);
}
return ourInstance;
}
private AppDBHandler(Context context) {
AppDatabase db = Room.databaseBuilder(
context,
AppDatabase.class,
AppConstants.DB_NAME
).build();
appdao = db.appDao();
}
public AppDao getAppDao() {
return appdao;
}
}
## A Critical Note on Threading
Warning
All Room database operations are synchronous and run on the calling thread. Calling them on the UI thread throws IllegalStateException. Run them on a background thread and post UI updates back to the main thread.
### Loading todos
recyclerView.setAdapter(todoListAdapter);
final AppDao dao = AppDBHandler.getInstance(getApplicationContext()).getAppDao();
Thread T1 = new Thread(new Runnable() {
@Override
public void run() {
todos.addAll(dao.getAllTodos());
runOnUiThread(new Runnable() {
@Override
public void run() {
todoListAdapter.notifyItemRangeInserted(0, todos.size());
}
});
}
});
T1.start();
### Deleting a todo
case 1: {
final AppDao dao = AppDBHandler.getInstance(getApplicationContext()).getAppDao();
Thread T1 = new Thread(new Runnable() {
@Override
public void run() {
dao.deleteTodo(todos.get(position));
todos.remove(position);
runOnUiThread(new Runnable() {
@Override
public void run() {
Snackbar.make(parentLayout, "Todo Deleted", Snackbar.LENGTH_SHORT).show();
todoListAdapter.notifyItemRemoved(position);
}
});
}
});
T1.start();
break;
}
### Creating and updating a todo
@Override
public void onClick(View view) {
if (this.todo == null) {
todo = new Todo();
todo.set_id(System.currentTimeMillis());
todo.setTimeStamp(System.currentTimeMillis());
todo.setTodoTitle(edtTitle.getText().toString());
todo.setTodoText(edtText.getText().toString());
final AppDao dao = AppDBHandler.getInstance(getContext()).getAppDao();
Thread T1 = new Thread(new Runnable() {
@Override
public void run() {
dao.insertTodo(todo);
dataCallbackListener.onDataReceived(todo);
}
});
T1.start();
} else {
todo.setTimeStamp(System.currentTimeMillis());
todo.setTodoTitle(edtTitle.getText().toString());
todo.setTodoText(edtText.getText().toString());
final AppDao dao = AppDBHandler.getInstance(getContext()).getAppDao();
Thread T1 = new Thread(new Runnable() {
@Override
public void run() {
dao.updateTodo(todo);
dataCallbackListener.onDataReceived(todo);
}
});
T1.start();
}
}
Room replaces a significant amount of SQLite boilerplate with annotated interfaces and compile-time verification. The threading model is explicit — which is the right call for a database library. In practice you'd reach for RxJava or coroutines on top of Room rather than raw threads, but the underlying principle is the same: keep database work off the UI thread.