Proyecto de Android con Kotlin usando el patrón MVVM donde implemento algunas librerías de Android Jetpack como Navigation, Databinding, Room y pruebas con JUnit; para la creación de una aplicación sencilla donde se pueden crear, editar, eliminar y filtrar notas.
- Kotlin
- Android KTX version: '2.3.0'
- Room version: '2.2.5'
- Material version: '1.2.0'
- JUnit version: '4.12'
Este proyecto esta basado en la arquitectura MVVM (Model View ViewModel), tambien uso el patrón Repository Pattern
.
Para utilizar la librería en el proyecto tienen que agregar en el archivo build.gradle
de tu modulo lo siguiente en dependencies
:
apply plugin: 'kotlin-kapt'
dependencies {
...
// Room
implementation "androidx.room:room-runtime:2.2.5"
kapt "androidx.room:room-compiler:2.2.5"
...
}
Ahora procedemos a crear nuestro directorio model
donde vamos a crear nuestro archivo Note una vez creado el archivo
agregamos los atributos que tendra nuestro modelo como son noteId
, title
, content
, updatedAt
@Entity(tableName = "note_table")
data class Note(
@PrimaryKey(autoGenerate = true)
var noteId: Long,
@ColumnInfo(name = "title")
var title: String,
@ColumnInfo(name = "content")
var content: String,
@ColumnInfo(name = "updatedAt")
var updatedAt: Long,
) {
constructor() : this(0L, "", "", -1L)
}
Veamos qué hacen estas anotaciones:
-
@Entity(tableName = "note_table")
El@Entity
va a representar una tabla en SQLite. La cual para este caso le pasamos el nombre de la tabla si no queremos que se llame igual al modelo el cual nos quedatableName = "note_table"
y nos indica que nuestra tabla se llamaranote_table
. -
@PrimaryKey(autoGenerate = true)
El@PrimaryKey
indicamos que este valor sera nuestra clave primaria de la tabla y conautoGenerate = true
ques este valor sera generado automáticamente. -
@ColumnInfo(name = "content")
El@ColumnInfo
especifica el nombre de la columna en la tabla y conname = "content"
le damos un nombre a dicho elemento el cual puede ser diferente a nuestro atributo del modelo.
Y por ultimo un constructor que nos creara un elemento para hacer pruebas mas adelante constructor() : this(0L, "", "", -1L)
esta parte es opcional
DAO (data access object) especifica consultas SQL y las asocia con llamadas a métodos. El cual debe ser una interfaz o una clase abstracta.
Ahora procedemos a crear nuestro directorio database
donde vamos a crear nuestro Dao
el cual llamaremos NoteDatabaseDao
una vez creado el archivo
agregamos nuestras consultas que SQL que sera insertar, actualizar, eliminar, listar por ID y listar todas.
@Dao
interface NoteDatabaseDao {
@Insert
fun insert(note: Note)
@Update
fun update(note: Note)
@Query("SELECT * FROM note_table ORDER BY noteId DESC")
fun getAllNotes(): LiveData<List<Note>>
@Query("SELECT * FROM note_table WHERE noteId = :key")
fun get(key: Long): Note
@Delete
fun delete(note: Note)
}
Veamos qué hace esta interface:
@Dao
El@Dao
identifica esto como una clase DAO para ROOM.@Insert
El@Insert
es una anotación de método DAO especial la cual no necesita proporcionar ninguún SQL para realizar la operacion de insertar datos (También tenemos @Delete y @Update para eliminar y actualizar filas).@Query
El@Query
requiere que se le pase una consulta SQL como parametro lo cual se usa para consultas complejas y otras operaciones como en este caso@Query("SELECT * FROM note_table ORDER BY noteId DESC")
le indicamos que queremos todos los datos y que los ordene de manera descendente y en este@Query("SELECT * FROM note_table WHERE noteId = :key")
queremos que nos consulte una nota por id; para mas información consultar.
Ahora procedemos a crear en nuestro directorio database
el archivo NoteDatabase
una vez creado el archivo
agregamos la información correspondiente.
@Database(entities = [Note::class], version = 1, exportSchema = false)
abstract class NoteDatabase : RoomDatabase() {
abstract val noteDatabaseDao: NoteDatabaseDao
companion object {
@Volatile
private var INSTANCE: NoteDatabase? = null
fun getInstance(context: Context) : NoteDatabase {
synchronized(this) {
var instance = INSTANCE
if (instance == null) {
instance = Room.databaseBuilder(
context.applicationContext,
NoteDatabase::class.java,
"note_history_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
}
return instance
}
}
}
}
Veamos qué temenos:
- Nuestra clase debe ser
abstract
y extender deRoomDatabase
@Database
Al@Database
le agregamos las entidades que tendrá que en este caso es solo unaentities = [Note::class]
el cual recibe un array de las mismas, también le indicamos la versión que para este sera 1version = 1
y como no vamos a manejar migraciones dejamosexportSchema
enfalse
.- Implementamos singleton para evitar que se abran varias instancias de la base de datos al mismo tiempo el cual nos garantiza que siempre
tenemos la misma instancia de la Database la cual la obtendremos de
getInstance
.
Ya para finalizar y comprobar que todo este funcionando creamos nuestro test procedemos a crear nuestro archivo NoteDatabaseTest
en el directorio de nuestra app androidTest
@RunWith(AndroidJUnit4::class)
class NoteDatabaseTest {
private lateinit var noteDao: NoteDatabaseDao
private lateinit var db: NoteDatabase
@Before
fun createDb() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
db = Room.inMemoryDatabaseBuilder(context, NoteDatabase::class.java)
.allowMainThreadQueries()
.build()
noteDao = db.noteDatabaseDao
}
@After
@Throws(IOException::class)
fun closeDb() {
db.close()
}
@Test
@Throws(Exception::class)
fun insetAndGetNote() {
val note = Note()
noteDao.insert(note)
val toNote = noteDao.get(note.noteId)
assertEquals(toNote?.updatedAt, -1L)
}
}
Veamos qué temenos:
@RunWith
El@RunWith
nos indica que la prueba va a ser ejecutada conJUnit 4
@Before
En el@Before
creamos un método para inicializar la base de datos@After
En el@After
creamos un método para limpiar la base de datos usandodb.close()
@Test
En el@Test
creamos un método de prueba donde vamos a insertar una nota y consultarla.
Si recordamos en nuestro constructor del modelo Note
tenemos constructor() : this(0L, "", "", -1L)
por eso no le pasamos valores
cuando creamos la nota val note = Note()
luego procedemos a insertarla en la base de datos con noteDao.insert(note)
y luego con el
noteId
la consultamos val toNote = noteDao.get(note.noteId)
y con el método de junit
assertEquals
comprobamos que la nota consultada
tenga el mismo valor en el updatedAt
con el cual se creo assertEquals(toNote?.updatedAt, -1L)
y corremos el test.
Ahora procedemos a cambiar un valor en de -1L
a -6l
en assertEquals(toNote?.updatedAt, -6L)
y corremos el test el cual no debe pasar
Ya con esa comprobación procedemos a implementar el patrón Repository Pattern
y la creación de los ViewModel
para alimentar nuestras vistas.