Taller de Desarrollo Móvil en Android

Ingeniería de Software y Sistemas Computacionales | La Salle Nezahualcóyotl

Fisiología de la Arquitectura MVVM

Hasta la práctica anterior, nuestras actividades sufrían del antipatrón Massive Activity (hacían demasiadas cosas). Hoy separaremos el código en tres capas independientes para cumplir con el principio de responsabilidad única de la ingeniería de software.

1 Configuración de Ciclos de Vida (Gradle)

Para poder heredar de las clases de arquitectura de Google, necesitamos integrar las extensiones de ciclo de vida en nuestro motor de construcción.

📍 Archivo: Gradle Scripts > build.gradle.kts (Module :app)

Abre el archivo, localiza el bloque de dependencies { ... } y añade las siguientes líneas al final de la lista:

dependencies {
    ...
    // Extensiones de Arquitectura: ViewModel y LiveData
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
    implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.1")
    implementation("androidx.activity:activity-ktx:1.7.2")
}
¿Por qué hacemos esto? La librería activity-ktx nos proporcionará delegados de Kotlin como by viewModels(), permitiendo que la Activity se vincule al ViewModel respetando el ciclo de vida del sistema sin código repetitivo.

⚠️ Al terminar de escribir, presiona el botón "Sync Now" en la esquina superior derecha del IDE.

2 Re-estructuración Física de Paquetes

El orden en el sistema de archivos previene errores de importación y colisiones de alcance.

1. Ve al panel de la izquierda en modo Android.
2. Despliega la carpeta java hasta llegar a tu paquete principal: mx.lasalle.ciclovida.
3. Haz clic derecho sobre mx.lasalle.ciclovida y selecciona New > Package.
4. Crea tres sub-paquetes con los siguientes nombres exactos:

Nota de Mantenimiento: Si Android Studio te pregunta si deseas actualizar los import al arrastrar tus archivos anteriores (como MainActivity o UserAdapter) a estas nuevas carpetas, haz clic en "Yes" para que el IDE repare las referencias automáticamente.

3 Extracción de Datos: El Patrón Repository

El repositorio será la ventanilla única de datos. Aislará la configuración de Retrofit de toda la aplicación.

📍 Nuevo Archivo: app > java > mx.lasalle.ciclovida > data > MensajeRepository.kt

Crea este archivo dentro del paquete data y pega el siguiente código íntegro:

package mx.lasalle.ciclovida.data

import mx.lasalle.ciclovida.ApiService
import mx.lasalle.ciclovida.Mensaje
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.Response

class MensajeRepository {

    // Centralizamos la configuración del cliente HTTP
    private val retrofit = Retrofit.Builder()
        .baseUrl("http://10.118.137.27:8080")
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    private val apiService = retrofit.create(ApiService::class.java)

    // Ejecuta la consulta en el hilo de fondo que invoque el ViewModel
    suspend fun fetchMensajesFromServer(): Response<List<Mensaje>> {
        return apiService.consultarTodo()
    }

    suspend fun enviarMensajeAlServidor(user: String, pass: String, texto: String): Response<Void> {
        val payload = mapOf("mensaje" to texto)
        return apiService.publicar(user, pass, payload)
    }
}
¿Por qué hacemos esto? Si en el futuro decidimos cambiar la API externa por una base de datos local en SQLite (Room), la interfaz de usuario jamás se enterará, porque ella solo habla con este Repositorio.

4 El Cerebro Lógico: MensajeViewModel

El ViewModel procesará las respuestas de red y las mantendrá vivas en memoria, independientemente de si el teléfono se rota o cambia de orientación.

📍 Nuevo Archivo: app > java > mx.lasalle.ciclovida > viewmodel > MensajeViewModel.kt

Crea este archivo dentro del paquete viewmodel y pega el siguiente código:

package mx.lasalle.ciclovida.viewmodel

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mx.lasalle.ciclovida.data.MensajeRepository

class MensajeViewModel : ViewModel() {

    private val repository = MensajeRepository()

    // Encapsulamiento: Mutable localmente, Inmutable externamente
    private val _listaMensajes = MutableLiveData<List<String>>()
    val listaMensajes: LiveData<List<String>> get() = _listaMensajes

    private val _statusError = MutableLiveData<String>()
    val statusError: LiveData<String> get() = _statusError

    // Lógica asíncrona gestionada con el ciclo de vida del ViewModel
    fun cargarMensajes() {
        viewModelScope.launch(Dispatchers.IO) {
            try {
                val response = repository.fetchMensajesFromServer()
                if (response.isSuccessful) {
                    val nombresConMensaje = response.body()?.map { 
                        "${it.id_usuario}: ${it.mensaje}" 
                    } ?: emptyList()
                    
                    // Modificamos el valor en hilo de fondo de forma segura
                    _listaMensajes.postValue(nombresConMensaje)
                } else {
                    _statusError.postValue("Error en servidor: ${response.code()}")
                }
            } catch (e: Exception) {
                _statusError.postValue("Fallo de conexión a la red local")
            }
        }
    }
}
¿Por qué hacemos esto? Usamos viewModelScope. Si la Activity se destruye mientras la petición HTTP está viajando en el aire, la corrutina se cancela automáticamente, eliminando por completo los bloqueos de memoria.

5 Reactividad en la Interfaz (HomeActivity.kt)

Ahora limpiaremos la Activity para que deje de configurar hilos y conexiones. Su único trabajo será "observar" cuándo el ViewModel tiene datos listos.

📍 Archivo: app > java > mx.lasalle.ciclovida > ui > HomeActivity.kt

Sustituye por completo el código de tu archivo por esta versión limpia y reactiva:

package mx.lasalle.ciclovida.ui

import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewmodels
import androidx.appcompat.app.AppCompatActivity
import mx.lasalle.ciclovida.UserAdapter
import mx.lasalle.ciclovida.databinding.ActivityHomeBinding
import mx.lasalle.ciclovida.viewmodel.MensajeViewModel

class HomeActivity : AppCompatActivity() {

    private lateinit var binding: ActivityHomeBinding
    
    // Inyección consciente del ciclo de vida usando extensiones KTX
    private val mensajeViewModel: MensajeViewModel by viewmodels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityHomeBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // SUSCRIPCIÓN AL PATRÓN OBSERVER
        // Cada vez que 'listaMensajes' cambie su valor, este bloque se ejecutará solo
        mensajeViewModel.listaMensajes.observe(this) { mensajesObtenidos ->
            binding.rvUsuarios.adapter = UserAdapter(mensajesObtenidos)
        }

        // Observamos fallos del sistema o de red
        mensajeViewModel.statusError.observe(this) { mensajeDeError ->
            Toast.makeText(this, mensajeDeError, Toast.LENGTH_LONG).show()
        }

        // Disparamos la carga inicial de datos
        mensajeViewModel.cargarMensajes()
    }
}
Validación del Alumno: Observa que la Activity ya no contiene la URL del servidor, ni el objeto Retrofit, ni bloques CoroutineScope(Dispatchers.IO). Se ha convertido en una vista pura y desacoplada.

📝 Evaluación de Control Arquitectónico

1. Encapsulamiento de Datos:

En el paso 4 definimos una variable mutable privada (_listaMensajes) y una inmutable pública (listaMensajes). Desde el punto de vista de la robustez del software, ¿por qué es peligroso permitir que la Activity pueda modificar directamente el valor de un LiveData?

2. Persistencia en Memoria:

Ejecuta la aplicación en tu emulador, entra a la lista de mensajes y rota la pantalla del dispositivo (Ctrl + Flecha Izquierda). Revisa tu Logcat: ¿por qué la aplicación ya no vuelve a realizar la petición HTTP al servidor al cambiar de orientación?

💡 Hito del Laboratorio: Presenta el código de tu Activity limpio de configuraciones de red y demuestra al profesor que el flujo reactivo actualiza el RecyclerView de forma automática utilizando patrones observables.