Actualiza tu vista con el patrón Observer

Seguro que más de una vez, al encontrarnos ante la necesidad de saber si los datos han cambiado, hemos optado por lanzar una Activity y esperar su resultado (startActivityForResult()), o por actualizar el contenido siempre que la pantalla pasa a primer plano (onResume()). Estas, aunque válidas, no son opciones del todo eficientes: la primera, si el flujo de pantallas aumenta, nos exigirá mantener el estado a través de una cadena de llamadas; por otro lado, la segunda opción nos llevará en muchos casos a ejecutar un refresco de la información incluso aunque nada haya cambiado.

En este artículo hablaré de cómo el patrón Observer puede ayudarnos a resolver este problema de una forma más óptima y elegante. Como complemento he subido a GitHub una sencilla app de contactos en la que utilizo este patrón para mantener la lista actualizada.

Uno observa, otro notifica

El patrón Observer es un patrón de comportamiento que nos ayuda a conseguir que la información de nuestro proyecto sea consistente a través de sus diversas representaciones. Esto básicamente significa que, si dicha información cambia, todos los objetos que dependan de ella deberían ser notificados.

Los dos elementos clave del patrón Observer son el Observer y el Observable:

  • Llamamos Observable al objeto que tiene cualquier número de observers dependientes de él y que se encarga de gestionar la información, notificando a los observers cada vez que esta cambia.

interface Observable {  
  fun subscribe(observer: Observer)  
  fun unsubscribe(observer: Observer)  
  fun notifyObservers()
}

  • El Observer, por su parte, se limita a suscribirse al Observable y a actuar cada vez que se le notifica que la información ha cambiado.

interface Observer {  
  fun update(contacts: List<Contact>)
}

El que notifica

En mi aplicación de ejemplo he decidido que sea mi clase Repository la que ejerza el papel de Observable concreto.

interface Repository: Observable {  
  fun getContacts(): List<Contact>  
  fun getContact(name: String): Contact  
  fun addContact(contact: Contact)  
  fun deleteContact(name: String)
}

Es en esta clase donde valido qué acciones considero dignas de ser notificadas. En mi caso, tanto al añadir como al eliminar un elemento mi lista se ve alterada, por lo tanto, necesitaré informar a cualquiera que esté observando que los datos han cambiado.

class RepositoryImpl @Inject constructor(): Repository {

  private val contacts = createContacts()

  private val observers = mutableListOf<Observer>()

  override fun getContacts(): List<Contact> {  
    return contacts.values.toList()
  }
  override fun getContact(name: String): Contact {  
    return contacts[name]!!
  }

  override fun addContact(contact: Contact) {  
    contacts[contact.name] = contact  
    notifyObservers()
  }

  override fun deleteContact(name: String) {  
    contacts.remove(name)  
    notifyObservers()
  }

  override fun subscribe(observer: Observer) {  
    observers.add(observer)
  }

  override fun unsubscribe(observer: Observer) {  
    observers.remove(observer)
  }

  override fun notifyObservers() {  
    for(observer in observers) {    
      observer.update(contacts.values.toList())  
    }  
  }

  private fun createContacts(): MutableMap<String, Contact> {  
    //Create contact list...
  }

}

El que observa

En mi proyecto de ejemplo he utilizado MVP para estructurar el código. En este caso parece bastante evidente que el Presenter ejercerá el papel de Observer concreto, ya que será el que gestione la lógica de presentación y el que tenga una instancia de la lista de datos a representar.

class ListPresenter(listView: ListView, private val repository: Repository): Observer {

  private val view: WeakReference<ListView> = WeakReference(listView)

  private var items = emptyList<Contact>()

  fun viewReady(){
    repository.subscribe(this)
    val contacts = repository.getContacts()
    saveItems(contacts)
    view.get()?.refreshList()
  }

  fun getItemsCount(): Int {
    return if(!listIsEmpty()) items.size else 0
  }

  fun configureCell(cellView: CellView, position: Int) {
    cellView.displayName(getContactName(position))
  }

  fun onItemClick(position: Int) {
    view.get()?.openDetailScreen(getContactName(position))
  }

  fun onDestroy() {
    repository.unsubscribe(this)
  }

  override fun update(contacts: List<Contact>) {
    saveItems(contacts)
    view.get()?.refreshList()
  }

  private fun saveItems(items: List<Contact>) {
    this.items = items
  }

  private fun listIsEmpty(): Boolean {
    return items.isEmpty()
  }

  private fun getContactName(position: Int): String {
    return items[position].name
  }

}

Básicamente me suscribo al Observable al inicio, y me desvinculo del mismo cuando la vista se destruye. Por el camino, cuando recibo una actualización a través del método update(), refresco la lista. De esta forma, como comentaba al principio del artículo, mantengo la consistencia entre la fuente de datos y su representación.

Por motivos de eficiencia, he dado por hecho que mi Observer siempre necesita una lista de datos (por eso la envío como parámetro del método update()). En este caso concreto la suposición parece lógica, pero cuando tengáis más de un Observer, puede que ataros a tal restricción haga que vuestros Observers sean menos reutilizables. Es aquí donde tenéis que medir qué necesitáis enviar como parámetro.

Conclusión

El patrón Observer nos facilita que varias clases cooperen juntas manteniendo un bajo acoplamiento. Por otro lado, la dependencia de uno a muchos entre el Observable y sus Observers nos permite enviar la notificación a todos los elementos suscritos a la vez, además de darle a los observers la libertad de gestionar sus suscripciones.

Código fuente

Enlaces de referencia