Android/MVC. Implementació amb el patró observador
Què és el patró Model-Vista-Controlador?
[modifica]El patró MVC (Model-Vista-Controlador) neix amb la idea d'independitzar les dades de la seva presentació i de la interacció que fa l'usuari sobre aquesta presentació. Consta de 3 parts fonamentals:
- El Model: Conjunt de dades que manipulem. Envia a la Vista aquella part de la informació que en cada moment es vol mostrar, normalment per part de l'usuari.
- El Controlador: Respon events (habitualment accions de l'usuari) i fa peticions al Model quan es fa alguna solicitud sobre la informació.
- La Vista: És la visualització del Model. S'encarrega de mostrar les dades del Model en un format adequat per interactuar-hi des de la interfície d'usuari.
Hi ha diverses versions del patró MVC, però totes comparteixen un flux de control similar:
- L'usuari interactua amb la interfície, per exemple al prèmer un botó.
- El controlador reb l'avís d'aquesta interacció i gestiona l'event que arriba.
- El controlador accedeix al model, actualitzant-lo segons l'event de rebut.
- El controlador delega a la vista la tasca de mostrar la nova interfície amb les dades actualitzades. El model no té coneixement directe sobre la vista (excepte si s'implementa el patró observador).
Quina funció té l'observador dins del MVC?
[modifica]Com explicàvem abans, el Model-Vista-Controlador es pot implementar de manera que el model no tingui cap visibilitat sobre la Vista. Això comporta que el controlador hagi de gestionar la interacció entre el Model i la Vista, cosa que ens augmenta l'acoblament. L'ús del patró observador permet al Model avisar directament a la Vista de qualsevol canvi, sense haver de passar pel controlador.
L'observador s'acostuma a implementar de la següent manera:
- Vista: Observador del Model
- Model: Observable
Per tant, en aquest cas:
- El model defineix una interficie i els seus mètodes són els events que pot generar el model.
- La vista implementa aquesta interficie.
Cal afegir que hi poden haver múltiples Observadors sobre el mateix Observable, i de fet en alguns casos és gairebé una necessitat.
D'altra banda, en fer servir el Patró Observador obtenim alguns avantatges, detallats a continuació:
- Garanteix el principi de menor acoblament entre objectes que interactuen
- Permet tenir molts observadors vinculats a un mateix observable i viceversa
- Permet enviar dades a altres objectes d'una manera efectiva sense afectar
- Es poden afegir o eliminar observadors de l'observable en qualsevol moment
L'Exemple: Una llista d'Estudiants modificable
[modifica]Per posar en pràctica l'esquema Model-Vista-Controlador amb patró observador, implementarem un exemple amb una llista d'estudiants, que permeti afegir nous estudiants, esborrar el primer estudiant de la llista o esborrar-los tots.
La classe Estudiant
[modifica]Comencem per crear la classe Estudiant, necessària per la representació d'un conjunt d'estudiants.
- Link al repositori: Estudiant.java
- Anotacions: Revisat 06/11/2019.
- Vegeu també: No
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class Estudiant {
private String nom;
private String cognom;
private LocalDate naixement;
private String dni;
/**
* Constructor manual
*
* @param nom
* @param cognom
* @param naixement
* @param dni
*/
public Estudiant(String nom, String cognom, LocalDate naixement, String dni){
this.nom = nom;
this.cognom = cognom;
this.naixement = naixement;
this.dni = dni;
}
/**
* Constructor automàtic
*/
public Estudiant(){
nom = GeneradorEstudiant.getNom();
cognom = GeneradorEstudiant.getNom();
naixement = GeneradorEstudiant.getNaixement();
dni = GeneradorEstudiant.getDni();
}
/**
* L'ArrayAdapter cridarà a Estudiant.toString() per a passar l'objecte
* Estudiant a String.
* Per tant, cal indicar-li com volem que ho faci.
*/
@Override
public String toString() {
DateTimeFormatter formatejador = DateTimeFormatter.ofPattern("dd/MM/yyyy");
return nom + ' ' + cognom + "\n" + naixement.format(formatejador) + "\n" + dni;
}
}
Com veiem, cada estudiant consta de 4 atributs:
- Nom
- Cognom
- Naixement
- DNI
Aquests atributs poden prendre valors aleatoris gràcies a la classe pròpia GeneradorEstudiant.java, o bé poden prendre vendre els valors que li especifiquem.
Classe GeneradorEstudiant
[modifica]Per poder generar múltiples estudiants fàcilment, hem creat la classe GeneradorEstudiant que permet crear Estudiants amb Noms, Cognoms, data de naixement i dni generats aleatòriament.
- Link al repositori: GeneradorEstudiant.java
- Anotacions: Revisat 06/11/2019.
- Vegeu també: No
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.time.LocalDate;
public class GeneradorEstudiant {
private static String[] Beginning = { "Kr", "Ca", "Ra", "Mrok", "Cru",
"Ray", "Bre", "Zed", "Drak", "Mor", "Jag", "Mer", "Jar", "Mjol",
"Zork", "Mad", "Cry", "Zur", "Creo", "Azak", "Azur", "Rei", "Cro",
"Mar", "Luk" };
private static String[] Middle = { "air", "ir", "mi", "sor", "mee", "clo",
"red", "cra", "ark", "arc", "miri", "lori", "cres", "mur", "zer",
"marac", "zoir", "slamar", "salmar", "urak" };
private static String[] End = { "d", "ed", "ark", "arc", "es", "er", "der",
"tron", "med", "ure", "zur", "cred", "mur" };
public static String getNom(){
Random rand = new Random();
return Beginning[rand.nextInt(Beginning.length)] +
Middle[rand.nextInt(Middle.length)]+
End[rand.nextInt(End.length)];
}
public static int genNumDni(){
return ThreadLocalRandom.current().nextInt(40000000, 50000000);
}
public static String getDni(){
int n = genNumDni();
char c = genLletraDni(n);
return (Integer.toString(n) + c);
}
public static LocalDate getNaixement(){
Random random = new Random();
int minDay = (int) LocalDate.of(1900, 1, 1).toEpochDay();
int maxDay = (int) LocalDate.of(2015, 1, 1).toEpochDay();
long randomDay = minDay + random.nextInt(maxDay - minDay);
LocalDate randomBirthDate = LocalDate.ofEpochDay(randomDay);
return randomBirthDate;
}
public static char genLletraDni(int n){
int num = n % 23;
char c = 'T';
switch(num){
case 0:
c = 'T';
break;
case 1:
c = 'R';
break;
case 2:
c = 'W';
break;
case 3:
c = 'A';
break;
case 4:
c = 'G';
break;
case 5:
c = 'M';
break;
case 6:
c = 'Y';
break;
case 7:
c = 'F';
break;
case 8:
c = 'P';
break;
case 9:
c = 'D';
break;
case 10:
c = 'X';
break;
case 11:
c = 'B';
break;
case 12:
c = 'N';
break;
case 13:
c = 'J';
break;
case 14:
c = 'Z';
break;
case 15:
c = 'S';
break;
case 16:
c = 'Q';
break;
case 17:
c = 'V';
break;
case 18:
c = 'H';
break;
case 19:
c = 'L';
break;
case 20:
c = 'C';
break;
case 21:
c = 'K';
break;
case 22:
c = 'E';
break;
}
return c;
}
}
Implementació del Model: Conjunt d'estudiants
[modifica]El model és el conjunt d'estudiants. Necessitem l'existència d'una classe ConjuntEstudiants.java que encapsuli aquest model.
- Link al repositori: ConjuntEstudiants.java
- Anotacions: Revisat 06/11/2019.
- Vegeu també: No
import java.util.ArrayList;
import java.util.List;
public class ConjuntEstudiants { // el Model
/**
* El Model en sí, el conjunt de dades amb les quals treballarem
*/
private List<Estudiant> estudiants;
/**
* Constructor amb un conjunt d'estudiants buit
*/
public ConjuntEstudiants() {
estudiants = new ArrayList<>();
}
/**
* Constructor amb nombre d'estudiants inicials
*/
public ConjuntEstudiants(int numEstudiants) {
estudiants = new ArrayList<>();
for (int i = 0; i < numEstudiants; i++) {
afegeixEstudiant(new Estudiant());
}
}
/**
* Mètodes públics que fa servir el Controlador per modificar el Model
*/
public void afegeixEstudiant(Estudiant estudiant) {
estudiants.add(0,estudiant);
}
public void esborraTotsEstudiants() {
estudiants.clear();
}
public void esborraUnEstudiant() {
if (!estudiants.isEmpty()) {
estudiants.remove(0);
}
}
/**
* @return La llista d'estudiants actual del Model
*/
public List<Estudiant> getEstudiants() {
return estudiants;
}
}
La classe consta d'una llista privada d'estudiants, i dels constructors bàsics. A més, li hem implementat les operacions bàsiques d'afegir i eliminar estudiants de la llista.
El model fa d'observable
[modifica]Definim la interfície pública interna a ConjuntEstudiants que els nostres observadors hauran d'implementar, amb un únic mètode: onModificaConjuntEstudiants()
- Link al repositori: ConjuntEstudiants.java
- Anotacions: Revisat 06/11/2019.
- Vegeu també: No
/**
* Interfície d'observabilitat que cal que implementin els observadors d'aquest Model
*/
public interface ModificacioObservadorConjuntEstudiants {
void onModificaConjuntEstudiants();
}
onModificaConjuntEstudiants() és l'event que s'executa quan hi ha algun canvi en el nostre model (ConjuntEstudiants).
El comportament d'observable ens requereix un mètode per vincular els Observadors. En aquest exemple es podria haver utilitzat un únic Observador, però fent-ho amb múltiples observadors és més generalista.
- Link al repositori: ConjuntEstudiants.java
- Anotacions: Revisat 06/11/2019.
- Vegeu també: No
/**
* La llista d'observadors, als quals se'ls notificarà de canvis en el Model
*/
private List<ModificacioObservadorConjuntEstudiants> observadors = new ArrayList<>();
/**
* @param observador L'observador a afegir a la llista d'observadors vinculats
*/
public void vinculaObservadorConjuntEstudiants(ModificacioObservadorConjuntEstudiants observador) {
observadors.add(observador);
}
També necessitem un mètode per notificar als observadors quan es produeix una modificació en el model.
- Link al repositori: ConjuntEstudiants.java
- Anotacions: Revisat 06/11/2019.
- Vegeu també: No
/**
* Notifica a tots els observadors que hi ha hagut un canvi en el Model
*/
private void notificaObservadors() {
if (observadors != null) {
for (ModificacioObservadorConjuntEstudiants observador:observadors) {
observador.onModificaConjuntEstudiants();
}
}
}
Els mètodes modificadors, és a dir, el d'afegir i els d'esborrar, han d'avisar als observadors de que hi ha hagut canvis. Per tant hem d'afegir una crida a notificaObservadors()
- Link al repositori: ConjuntEstudiants.java
- Anotacions: Revisat 06/11/2019.
- Vegeu també: No
public void afegeixEstudiant(Estudiant estudiant) {
estudiants.add(0,estudiant);
notificaObservadors();
}
public void esborraTotsEstudiants() {
estudiants.clear();
notificaObservadors();
}
public void esborraUnEstudiant() {
if (!estudiants.isEmpty()) {
estudiants.remove(0);
notificaObservadors();
}
}
Implementació de la Vista: Vista de la llista d'estudiants
[modifica]Com el nostre model és una llista d'estudiants, una manera raonable i senzilla de definir la Vista és partint d'una ListView, que la podem veure com una Vista gestionada per un Adapter. Crearem una classe pròpia que extengui de ListView.
- Link al repositori: VistaDeLlistaEstudiants.java
- Anotacions: Revisat 06/11/2019.
- Vegeu també: No
import android.content.Context;
import android.util.AttributeSet;
import android.widget.ArrayAdapter;
import android.widget.ListView;
public class VistaDeLlistaEstudiants extends ListView implements
ConjuntEstudiants.ModificacioObservadorConjuntEstudiants { // la Vista
private ConjuntEstudiants estudiants; // el Model
private ArrayAdapter<Estudiant> adapter; // l'adapter per visualitzar les dades del Model
/**
* Constructor usat per crear una llista manualment des del codi
* @param context
*/
public VistaDeLlistaEstudiants(Context context) {
super(context);
}
/**
* Constructor usat pel framework per inflar la vista des de l'XML
* @param context
* @param attrs
*/
public VistaDeLlistaEstudiants(Context context, AttributeSet attrs) {
super(context,attrs);
}
/**
* Constructor usat pel framework per inflar la vista des de l'XML
* aplicant un estil base d'un Tema
* @param context
* @param attrs
* @param defStyle
*/
public VistaDeLlistaEstudiants(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* Implementa l'interfície d'observabilitat definida en el Model (observable)
* Cal tornar a vincular l'adapter amb les noves dades.
*/
public void onModificaConjuntEstudiants() {
if (adapter != null)
adapter.notifyDataSetChanged();
}
}
La Vista fa d'observador
[modifica]Ens cal afegir un mètode per vincular el model a la Vista i establir-la com a Observadora del Model
- Link al repositori: VistaDeLlistaEstudiants.java
- Anotacions: Revisat 06/11/2019.
- Vegeu també: No
/**
* Vincula el Model a la Vista i estableix la Vista com a observador del Model
* @param estudiants
*/
public void vinculaModel(ConjuntEstudiants estudiants) {
this.estudiants = estudiants;
this.estudiants.vinculaObservadorConjuntEstudiants(this);
adapter = new ArrayAdapter<>(getContext(), R.layout.estudiant_item, estudiants.getEstudiants());
setAdapter(adapter);
}
Tenim un TextView per representar cada estudiant. Ho describim en el fitxer estudiant_item.xml
- Link al repositori: estudiant_item.xml
- Anotacions: Revisat 06/11/2019.
- Vegeu també: No
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textSize="18sp">
</TextView>
Implementació del Controlador: l'Activitat Principal
[modifica]El controlador és l'Activity. És el primer que actua quan hi ha una interacció de l'usuari amb la interfície. Té la responsabilitat de:
- Crear la Vista
- Crear el Model
- Vincular la Vista com a observadora del Model.
Per tant, ha de tenir una visibilitat d'atribut sobre el model i sobre la vista.
- Link al repositori: MainActivity.java
- Anotacions: Revisat 06/11/2019.
- Vegeu també: No
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
public class MainActivity extends Activity { // el Controlador
VistaDeLlistaEstudiants vistaDeLlistaEstudiants; // la Vista
ConjuntEstudiants conjuntEstudiants; // el Model
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
inicialitza();
}
/**
* Inicialitza el Controlador
* Obtenim la Vista i el Model i els lliguem
* Definim el comportament dels botons
*/
private void inicialitza() {
vistaDeLlistaEstudiants = findViewById(R.id.vistadellistaestudiants);
conjuntEstudiants = new ConjuntEstudiants(4);
vistaDeLlistaEstudiants.vinculaModel(conjuntEstudiants);
findViewById(R.id.botoafegeix).setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
afegeixEstudiant();
}
}
);
findViewById(R.id.botoesborraun).setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
esborraUnEstudiant();
}
}
);
findViewById(R.id.botoesborratots).setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
esborraTotsEstudiants();
}
}
);
}
/**
* Mètodes privats per modificar el Model
*/
private void afegeixEstudiant() {
Estudiant nouEstudiant = new Estudiant();
conjuntEstudiants.afegeixEstudiant(nouEstudiant);
}
private void esborraUnEstudiant() {
conjuntEstudiants.esborraUnEstudiant();
}
private void esborraTotsEstudiants() {
conjuntEstudiants.esborraTotsEstudiants();
}
}
Visualització de l'aplicació
[modifica]Per aquest exemple s'ha utilitzat un Constraint Layout amb els següents components:
- Un TextView, que mostra el títol "Llista d'Estudiants"
- 3 botons: Afegir un estudiant/Esborrar un estudiant/Esborrar tots els estudiants
- Link al repositori: activity_main.xml
- Anotacions: Revisat 06/11/2019.
- Vegeu també: No
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/titol"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:text="@string/llista_d_estudiants"
android:textSize="24sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/botoafegeix"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginBottom="24dp"
android:text="@string/afegir"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/botoesborraun"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/esborrar"
app:layout_constraintEnd_toStartOf="@+id/botoesborratots"
app:layout_constraintStart_toEndOf="@+id/botoafegeix"
app:layout_constraintTop_toTopOf="@+id/botoafegeix" />
<Button
android:id="@+id/botoesborratots"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:text="@string/esborrar_tots"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/botoesborraun" />
</androidx.constraintlayout.widget.ConstraintLayout>
La nostra classe VistaDeLlistaEstudiants és una extensió d'una ListView, la qual cosa no pot aparèixer en un Layout XML.
La solució és referenciar-la afegint-hi el prefix del package que la conté.
- Link al repositori: activity_main.xml
- Anotacions: Revisat 06/11/2019.
- Vegeu també: No
<edu.damo.mvc.VistaDeLlistaEstudiants
android:id="@+id/vistadellistaestudiants"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toTopOf="@+id/botoafegeix"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/titol">
</edu.damo.mvc.VistaDeLlistaEstudiants>
Mostres d'execució
[modifica]-
Llista inicial
-
Llista després d'afegir un element
-
Llista després d'esborrar un element
-
Llista després d'esborrar tots els elements