En este tutorial vamos a crear una base de datos Room para una App Android de ejemplo sobre listas de compras.
El objetivo es crear los componentes vistos en la introducción a Room con el fin de mostrar las listas de compras en un TextView
. E ilustrar el funcionamiento de la librería y sus componentes.
Puedes descargar el resultado final del tutorial desde el siguiente enlace:
Empecemos el desarrollo.
1. Crear Proyecto En Android Studio
- Abre Android Studio y crea un nuevo proyecto
- Selecciona una Empty Activity y presiona Next
- Nombra al aplicativo como Shopping List y presiona Finish.
2. Añadir Dependencias Gradle De Room
- Abre tu archivo build.gradle del módulo y agrega las siguiente dependencias.
dependencies {
implementation "androidx.appcompat:appcompat:$appCompatVersion"
// Room
implementation "androidx.room:room-runtime:$roomVersion"
annotationProcessor "androidx.room:room-compiler:$roomVersion"
// Lifecycle
implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
// UI
implementation "com.google.android.material:material:$materialVersion"
implementation "androidx.constraintlayout:constraintlayout:$contraintLayoutVersion"
// Testing
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
}
2. Ahora abre build.gradle del proyecto y crea un bloque llamado ext
con el número de las versiones:
ext {
appCompatVersion = '1.2.0'
roomVersion = '2.2.5'
lifecycleVersion = '2.2.0'
materialVersion = '1.2.1'
contraintLayoutVersion = '2.0.4'
junitVersion = '4.13.1'
espressoVersion = '3.3.0'
androidxJunitVersion = '1.1.2'
}
Puedes obtener el número de versión más reciente desde AndroidX releases.
3. Crear Una Entidad
La primera característica que vamos a desarrollar es ver las listas de compras que existen.
En este momento la entidad tiene una clave primaria y el nombre como se muestra en el siguiente esquema:
¿Cómo crear una entidad?
- Añade la clase
ShoppingList
que represente a las listas de compra. Cada atributo hará referencia a la columna en la tabla.
public class ShoppingList {
private final String mId;
private final String mName;
public ShoppingList(@NonNull String id, @NonNull String name) {
mId = id;
mName = name;
}
public String getId() {
return mId;
}
public String getName() {
return mName;
}
}
2. Actualiza el modelo con las siguientes anotaciones:
@Entity(tableName = "shopping_list")
public class ShoppingList {
@PrimaryKey
@NonNull
@ColumnInfo(name = "id")
private final String mId;
@NonNull
@ColumnInfo(name = "name")
private final String mName;
public ShoppingList(@NonNull String id, @NonNull String name) {
mId = id;
mName = name;
}
public String getId() {
return mId;
}
public String getName() {
return mName;
}
}
¿Cuál es el propósito de cada una?
@Entity
: Marca una clase para ser mapeada por Room como tabla. El nombre por defecto de la tabla será el de la clase. Para especificar uno distinto usa la propiedadtableName
.@PrimaryKey
: Marca un atributo como la clave primaria@ColumnInfo
: Permite la personalización de la columna asociada con este campo. El atributo name permite cambiar el nombre de la columna (por defecto es el nombre del campo)@NonNull
: Denota que un campo, parámetro o valor de retorno de un método no puede sernull
. Con ella Room añade la restricciónNOT NULL
a la columna.
4. Crear DAO
Un DAO (Data Access Object) u objeto de acceso a datos es la clase principal donde se definen las interacciones con tu base de datos.
Aunque podemos tener uno solo para todos los accesos, es recomendado crear uno por cada tabla operada.
Ahora bien, ¿qué acciones realizaremos en la tabla shopping_list
?
Por el momento:
- Insertar una lista de compras
- Obtener todas las listas de compras
Vamos a plasmar esto creando una interfaz (también puede ser una clase abstracta) con el nombre de ShoppingListDao
con el siguiente código:
@Dao
public interface ShoppingListDao {
@Query("SELECT * FROM shopping_list")
LiveData<List<ShoppingList>> getAll();
@Insert(onConflict = OnConflictStrategy.IGNORE)
void insert(ShoppingList shoppingList);
}
Analicemos las partes:
@Dao
: Marca la clase como un DAO@Insert
: Marca un método de un DAO como una operación de inserción. Pasamos como parámetro la entidad (o colección de esta). La propiedadonConflict
indica que hacer si existe un conflicto al insertar.IGNORE
ignora el conflicto y mantiene la fila existente.@Query
: Marca un método de un DAO como una consulta. Usamos unSELECT
para obtener una lista de las entidades consultadas.
Como ves, getAll()
retorna un LiveData. Esto se debe a que Room se las arregla para interactuar con este componente y enviarnos actualizaciones sobre cambios en el esquema de la tabla shopping_list
.
En los siguientes tutoriales iremos expandiendo este DAO según lo requiera la incorporación de nuevas funcionalidades.
5. Crear Base De Datos Room
Habíamos dicho en la introducción que RoomDatabase
es la clase base para todas las bases de datos en Room. Así que debes extender tu clase de base de datos de ella.
Esta provee métodos de acceso directo hacia las operaciones de la base de datos Room, sin embargo deberíamos preferir usar los DAOs para ello.
Crea una nueva clase abstracta llamada ShoppingListDatabase
y agrega el siguiente código:
@Database(entities = {ShoppingList.class}, version = 1, exportSchema = false)
public abstract class ShoppingListDatabase extends RoomDatabase {
// Exposición de DAOs
public abstract ShoppingListDao shoppingListDao();
private static final String DATABASE_NAME = "shopping-list-db";
private static ShoppingListDatabase INSTANCE;
private static final int THREADS = 4;
public static final ExecutorService dbExecutor = Executors.newFixedThreadPool(THREADS);
public static ShoppingListDatabase getInstance(final Context context) {
if (INSTANCE == null) {
synchronized (ShoppingListDatabase.class) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context.getApplicationContext(), ShoppingListDatabase.class,
DATABASE_NAME)
.build();
}
}
}
return INSTANCE;
}
}
Resaltemos las características:
@Database
: Marca la clase como una base de datos Room. Usamos las propiedades:entities
: La lista de entidades incluidas en la base de datosversion
: Versión de la base de datosexportSchema
: Le dice a Room que exporte el esquema
- Exposición de DAOs: Creamos un método
get*()
abstracto por cada DAO que tengamos. - Singleton: Usamos este patrón si queremos una sola instancia de la base de datos abierta (
INSTANCE
ygetInstance()
) - Hilos: Declaramos un
ExecutorService
para ejecutar las operaciones de bases de datos en otros hilos de trabajo y por ende no entorpecer la UI. - Creación de la base de datos: Pasamos al método
Room.databaseBuilder()
el contexto de la aplicación, el tipo de la clase de base de datos y el nombre de la base de datos. Al final usamosbuild()
y nuestra instancia se creará.
6. Crear Repositorio
El siguiente paso es crear la clase del repositorio de las listas de compras.
¿Qué debemos tener en cuenta?
- Declaramos la dependencia al DAO relacionado con las listas de compra
- Declaramos un campo para el LiveData de las listas de compra
- Añadimos un método por cada operación que nos interese realizar en la tabla (
insert()
ygetAllShoppingLists()
)
Crea la clase ShoppingListRepository
y pega el siguiente código:
public class ShoppingListRepository {
private final LiveData<List<ShoppingList>> mShoppingLists;
private final ShoppingListDao mShoppingListDao;
public ShoppingListRepository(Context context) {
ShoppingListDatabase db = ShoppingListDatabase.getInstance(context);
mShoppingListDao = db.shoppingListDao();
mShoppingLists = mShoppingListDao.getAll();
}
public LiveData<List<ShoppingList>> getAllShoppingLists() {
return mShoppingLists;
}
public void insert(ShoppingList shoppingList) {
ShoppingListDatabase.dbExecutor.execute(
() -> mShoppingListDao.insert(shoppingList)
);
}
}
En el constructor ordenaremos una carga automática de las listas de compra con el fin de recibir las notificaciones de datos lo más pronto posible.
De esta forma, Room envía a otro hilo de trabajo a las consultas con LiveData y se asegura de comunicar los cambios a la UI.
En el caso de la inserción debemos llamar al ExecutorService
para ejecutarla en su hilo correspondiente.
7. Crear ViewModel
Para crear el ViewModel nospreguntamos:
¿Cuáles son las interacciones que vendrán desde la interfaz?
En este momento solo vamos a insertar un par de registros programaticamente y los mostraremos en el TextView
que aparece en el layout creado automáticamente por la plantilla Empty Activity.
Por lo que observaremos la lista de listas de compras con un LiveData desde MainActivity
.
Esto significa que envolveremos a los métodos getAllShoppingLists()
e insert()
del repositorio en los del ViewModel
.
Teniendo en mente lo anterior, crea la clase ShoppingListViewModel
y materializa estas características:
public class ShoppingListViewModel extends AndroidViewModel {
private final ShoppingListRepository mRepository;
private final LiveData<List<ShoppingList>> mShoppingLists;
public ShoppingListViewModel(@NonNull Application application) {
super(application);
mRepository = new ShoppingListRepository(application);
mShoppingLists = mRepository.getAllShoppingLists();
}
public LiveData<List<ShoppingList>> getShoppingLists() {
return mShoppingLists;
}
public void insert(ShoppingList shoppingList) {
mRepository.insert(shoppingList);
}
}
En el constructor cargaremos todas las listas de compra para actualizar la vista inmediatamente.
8. Prepoblar La Base De Datos
Si queremos que la base de datos comience con registros predefinidos podemos hacer uso de RoomDatabase#Callback
.
Crea una callback en la clase de base de datos y sobrescribe su método onCreate()
para agrega 2 listas de compras:
// Prepoblar base de datos con callback
private static final RoomDatabase.Callback mRoomCallback = new Callback() {
@Override
public void onCreate(@NonNull SupportSQLiteDatabase db) {
super.onCreate(db);
dbExecutor.execute(() -> {
ShoppingListDao dao = INSTANCE.shoppingListDao();
ShoppingList list1 = new ShoppingList("1", "Lista de ejemplo");
ShoppingList list2 = new ShoppingList("2", "Banquete de Navidad");
dao.insert(list1);
dao.insert(list2);
});
}
};
Luego la añádela con addCallback()
en el builder.
INSTANCE = Room.databaseBuilder(
context.getApplicationContext(), ShoppingListDatabase.class,
DATABASE_NAME)
.addCallback(mRoomCallback)
.build();
9. Mostrar Datos En La Actividad
La actividad creada por defecto trae consigo un layout con un TextView
. Algo así:
<?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"
tools:context=".MainActivity">
<TextView
android:id="@+id/db_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Finalizando, conecta ese view con los datos de la tabla.
Es decir, Obtenemos el ViewModel
en el método onCreate()
y observamos su contenido con observe()
:
public class MainActivity extends AppCompatActivity {
private ShoppingListViewModel mViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView dbText = findViewById(R.id.db_text);
ViewModelProvider.AndroidViewModelFactory factory =
ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication());
mViewModel = new ViewModelProvider(this, factory)
.get(ShoppingListViewModel.class);
mViewModel.getShoppingLists().observe(this, shoppingLists -> {
StringBuilder sb = new StringBuilder();
for (ShoppingList list : shoppingLists) {
sb.append(list.getName()).append("\n");
}
dbText.setText(sb.toString());
}
);
}
}
Al usar un StringBuilder
podemos concatenar el nombre de cada lista y proyectarlo en el texto con setText()
.
Y para terminar, ejecuta el proyecto.
Deberías ver lo siguiente:
Ver siguiente tutorial: Insertar Datos Con Room