Hola amigos 👋 Bienvenidos a un nuevo tutorial de Universo Android. Hoy aprenderemos a usar Room, la biblioteca de persistencia de datos moderna sobre SQLite que simplifica el acceso a bases de datos locales con type-safety y menos código boilerplate.
Al finalizar este tutorial tendrás:
- Base de datos Room completa
- Entities, DAOs y Database
- Operaciones CRUD (Create, Read, Update, Delete)
- Queries personalizadas
- LiveData y observables
- RecyclerView con datos locales
- Migraciones de base de datos
🟩 1. ¿Qué es Room?
Room es una capa de abstracción sobre SQLite que proporciona acceso fluido a la base de datos, validación en compile-time de queries SQL y observables automáticos para actualizar la UI.
🟩 2. Componentes de Room
- Entity: Representa una tabla en la BD
- DAO: Data Access Object - Define operaciones
- Database: Contiene la BD y sirve como punto de acceso
🟩 3. Crear el Proyecto
Creamos un nuevo proyecto en Android Studio con Empty Activity.
🟩 4. Agregar Dependencias
📄 build.gradle (Module: app)
plugins {
id 'com.android.application'
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.cardview:cardview:1.0.0'
// Room
implementation 'androidx.room:room-runtime:2.6.1'
annotationProcessor 'androidx.room:room-compiler:2.6.1'
// Room para coroutines (opcional pero recomendado)
implementation 'androidx.room:room-ktx:2.6.1'
// Lifecycle components
implementation 'androidx.lifecycle:lifecycle-livedata:2.7.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.7.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
}🟩 5. Entity (Modelo)
📄 Task.java
package com.example.roomapp;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity(tableName = "tasks_table")
public class Task {
@PrimaryKey(autoGenerate = true)
private int id;
@ColumnInfo(name = "task_title")
private String title;
@ColumnInfo(name = "task_description")
private String description;
@ColumnInfo(name = "task_priority")
private int priority; // 1=Baja, 2=Media, 3=Alta
@ColumnInfo(name = "task_completed")
private boolean completed;
@ColumnInfo(name = "task_date")
private long date;
// Constructor
public Task(String title, String description, int priority, boolean completed, long date) {
this.title = title;
this.description = description;
this.priority = priority;
this.completed = completed;
this.date = date;
}
// Getters y Setters
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public int getPriority() { return priority; }
public void setPriority(int priority) { this.priority = priority; }
public boolean isCompleted() { return completed; }
public void setCompleted(boolean completed) { this.completed = completed; }
public long getDate() { return date; }
public void setDate(long date) { this.date = date; }
}🟩 6. DAO (Data Access Object)
📄 TaskDao.java
package com.example.roomapp;
import androidx.lifecycle.LiveData;
import androidx.room.*;
import java.util.List;
@Dao
public interface TaskDao {
// Insertar
@Insert
void insert(Task task);
@Insert
void insertAll(Task... tasks);
// Actualizar
@Update
void update(Task task);
// Eliminar
@Delete
void delete(Task task);
@Query("DELETE FROM tasks_table")
void deleteAllTasks();
@Query("DELETE FROM tasks_table WHERE id = :taskId")
void deleteById(int taskId);
// Consultas
@Query("SELECT * FROM tasks_table ORDER BY task_priority DESC, task_date DESC")
LiveData<List<Task>> getAllTasks();
@Query("SELECT * FROM tasks_table WHERE id = :id")
LiveData<Task> getTaskById(int id);
@Query("SELECT * FROM tasks_table WHERE task_completed = 0 ORDER BY task_priority DESC")
LiveData<List<Task>> getPendingTasks();
@Query("SELECT * FROM tasks_table WHERE task_completed = 1 ORDER BY task_date DESC")
LiveData<List<Task>> getCompletedTasks();
@Query("SELECT * FROM tasks_table WHERE task_priority = :priority ORDER BY task_date DESC")
LiveData<List<Task>> getTasksByPriority(int priority);
@Query("SELECT * FROM tasks_table WHERE task_title LIKE '%' || :searchQuery || '%' OR task_description LIKE '%' || :searchQuery || '%'")
LiveData<List<Task>> searchTasks(String searchQuery);
@Query("SELECT COUNT(*) FROM tasks_table")
LiveData<Integer> getTasksCount();
@Query("SELECT COUNT(*) FROM tasks_table WHERE task_completed = 0")
LiveData<Integer> getPendingTasksCount();
}🟩 7. Database
📄 TaskDatabase.java
package com.example.roomapp;
import android.content.Context;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Database(entities = {Task.class}, version = 1, exportSchema = false)
public abstract class TaskDatabase extends RoomDatabase {
public abstract TaskDao taskDao();
private static volatile TaskDatabase INSTANCE;
private static final int NUMBER_OF_THREADS = 4;
static final ExecutorService databaseWriteExecutor =
Executors.newFixedThreadPool(NUMBER_OF_THREADS);
static TaskDatabase getDatabase(final Context context) {
if (INSTANCE == null) {
synchronized (TaskDatabase.class) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context.getApplicationContext(),
TaskDatabase.class,
"task_database"
)
.fallbackToDestructiveMigration()
.build();
}
}
}
return INSTANCE;
}
}🟩 8. Repository
📄 TaskRepository.java
package com.example.roomapp;
import android.app.Application;
import androidx.lifecycle.LiveData;
import java.util.List;
public class TaskRepository {
private TaskDao taskDao;
private LiveData<List<Task>> allTasks;
private LiveData<List<Task>> pendingTasks;
private LiveData<Integer> tasksCount;
public TaskRepository(Application application) {
TaskDatabase database = TaskDatabase.getDatabase(application);
taskDao = database.taskDao();
allTasks = taskDao.getAllTasks();
pendingTasks = taskDao.getPendingTasks();
tasksCount = taskDao.getTasksCount();
}
// Observables
public LiveData<List<Task>> getAllTasks() {
return allTasks;
}
public LiveData<List<Task>> getPendingTasks() {
return pendingTasks;
}
public LiveData<List<Task>> getCompletedTasks() {
return taskDao.getCompletedTasks();
}
public LiveData<Integer> getTasksCount() {
return tasksCount;
}
public LiveData<List<Task>> searchTasks(String query) {
return taskDao.searchTasks(query);
}
// Operaciones en background
public void insert(Task task) {
TaskDatabase.databaseWriteExecutor.execute(() -> {
taskDao.insert(task);
});
}
public void update(Task task) {
TaskDatabase.databaseWriteExecutor.execute(() -> {
taskDao.update(task);
});
}
public void delete(Task task) {
TaskDatabase.databaseWriteExecutor.execute(() -> {
taskDao.delete(task);
});
}
public void deleteAllTasks() {
TaskDatabase.databaseWriteExecutor.execute(() -> {
taskDao.deleteAllTasks();
});
}
}🟩 9. ViewModel
📄 TaskViewModel.java
package com.example.roomapp;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import java.util.List;
public class TaskViewModel extends AndroidViewModel {
private TaskRepository repository;
private LiveData<List<Task>> allTasks;
private LiveData<List<Task>> pendingTasks;
private LiveData<Integer> tasksCount;
public TaskViewModel(@NonNull Application application) {
super(application);
repository = new TaskRepository(application);
allTasks = repository.getAllTasks();
pendingTasks = repository.getPendingTasks();
tasksCount = repository.getTasksCount();
}
// Getters
public LiveData<List<Task>> getAllTasks() {
return allTasks;
}
public LiveData<List<Task>> getPendingTasks() {
return pendingTasks;
}
public LiveData<List<Task>> getCompletedTasks() {
return repository.getCompletedTasks();
}
public LiveData<Integer> getTasksCount() {
return tasksCount;
}
public LiveData<List<Task>> searchTasks(String query) {
return repository.searchTasks(query);
}
// Operaciones
public void insert(Task task) {
repository.insert(task);
}
public void update(Task task) {
repository.update(task);
}
public void delete(Task task) {
repository.delete(task);
}
public void deleteAllTasks() {
repository.deleteAllTasks();
}
}🟩 10. Diseño XML
📄 activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:background="#2196F3">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Mis Tareas"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="#FFFFFF" />
<TextView
android:id="@+id/txtTaskCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="#FFFFFF"
android:background="@drawable/badge_background"
android:padding="8dp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<Button
android:id="@+id/btnShowAll"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Todas"
android:layout_margin="4dp" />
<Button
android:id="@+id/btnShowPending"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Pendientes"
android:layout_margin="4dp" />
<Button
android:id="@+id/btnShowCompleted"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Completadas"
android:layout_margin="4dp" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp" />
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:src="@android:drawable/ic_input_add"
app:tint="#FFFFFF" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>🟩 11. Layout del Item
📄 res/layout/item_task.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<CheckBox
android:id="@+id/checkboxCompleted"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="12dp">
<TextView
android:id="@+id/txtTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TÃtulo"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#000000" />
<TextView
android:id="@+id/txtDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Descripción"
android:textSize="14sp"
android:textColor="#666666"
android:layout_marginTop="4dp"
android:maxLines="2"
android:ellipsize="end" />
<TextView
android:id="@+id/txtDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Fecha"
android:textSize="12sp"
android:textColor="#999999"
android:layout_marginTop="8dp" />
</LinearLayout>
<View
android:id="@+id/viewPriority"
android:layout_width="4dp"
android:layout_height="match_parent"
android:background="#4CAF50"
android:layout_marginStart="12dp" />
</LinearLayout>
</androidx.cardview.widget.CardView>🟩 12. Adaptador
📄 TaskAdapter.java
import android.graphics.Paint;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
public class TaskAdapter extends RecyclerView.Adapter<TaskAdapter.ViewHolder> {
private List<Task> tasks = new ArrayList<>();
private OnTaskListener listener;
public interface OnTaskListener {
void onTaskClick(Task task);
void onTaskLongClick(Task task);
void onTaskCompleteChange(Task task, boolean isCompleted);
}
public TaskAdapter(OnTaskListener listener) {
this.listener = listener;
}
public void setTasks(List<Task> tasks) {
this.tasks = tasks;
notifyDataSetChanged();
}
public Task getTaskAt(int position) {
return tasks.get(position);
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_task, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.bind(tasks.get(position));
}
@Override
public int getItemCount() {
return tasks.size();
}
class ViewHolder extends RecyclerView.ViewHolder {
CheckBox checkboxCompleted;
TextView txtTitle, txtDescription, txtDate;
View viewPriority;
ViewHolder(View itemView) {
super(itemView);
checkboxCompleted = itemView.findViewById(R.id.checkboxCompleted);
txtTitle = itemView.findViewById(R.id.txtTitle);
txtDescription = itemView.findViewById(R.id.txtDescription);
txtDate = itemView.findViewById(R.id.txtDate);
viewPriority = itemView.findViewById(R.id.viewPriority);
}
void bind(Task task) {
txtTitle.setText(task.getTitle());
txtDescription.setText(task.getDescription());
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.getDefault());
txtDate.setText(sdf.format(new Date(task.getDate())));
checkboxCompleted.setChecked(task.isCompleted());
// Estilo tachado si está completada
if (task.isCompleted()) {
txtTitle.setPaintFlags(txtTitle.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
txtDescription.setPaintFlags(txtDescription.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
} else {
txtTitle.setPaintFlags(txtTitle.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
txtDescription.setPaintFlags(txtDescription.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
}
// Color según prioridad
int priorityColor;
switch (task.getPriority()) {
case 3: // Alta
priorityColor = 0xFFF44336;
break;
case 2: // Media
priorityColor = 0xFFFF9800;
break;
default: // Baja
priorityColor = 0xFF4CAF50;
break;
}
viewPriority.setBackgroundColor(priorityColor);
// Listeners
checkboxCompleted.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (listener != null) {
listener.onTaskCompleteChange(task, isChecked);
}
});
itemView.setOnClickListener(v -> {
if (listener != null) {
listener.onTaskClick(task);
}
});
itemView.setOnLongClickListener(v -> {
if (listener != null) {
listener.onTaskLongClick(task);
}
return true;
});
}
}
}🟩 13. MainActivity
📄 MainActivity.java
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
public class MainActivity extends AppCompatActivity {
private TaskViewModel taskViewModel;
private TaskAdapter adapter;
private TextView txtTaskCount;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView recyclerView = findViewById(R.id.recyclerView);
FloatingActionButton fabAdd = findViewById(R.id.fabAdd);
Button btnShowAll = findViewById(R.id.btnShowAll);
Button btnShowPending = findViewById(R.id.btnShowPending);
Button btnShowCompleted = findViewById(R.id.btnShowCompleted);
txtTaskCount = findViewById(R.id.txtTaskCount);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
adapter = new TaskAdapter(new TaskAdapter.OnTaskListener() {
@Override
public void onTaskClick(Task task) {
Toast.makeText(MainActivity.this, task.getTitle(), Toast.LENGTH_SHORT).show();
}
@Override
public void onTaskLongClick(Task task) {
taskViewModel.delete(task);
Toast.makeText(MainActivity.this, "Tarea eliminada", Toast.LENGTH_SHORT).show();
}
@Override
public void onTaskCompleteChange(Task task, boolean isCompleted) {
task.setCompleted(isCompleted);
taskViewModel.update(task);
}
});
recyclerView.setAdapter(adapter);
// Swipe to delete
new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
return false;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
Task task = adapter.getTaskAt(viewHolder.getAdapterPosition());
taskViewModel.delete(task);
Toast.makeText(MainActivity.this, "Tarea eliminada", Toast.LENGTH_SHORT).show();
}
}).attachToRecyclerView(recyclerView);
taskViewModel = new ViewModelProvider(this).get(TaskViewModel.class);
taskViewModel.getAllTasks().observe(this, tasks -> {
adapter.setTasks(tasks);
});
taskViewModel.getTasksCount().observe(this, count -> {
txtTaskCount.setText(String.valueOf(count != null ? count : 0));
});
fabAdd.setOnClickListener(v -> addSampleTask());
btnShowAll.setOnClickListener(v -> {
taskViewModel.getAllTasks().observe(this, tasks -> adapter.setTasks(tasks));
});
btnShowPending.setOnClickListener(v -> {
taskViewModel.getPendingTasks().observe(this, tasks -> adapter.setTasks(tasks));
});
btnShowCompleted.setOnClickListener(v -> {
taskViewModel.getCompletedTasks().observe(this, tasks -> adapter.setTasks(tasks));
});
}
private void addSampleTask() {
Task task = new Task(
"Nueva Tarea",
"Descripción de la tarea",
2, // Prioridad media
false,
System.currentTimeMillis()
);
taskViewModel.insert(task);
Toast.makeText(this, "Tarea agregada", Toast.LENGTH_SHORT).show();
}
}🟩 14. Drawable Badge
📄 res/drawable/badge_background.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#FF5722" />
</shape>▶️ Cómo Ejecutar
- Abre Android Studio
- Copia el código
- Sync Gradle
- Presiona Run
- Agrega, completa y elimina tareas
🧪 Resultado Final
App completa con base de datos SQLite usando Room, CRUD operations, filtros y swipe to delete.
📥 Descargar Proyecto
👉
🙌 Gracias por Visitar mi Blog
✔️ Compártelo
✔️ Déjame un comentario
✔️ SÃgueme para más contenido
❓ Preguntas Frecuentes
1. ¿Room reemplaza SQLite?
No, Room es una capa sobre SQLite que facilita su uso con menos código y más seguridad.
2. ¿Necesito conocer SQL?
Es útil pero no obligatorio. Room genera mucho código automáticamente.
3. ¿Qué es LiveData?
Un observable que actualiza la UI automáticamente cuando los datos cambian.
4. ¿Cómo migro entre versiones?
Define Migration objects o usa fallbackToDestructiveMigration() para desarrollo.

No hay comentarios:
Publicar un comentario