Ingeniería de Software y Sistemas Computacionales | La Salle Nezahualcóyotl
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.
Para poder heredar de las clases de arquitectura de Google, necesitamos integrar las extensiones de ciclo de vida en nuestro motor de construcción.
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")
}
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.
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:
data (Aquí moveremos los modelos, la interfaz de Retrofit y el nuevo Repositorio).ui (Aquí moveremos las Activities y los Adapters).viewmodel (Aquí crearemos nuestros controladores lógicos).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.
El repositorio será la ventanilla única de datos. Aislará la configuración de Retrofit de toda la aplicación.
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)
}
}
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.
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")
}
}
}
}
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.
Ahora limpiaremos la Activity para que deje de configurar hilos y conexiones. Su único trabajo será "observar" cuándo el ViewModel tiene datos listos.
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()
}
}
CoroutineScope(Dispatchers.IO). Se ha convertido en una vista pura y desacoplada.
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.