Aplicación NotesApp en Kotlin

App en Kotlin que se comunica con el API Rest de notas creado en el VPS laravel.alumno.me

¿Esquema de la red?

Aplicación en Kotlin

Crear el proyecto NotesAppWeb

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.NotesAppWeb"
        tools:targetApi="31">

        <activity
            android:name=".LoginActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity
            android:name=".RegisterActivity"
            android:exported="false"
            android:windowSoftInputMode="adjustNothing"
            android:parentActivityName=".LoginActivity">
        </activity>

        <activity
            android:name=".MainActivity"
            android:exported="false"
            android:parentActivityName=".LoginActivity">
        </activity>

    </application>

</manifest>

 

lib.versions.toml

[versions]
agp = "8.9.3"
kotlin = "2.0.21"
coreKtx = "1.16.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
appcompat = "1.7.0"
material = "1.12.0"
activity = "1.10.1"
constraintlayout = "2.2.1"

datastorePreferences = "1.1.7"
kotlinxCoroutinesCore = "1.10.2"
kotlinxCoroutinesAndroid = "1.10.2"
lifecycleCommonJava11 = "2.9.0"
okhttp = "4.12.0"
retrofit = "3.0.0"

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }

androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleCommonJava11" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

buid.gradle (del módulo)

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
}

android {
    namespace = "com.example.notesappweb"
    compileSdk = 35

    defaultConfig {
        applicationId = "com.example.notesappweb"
        minSdk = 28
        targetSdk = 35
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildFeatures {
        viewBinding = true
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
    kotlinOptions {
        jvmTarget = "11"
    }
}

dependencies {

    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.appcompat)
    implementation(libs.material)
    implementation(libs.androidx.activity)
    implementation(libs.androidx.constraintlayout)

    implementation(libs.androidx.datastore.preferences)

    implementation (libs.androidx.lifecycle.viewmodel.ktx)

    // implementation ("com.squareup.retrofit2:retrofit:3.0.0")
    // implementation ("com.squareup.retrofit2:converter-gson:3.0.0")
    implementation(libs.okhttp)
    implementation (libs.retrofit)
    implementation (libs.converter.gson)

    implementation(libs.kotlinx.coroutines.core)
    implementation (libs.kotlinx.coroutines.android)

    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
}

model/Login

data class Login(
    val token: String,
    val name: String
)

model/LoginResponse

data class LoginResponse(
    @SerializedName("success")
    var success: Boolean,
    @SerializedName("data")
    var login: Login,
    @SerializedName("message")
    var message: String
)

model/Note

data class Note(
    @SerializedName("id")
    val id: Int,
    @SerializedName("title")
    val title: String,
    @SerializedName("description")
    val description: String,
    @SerializedName("user_id")
    val user_id: Int,
    /*
    @SerializedName("created_at")
    val created_at: Timestamp,
    @SerializedName("updated_at")
    val updated_at: Timestamp
     */
) : Serializable

model/NoteResponse

data class NoteResponse(
    @SerializedName("success")
    var success: Boolean,
    @SerializedName("data")
    var note: Note?,
    @SerializedName("message")
    var message: String
)

model/NotesResponse

data class NotesResponse(
    @SerializedName("success")
    var success: Boolean,
    @SerializedName("data")
    var notes: ArrayList<Note>,
    @SerializedName("message")
    var message: String
)

 

network/ApiService

interface ApiService {

    @POST("api/login")
    @FormUrlEncoded
    suspend fun login(
        @Field("email") email: String,
        @Field("password") password: String
    ) : Response<LoginResponse>

    @POST("api/register")
    @FormUrlEncoded
    suspend fun register(
        @Field("name") name: String,
        @Field("email") email: String,
        @Field("password") password: String,
        @Field("confirm_password") confirmPassword: String
    ) : Response<LoginResponse>

    @POST("api/logout")
    suspend fun logout() : Response<LogoutResponse>

    @Headers(
        "Accept: application/json",
    )
    @GET("api/note")
    suspend fun getNotes(): Response<NotesResponse>

    @Headers(
        "Accept: application/json",
    )
    @POST("api/note")
    @FormUrlEncoded
    suspend fun addNote(
        @Field("title") title: String,
        @Field("description") description: String
    ): Response<NoteResponse>

    @Headers(
        "Accept: application/json",
    )
    @PUT("api/note/{id}")
    @FormUrlEncoded
    suspend fun updateNote(
        @Path("id") id: Int,
        @Field("title") title: String,
        @Field("description") description: String
    ): Response<NoteResponse>

    @Headers(
        "Accept: application/json",
    )
    @DELETE("api/note/{id}")
    suspend fun deleteNote(
        @Path("id") id: Int
    ): Response<DeleteResponse>

}

network/ApiAdapter

object ApiAdapter {
    private var API_SERVICE: ApiService? = null
    private const val BASE_URL = "https://laravel.alumno.me/"
    public var API_TOKEN: String = ""

    @get:Synchronized
    val instance: ApiService?
        get() {
            if (API_SERVICE == null) {
                val client = OkHttpClient.Builder().addInterceptor(Interceptor { chain ->
                    val request = chain.request().newBuilder().addHeader("Authorization", "Bearer ${API_TOKEN}").build()

                    chain.proceed(request)
                }).build()

                val retrofit = Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .client(client)
                    .build()

                API_SERVICE = retrofit.create(ApiService::class.java)
            }
            return API_SERVICE
        }
}

network/ApiHelper

class ApiHelper(private val apiService: ApiService) {

    suspend fun login(email: String, password: String) : LoginResponse {
        return withContext(Dispatchers.IO) {
            val response = apiService.login(email, password)
            response.body() ?: LoginResponse(false, Login("", ""), "")
        }
    }

    suspend fun register(name: String, email: String, password: String, confirmPassword: String) : LoginResponse {
        return withContext(Dispatchers.IO) {
            val response = apiService.register(name, email, password, confirmPassword)
            response.body() ?: LoginResponse(false, Login("", ""), "")
        }
    }

    suspend fun logout() : LogoutResponse {
        return withContext(Dispatchers.IO) {
            val response = apiService.logout()
            response.body() ?: LogoutResponse(false, Logout(""), "")
        }
    }

    suspend fun getNotes(): NotesResponse {
        return withContext(Dispatchers.IO) {
            val response = apiService.getNotes()
            response.body() ?: NotesResponse(false, arrayListOf(), "")
        }
    }

    suspend fun addNote(note: Note): NoteResponse {
        return withContext(Dispatchers.IO) {
            val response = apiService.addNote(note.title, note.description)
            response.body() ?: NoteResponse(false, null, "")
        }
    }

    suspend fun updateNote(note: Note): NoteResponse {
        return withContext(Dispatchers.IO) {
            val response = apiService.updateNote(note.id, note.title, note.description)
            response.body() ?: NoteResponse(false, null, "")
        }
    }

    suspend fun deleteNote(note: Note): DeleteResponse {
        return withContext(Dispatchers.IO) {
            val response = apiService.deleteNote(note.id)
            response.body() ?: DeleteResponse(false, arrayListOf(), "")
        }
    }

}

repository/NoteRepository

class NoteRepository (private val apiHelper: ApiHelper) {
    suspend fun login(email: String, password: String) =
        apiHelper.login(email, password)

    suspend fun register(name: String, email: String, password: String, confirmPassword: String) =
        apiHelper.register(name, email, password, confirmPassword)

    suspend fun logout() = apiHelper.logout()

    suspend fun getNotes() = apiHelper.getNotes()

    suspend fun addNote(note: Note) = apiHelper.addNote(note)

    suspend fun updateNote(note: Note) = apiHelper.updateNote(note)

    suspend fun deleteNote(note: Note) = apiHelper.deleteNote(note)

}

viewmodel/MainActivityViewModel

class MainActivityViewModel(application: Application) : AndroidViewModel(application) {

    private val _notes: MutableLiveData<List<Note>> = MutableLiveData()
    val notes: LiveData<List<Note>> get() = _notes

    private var repository: NoteRepository = NoteRepository(ApiHelper(instance!!))

    suspend fun getNotes(): NotesResponse {
        val notesResponse = repository.getNotes()
        _notes.value = notesResponse.notes

        return notesResponse
    }

    suspend fun addNote(note: Note): Boolean {
        val noteResponse = repository.addNote(note)

        if (noteResponse.success) {
            // Añadimos la nota a la lista, y reasignamos el valor de LiveData para que se actualice
            //add note to _notes
            _notes.value = _notes.value?.toMutableList()?.apply {
                add(noteResponse.note!!)
            }
        }

        return noteResponse.success
    }

    suspend fun updateNote(note: Note, newNote: Note): Boolean {
        val noteResponse = repository.updateNote(newNote)

        if (noteResponse.success) {

            // Reasignamos el valor de LiveData para que se actualice
            _notes.value = _notes.value.orEmpty().map {
                if (it == note) newNote else it
            }

            _notes.value = _notes.value
        }

        return noteResponse.success
    }

    suspend fun deleteNote(note: Note): Boolean {
        val noteResponse = repository.deleteNote(note)

        if (noteResponse.success) {
            // Reasignamos el valor de LiveData para que se actualice
            _notes.value = _notes.value?.toMutableList()?.apply {
                remove(note)
            }
        }

        return noteResponse.success
    }

    suspend fun logout() : Boolean {
        val logoutResponse = repository.logout()
        return logoutResponse.success
    }
}

LoginActivity

class LoginActivity : AppCompatActivity() {

    private lateinit var binding: ActivityLoginBinding
    private var repository: NoteRepository = NoteRepository(ApiHelper(ApiAdapter.instance!!))

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityLoginBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Cargamos los datos de usuario
        getLogin()

        binding.btLogin.setOnClickListener {
            toggleUI(false)

            lifecycleScope.launch {
                val email = binding.etEmail.text.toString()
                val password = binding.etPassword.text.toString()

                // Validamos que todos los datos del login estén correctos
                if (validateLogin(email, password)) {
                    val response = repository.login(email, password)

                    if (response.success) {
                        withContext(Dispatchers.Main) {
                            toggleUI(true)
                        }

                        // Guardamos los datos del usuario
                        saveLogin(email, password)

                        // Lanzamos la actividad principal
                        val mainActivityIntent = Intent(this@LoginActivity, MainActivity::class.java)
                        mainActivityIntent.putExtra("API_TOKEN", response.login.token)
                        startActivity(mainActivityIntent)
                        finish()
                    } else {
                        withContext(Dispatchers.Main) {
                            val alertDialog: AlertDialog =
                                AlertDialog.Builder(this@LoginActivity).create().apply {
                                    setTitle("Error")
                                    setMessage("Error al iniciar sesión: " + response.message.ifEmpty { "No se ha podido iniciar sesión" })
                                    setButton(
                                        AlertDialog.BUTTON_POSITIVE,
                                        "Aceptar"
                                    ) { dialog, _ ->
                                        dialog.dismiss()
                                    }
                                }

                            alertDialog.show()

                            toggleUI(true)
                        }
                    }
                }
            }
        }

        binding.btRegister.setOnClickListener {
            startActivity(Intent(this@LoginActivity, RegisterActivity::class.java))
        }
    }

    private fun toggleUI(state: Boolean) {
        binding.btLogin.isEnabled = state
        binding.btRegister.isEnabled = state
    }

    private suspend fun saveLogin(email: String, password: String) {
        dataStore.edit { preferences ->
            preferences[booleanPreferencesKey("rememberLogin")] = binding.cbRememberLogin.isChecked

            if (binding.cbRememberLogin.isChecked) {
                preferences[stringPreferencesKey("email")] = email
                preferences[stringPreferencesKey("password")] = password
            } else {
                preferences.remove(stringPreferencesKey("email"))
                preferences.remove(stringPreferencesKey("password"))
            }
        }
    }

    private fun getLogin() {
        runBlocking {
            dataStore.data.map { preferences ->
                binding.cbRememberLogin.isChecked =
                    preferences[booleanPreferencesKey("rememberLogin")] ?: false

                if (binding.cbRememberLogin.isChecked) {
                    binding.etEmail.setText(preferences[stringPreferencesKey("email")].orEmpty())
                    binding.etPassword.setText(preferences[stringPreferencesKey("password")].orEmpty())
                }
            }.first()
        }
    }

    private fun validateLogin(email: String, password: String): Boolean {
        var loginValidated = true

        // Comprobamos si hay campos vacios
        if (email.isEmpty() || password.isEmpty()) {
            loginValidated = false

            showMessage("No puedes dejar campos sin rellenar")

            if (email.isEmpty())
                binding.etEmail.error = "El email no puede estar vacio"
            if (password.isEmpty())
                binding.etPassword.error = "La contraseña no puede estar vacia"

            toggleUI(true)
        }

        // Comprobamos si el email cumple el estandar en regex
        if (loginValidated && !Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
            loginValidated = false

            showMessage("El email no es válido")

            binding.etEmail.error = "El email no es válido"
            toggleUI(true)
        }

        return loginValidated
    }

    private fun showMessage(message: String) {
        Toast.makeText(this@LoginActivity, message, Toast.LENGTH_SHORT).show()
    }
}

RegisterActivity

class RegisterActivity: AppCompatActivity() {

    private lateinit var binding: ActivityRegisterBinding
    private var repository: NoteRepository = NoteRepository(ApiHelper(ApiAdapter.instance!!))

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityRegisterBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.btRegistro.setOnClickListener {
            binding.btRegistro.isEnabled = false

            lifecycleScope.launch {
                val name = binding.etTitle.text.toString()
                val email = binding.etEmail.text.toString()
                val password = binding.etPassword.text.toString()
                val confirmPassword = binding.etConfirmPassword.text.toString()

                // Validamos que todos los datos estén correctos
                if (validateRegister(name, email, password, confirmPassword)) {
                    val response = repository.register(
                        name,
                        email,
                        password,
                        confirmPassword
                    )

                    if (response.success) {
                        withContext(Dispatchers.Main) {
                            binding.btRegistro.isEnabled = true
                        }

                        val mainActivityIntent = Intent(this@RegisterActivity, MainActivity::class.java)
                        mainActivityIntent.putExtra("API_TOKEN", response.login.token)
                        startActivity(mainActivityIntent)
                        finish()
                    } else {
                        withContext(Dispatchers.Main) {
                            val alertDialog: AlertDialog =
                                AlertDialog.Builder(this@RegisterActivity).create().apply {
                                    setTitle("Error")
                                    setMessage("Error al registrarse")
                                    setButton(
                                        AlertDialog.BUTTON_POSITIVE,
                                        "Aceptar"
                                    ) { dialog, _ ->
                                        dialog.dismiss()
                                    }
                                }

                            alertDialog.show()

                            binding.btRegistro.isEnabled = true
                        }
                    }
                }
            }
        }
    }

    private fun validateRegister(
        name: String,
        email: String,
        password: String,
        confirmationPassword: String
    ): Boolean {
        var registerValidated = true

        // Comprobamos que todos los campos estén rellenos
        if (name.isEmpty() || email.isEmpty() || password.isEmpty() || confirmationPassword.isEmpty()) {
            registerValidated = false

            if (name.isEmpty()) binding.etTitle.error = "El nombre de usuario no puede estar vacio"
            if (email.isEmpty()) binding.etEmail.error = "El email no puede estar vacio"
            if (password.isEmpty()) binding.etPassword.error = "La contraseña no puede estar vacia"
            if (confirmationPassword.isEmpty()) binding.etConfirmPassword.error =
                "La confirmación de contraseña no puede estar vacia"

            showMessage( "No se puede registrar. Hay campos sin rellenar")

            binding.btRegistro.isEnabled = true
        }

        if (registerValidated && !Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
            registerValidated = false

            showMessage("El email no es válido")

            binding.etEmail.error = "El email no es válido"
            binding.btRegistro.isEnabled = true
        }

        if (registerValidated && password != confirmationPassword) {
            registerValidated = false

            showMessage("Las contraseñas no coinciden")

            binding.etConfirmPassword.error = "Las contraseñas no coinciden"
            binding.btRegistro.isEnabled = true
        }

        return registerValidated
    }

    private fun showMessage(message: String) {
        Toast.makeText(this@RegisterActivity, message, Toast.LENGTH_SHORT).show()
    }
}

adapter/NoteAdapter

class NoteAdapter() : RecyclerView.Adapter<NoteAdapter.NoteViewHolder>() {

    var notesList: List<Note> = emptyList()

    var onItemClick: ((Note) -> Unit)? = null
    var onLongItemClick: ((Note) -> Unit)? = null

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder {
        val itemBinding =
            NoteItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return NoteViewHolder(itemBinding)
    }

    override fun getItemCount(): Int = notesList.size

    override fun onBindViewHolder(holder: NoteViewHolder, position: Int) {
        val item = notesList[position]
        holder.render(item)
    }

    fun getItem(position: Int): Note {
        return notesList.get(position)
    }

    inner class NoteViewHolder(binding: NoteItemBinding) :
        RecyclerView.ViewHolder(binding.root) {
        val title = binding.tvTitle
        val description = binding.tvDescription

        init {
            itemView.setOnClickListener {
                onItemClick?.invoke(notesList[layoutPosition])
            }

            itemView.setOnLongClickListener {
                onLongItemClick?.invoke(notesList[layoutPosition])
                false
            }
        }

        fun render(note: Note) {
            title.text = note.title
            description.text = note.description
        }
    }
}

MainActivity

class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding
    private lateinit var viewModel: MainActivityViewModel
    private lateinit var adapter: NoteAdapter

    private var backPressedOnce = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        ApiAdapter.API_TOKEN = intent.getStringExtra("API_TOKEN").toString()

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        viewModel = ViewModelProvider(this)[MainActivityViewModel::class.java]

        setSupportActionBar(binding.toolbar)

        initRecyclerView()

        binding.fab.setOnClickListener {
            addNote(this)
        }

        // Confirmación para salir de la actividad
        onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                if (backPressedOnce) {
                    logout()
                } else {
                    backPressedOnce = true
                    Toast.makeText(this@MainActivity, "Presiona de nuevo para cerrar sesión", Toast.LENGTH_SHORT).show()

                    Handler(Looper.getMainLooper()).postDelayed(Runnable { backPressedOnce = false }, 2000)
                }
            }
        })
    }

    private fun initRecyclerView() {
        binding.recyclerNotes.layoutManager = LinearLayoutManager(this)

        getNotes()

        val itemSwipe = object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) {
            override fun onMove(
                recyclerView: RecyclerView,
                viewHolder: RecyclerView.ViewHolder,
                target: RecyclerView.ViewHolder
            ): Boolean {
                return false
            }

            override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
                if (binding.recyclerNotes.isEnabled) {
                    deleteDialog(viewHolder)
                } else {
                    adapter.notifyItemChanged(viewHolder.layoutPosition)
                }
            }
        }
        val swap = ItemTouchHelper(itemSwipe)
        swap.attachToRecyclerView(binding.recyclerNotes)
    }

    private fun getNotes() {
        lifecycleScope.launch {
            val response = viewModel.getNotes()

            if (response.success) {
                adapter = NoteAdapter()
                binding.recyclerNotes.adapter = adapter

                adapter.onLongItemClick = {
                    editNote(it, this@MainActivity)
                }

                // Cuando los datos de LiveData cambian, reasignamos la lista, ordenada por id,
                // falta usar DiffUtil para actualizar el RecyclerView en lugar de notifyDataSetChanged
                viewModel.notes.observe(this@MainActivity) { notesList ->
                    adapter.notesList = notesList.sortedWith(compareBy({ it.id }))
                    adapter.notifyDataSetChanged()
                }
            } else {
                withContext(Dispatchers.Main) {
                    showMessage("No se han podido cargar las notas")
                }
            }
        }
    }

    private fun updateNotes() {
        lifecycleScope.launch {
            if (viewModel.getNotes().success) {
                showMessage("Notas actualizadas")
            } else {
                showMessage("Error al actualizar las notas")
            }
        }
    }

    private fun addNote(context: Context) {
        val dialogBinding: AddNoteBinding = AddNoteBinding.inflate(LayoutInflater.from(context))

        val title = dialogBinding.etTitle
        val description = dialogBinding.etTDescription

        val addDialog = AlertDialog.Builder(context)

        addDialog.setView(dialogBinding.root)
        addDialog.setTitle("Añadir nota")
        addDialog.setPositiveButton("Añadir") { dialog, _ ->
            val titleNote = title.text.toString()
            val descriptionNote = description.text.toString()

            if (validateNote(titleNote, descriptionNote)) {
                val note = Note(
                    0,
                    titleNote,
                    descriptionNote,
                    0,
                )

                lifecycleScope.launch {
                    if (viewModel.addNote(note)) {
                        showMessage("Se ha añadido la nota")
                    } else {
                        showMessage("No se ha podido añadir la nota")
                    }
                }
            }
        }

        addDialog.setNegativeButton("Cancelar") { dialog, _ ->
            dialog.dismiss()
            showMessage("Cancelado")

        }
        addDialog.create()
        addDialog.show()
    }

    private fun editNote(note: Note, context: Context) {
        val dialogBinding: AddNoteBinding = AddNoteBinding.inflate(LayoutInflater.from(context))

        val titleNote = dialogBinding.etTitle
        val descriptionNote = dialogBinding.etTDescription

        titleNote.setText(note.title)
        descriptionNote.setText(note.description)

        val editDialog = AlertDialog.Builder(context)

        editDialog.setView(dialogBinding.root)
        editDialog.setTitle("Editar nota")
        editDialog.setPositiveButton("Editar") { dialog, _ ->
            val title = titleNote.text.toString()
            val description = descriptionNote.text.toString()

            if (validateNote(title, description)) {
                val newNote = Note(note.id, title, description, note.user_id)

                lifecycleScope.launch {
                    if (viewModel.updateNote(note, newNote)) {
                        showMessage("Se ha editado la nota")
                        dialog.dismiss()
                    } else
                        showMessage("No se ha podido editar la nota")
                }
            }
        }

        editDialog.setNegativeButton("Cancelar") { dialog, _ ->
            dialog.dismiss()
            showMessage("Cancelado")

        }
        editDialog.create()
        editDialog.show()
    }

    private fun deleteDialog(viewHolder: RecyclerView.ViewHolder) {
        val builder = AlertDialog.Builder(this)
        builder.setTitle("Eliminar Nota")
        builder.setMessage("¿Estás seguro de eliminar esta nota de la lista?")
        builder.setPositiveButton("Aceptar") { _, _ ->
            lifecycleScope.launch {
                if (viewModel.deleteNote(adapter.getItem(viewHolder.layoutPosition))) {
                    showMessage("La nota se ha eliminado correctamente")
                } else
                    showMessage("No se ha podido eliminar la nota")
            }
        }
        builder.setNegativeButton("Cancelar") { _, _ ->
            val position = viewHolder.adapterPosition
            adapter.notifyItemChanged(position)
        }
        builder.show()
    }

    // Menu options
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.menu_items, menu)

        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            R.id.action_refresh -> {
                updateNotes()
            }

            R.id.action_logout -> {
                logout()
            }

            else -> {
                super.onOptionsItemSelected(item)
            }
        }

        return true
    }

    private fun logout() {
        lifecycleScope.launch {
            if (viewModel.logout()){
                startActivity(Intent(this@MainActivity, LoginActivity::class.java))
                finish()
            } else
                withContext(Dispatchers.Main) {
                    showMessage("Error al cerrar la sesión")
            }
        }
    }

    private fun validateNote(title: String, description: String) : Boolean {
        var validated = true

        if (title.isEmpty() || description.isEmpty() ) {
            validated = false
            showMessage("No se ha validado la nota. Hay campos sin rellenar")
        }

        return validated
    }

    private fun showMessage(message: String) {
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
    }
}

 

Repositorio en GitHub

 

Mejoras:

– Usar DiffUtil en el adapter

Usar fragments (HomeFragment, AddFragment y EditFragment) y navegación entre ellos en la interface de usuario

 

Tarea presencial de la unidad 6 de HLC

Deja una respuesta