Tus ideas, siempre a un toque: crea, organiza y guarda con Kotlin. ¡Haz clic aquí!
Qué vas a lograr
Una app simple pero completa que permite:
- Crear, listar, editar y borrar notas (CRUD).
- Guardar datos localmente con Room.
- UI moderna con Jetpack Compose.
- Arquitectura MVVM con Repository + ViewModel.
- Operaciones asíncronas con Kotlin Coroutines.
Requisitos
- Android Studio (Arctic Fox o superior recomendado)
- Kotlin 1.6+
- SDK Android 31+
- Conocimientos básicos de Kotlin y Android
1) Dependencias (Gradle – app/build.gradle.kts
o build.gradle
)
plugins {
id("com.android.application")
kotlin("android")
kotlin("kapt")
}
android {
compileSdk = 33
defaultConfig {
applicationId = "com.example.notes"
minSdk = 21
targetSdk = 33
versionCode = 1
versionName = "1.0"
}
buildFeatures { compose = true }
composeOptions { kotlinCompilerExtensionVersion = "1.4.7" }
}
dependencies {
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.activity:activity-compose:1.7.0")
implementation("androidx.compose.ui:ui:1.4.3")
implementation("androidx.compose.material:material:1.4.3")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
// Room
implementation("androidx.room:room-runtime:2.5.2")
kapt("androidx.room:room-compiler:2.5.2")
implementation("androidx.room:room-ktx:2.5.2")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
// Navigation (Compose)
implementation("androidx.navigation:navigation-compose:2.7.0")
}
2) Modelo de datos (Entity)
Note.kt
@Entity(tableName = "notes")
data class Note(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val title: String,
val content: String,
val updatedAt: Long = System.currentTimeMillis()
)
3) DAO (Data Access Object)
NoteDao.kt
@Dao
interface NoteDao {
@Query("SELECT * FROM notes ORDER BY updatedAt DESC")
fun getAllNotes(): Flow<List<Note>>
@Query("SELECT * FROM notes WHERE id = :id")
suspend fun getNoteById(id: Long): Note?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(note: Note): Long
@Update
suspend fun update(note: Note)
@Delete
suspend fun delete(note: Note)
}
4) Room Database
NotesDatabase.kt
@Database(entities = [Note::class], version = 1)
abstract class NotesDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao
companion object {
@Volatile private var INSTANCE: NotesDatabase? = null
fun getInstance(context: Context): NotesDatabase =
INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
context.applicationContext,
NotesDatabase::class.java,
"notes_db"
).build().also { INSTANCE = it }
}
}
}
5) Repositorio
NoteRepository.kt
class NoteRepository(private val dao: NoteDao) {
val notes: Flow<List<Note>> = dao.getAllNotes()
suspend fun getNote(id: Long) = dao.getNoteById(id)
suspend fun upsert(note: Note) = dao.insert(note)
suspend fun update(note: Note) = dao.update(note)
suspend fun delete(note: Note) = dao.delete(note)
}
6) ViewModel
NotesViewModel.kt
class NotesViewModel(private val repo: NoteRepository) : ViewModel() {
val notes: StateFlow<List<Note>> = repo.notes
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
fun addNote(title: String, content: String) = viewModelScope.launch {
val note = Note(title = title, content = content, updatedAt = System.currentTimeMillis())
repo.upsert(note)
}
fun updateNote(note: Note) = viewModelScope.launch { repo.update(note.copy(updatedAt = System.currentTimeMillis())) }
fun deleteNote(note: Note) = viewModelScope.launch { repo.delete(note) }
}
Para proporcionar el ViewModel con un ViewModelProvider.Factory
, agrega una fábrica simple que inyecte el repo (o usa Hilt si prefieres DI).
7) UI con Jetpack Compose (esqueleto)
MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val db = NotesDatabase.getInstance(applicationContext)
val repo = NoteRepository(db.noteDao())
val vmFactory = object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return NotesViewModel(repo) as T
}
}
val viewModel: NotesViewModel = ViewModelProvider(this, vmFactory).get(NotesViewModel::class.java)
setContent {
NotesApp(viewModel)
}
}
}
NotesApp.kt
(navegación simple)
@Composable
fun NotesApp(vm: NotesViewModel) {
val nav = rememberNavController()
NavHost(navController = nav, startDestination = "list") {
composable("list") { NotesListScreen(vm, onAdd = { nav.navigate("edit/0") }, onEdit = { id -> nav.navigate("edit/$id") }) }
composable("edit/{id}", arguments = listOf(navArgument("id") { type = NavType.LongType })) { backStack ->
val id = backStack.arguments?.getLong("id") ?: 0L
NoteEditScreen(vm, id, onDone = { nav.popBackStack() })
}
}
}
NotesListScreen.kt
@Composable
fun NotesListScreen(vm: NotesViewModel, onAdd: () -> Unit, onEdit: (Long) -> Unit) {
val notes by vm.notes.collectAsState()
Scaffold(
topBar = { TopAppBar(title = { Text("Notas") }) },
floatingActionButton = { FloatingActionButton(onClick = onAdd) { Icon(Icons.Default.Add, contentDescription = "Agregar") } }
) {
LazyColumn(modifier = Modifier.fillMaxSize().padding(it)) {
items(notes) { note ->
NoteItem(note = note, onClick = { onEdit(note.id) }, onDelete = { vm.deleteNote(note) })
}
}
}
}
@Composable
fun NoteItem(note: Note, onClick: () -> Unit, onDelete: () -> Unit) {
Card(modifier = Modifier.fillMaxWidth().padding(8.dp).clickable { onClick() }) {
Row(modifier = Modifier.padding(12.dp), horizontalArrangement = Arrangement.SpaceBetween) {
Column(modifier = Modifier.weight(1f)) {
Text(text = note.title, style = MaterialTheme.typography.h6)
Text(text = note.content, maxLines = 2, style = MaterialTheme.typography.body2)
}
IconButton(onClick = onDelete) { Icon(Icons.Default.Delete, contentDescription = "Delete") }
}
}
}
NoteEditScreen.kt
@Composable
fun NoteEditScreen(vm: NotesViewModel, noteId: Long, onDone: () -> Unit) {
val scope = rememberCoroutineScope()
var title by rememberSaveable { mutableStateOf("") }
var content by rememberSaveable { mutableStateOf("") }
LaunchedEffect(noteId) {
if (noteId != 0L) {
vm.viewModelScope.launch {
vm.repo.getNote(noteId)?.let {
title = it.title
content = it.content
}
}
}
}
Scaffold(topBar = { TopAppBar(title = { Text(if (noteId == 0L) "Nueva nota" else "Editar nota") }) },
floatingActionButton = {
FloatingActionButton(onClick = {
if (title.isNotBlank() || content.isNotBlank()) {
scope.launch {
vm.addNote(title, content)
onDone()
}
} else onDone()
}) { Icon(Icons.Default.Check, contentDescription = "Guardar") }
}) { padding ->
Column(modifier = Modifier.padding(16.dp).fillMaxSize()) {
OutlinedTextField(value = title, onValueChange = { title = it }, label = { Text("Título") }, modifier = Modifier.fillMaxWidth())
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(value = content, onValueChange = { content = it }, label = { Text("Contenido") }, modifier = Modifier.fillMaxSize())
}
}
}
Nota: En
NoteEditScreen
hemos usado un acceso simplificado al repositorio a través del ViewModel. Para mantener separación más estricta, crea funciones comovm.getNoteById()
que retornenState
/Flow
.
8) Permisos y Manifest
AndroidManifest.xml
solo necesita declarar MainActivity
. No hay permisos especiales para este ejemplo local.
9) Pruebas rápidas (manual)
- Ejecuta la app en un emulador.
- Crea varias notas, edítalas y bórralas.
- Verifica que las notas persistan entre ejecuciones (Room).
10) Mejoras sugeridas (próximos pasos)
- Añadir búsqueda y filtrado.
- Ordenar por título o fecha con opciones de UI.
- Soporte para sincronización en la nube (Firebase o backend propio).
- Añadir etiquetas / categorías.
- Implementar backup/export (JSON).
- Mejorar validaciones y manejo de errores.
- Implementar UI más elaborada (Material3, animaciones).
Conclusión
Con este esqueleto tienes una app de notas completa y moderna: persiste datos con Room, usa Compose para una UI rápida y limpia, y organiza la lógica con MVVM. Si quieres, te genero el proyecto completo listo para importar en Android Studio (zipped), o te lo adapto para usar Hilt en la inyección de dependencias o Material3 en la interfaz.
Tus ideas, siempre a un toque: crea, organiza y guarda con Kotlin. ¡Haz clic aquí!