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)
}

Obtén descuentos exclusivos de nuestros cursos en vivo en línea

Capacítate con los expertos

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 como vm.getNoteById() que retornen State/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í!

About Author

Dale Tapia

0 0 votos
Article Rating
Suscribir
Notificar de
guest
0 Comments
La mas nueva
Más antiguo Más votada
Comentarios.
Ver todos los comentarios
0
¿Te gusta este articulo? por favor comentax