SQLite con Room en Android

SQLite con Room en Android

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

  1. Abre Android Studio
  2. Copia el c贸digo
  3. Sync Gradle
  4. Presiona Run
  5. 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