Design Pattern Observer in Android: Listener, Callback e LiveData con Java

Design Pattern Observer in Android Java: guida completa

In questo articolo vediamo come usare il pattern Observer in Android con Java, partendo dall'implementazione base fino agli esempi pratici con listener, callback e LiveData. Se non hai molta familiarità con le basi di Java e dell'OOP, ti consiglio prima di dare un'occhiata a Programmazione ad Oggetti in Android.

1. Cos'è il pattern Observer e quando usarlo in Android

Il pattern Observer è un pattern comportamentale che definisce una relazione uno-a-molti tra oggetti: quando lo stato di un oggetto (Subject) cambia, tutti gli oggetti interessati (Observer) vengono notificati automaticamente.

In Android questo modello è ovunque: pulsanti che espongono OnClickListener, liste con OnItemClickListener, TextWatcher per le EditText, LiveData in Architecture Components, e così via. Ogni volta che vuoi separare la logica che genera un evento dalla logica che reagisce a quell'evento, il pattern Observer è una scelta naturale.

Alcuni casi tipici in un'app Android:

  • Notificare più schermate quando cambia un dato (es. utente loggato, conteggio notifiche).
  • Decouplare componenti: Activity, Fragment, Adapter, Service che comunicano tra loro senza dipendenze forti.
  • Reagire a input dell'utente (click, swipe, selezione di un elemento di lista).

2. Implementazione base: interfaccia Observer e Subject

Partiamo da una versione Java pura del pattern, senza riferimenti diretti ad Android. Immagina un Subject che mantiene un valore intero e notifica tutti gli Observer ogni volta che cambia.

Definiamo prima l'interfaccia Observer:

public interface MyObserver {
    void onValueChanged(int newValue);
}

Ora il Subject che mantiene la lista degli observer iscritti:

import java.util.ArrayList;
import java.util.List;

public class MySubject {

    private final List<MyObserver> observers = new ArrayList<>();
    private int value;

    public void addObserver(MyObserver observer) {
        if (observer == null) return;
        if (!observers.contains(observer)) {
            observers.add(observer);
        }
    }

    public void removeObserver(MyObserver observer) {
        observers.remove(observer);
    }

    public void setValue(int newValue) {
        if (this.value != newValue) {
            this.value = newValue;
            notifyObservers();
        }
    }

    public int getValue() {
        return value;
    }

    private void notifyObservers() {
        for (MyObserver observer : observers) {
            observer.onValueChanged(value);
        }
    }
}

Qui vedi tutti gli elementi tipici:

  • Subject: espone metodi per registrare/deregistrare observer e per aggiornare lo stato.
  • Observer: è un'interfaccia con un metodo di callback invocato ad ogni cambiamento.
  • Notifica: il Subject chiama il metodo di ogni Observer registrato.

3. Listener personalizzati tra Activity e Fragment

Un caso molto comune in Android è la comunicazione tra Fragment e Activity. Il pattern classico è definire un'interfaccia nel Fragment e farla implementare all'Activity ospitante. In pratica, il Fragment fa da Subject e l'Activity è l'Observer.

Esempio: un Fragment mostra una lista di elementi; quando l'utente seleziona un elemento, il Fragment notifica l'Activity.

public class ItemListFragment extends Fragment {

    public interface OnItemSelectedListener {
        void onItemSelected(String itemId);
    }

    private OnItemSelectedListener listener;

    @Override
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
        if (context instanceof OnItemSelectedListener) {
            listener = (OnItemSelectedListener) context;
        } else {
            throw new ClassCastException(context.toString()
                    + " must implement OnItemSelectedListener");
        }
    }

    private void onUserClickItem(String itemId) {
        if (listener != null) {
            listener.onItemSelected(itemId);
        }
    }
}

L'Activity implementa l'interfaccia ed è quindi osservatrice degli eventi del Fragment:

public class MainActivity extends AppCompatActivity
        implements ItemListFragment.OnItemSelectedListener {

    @Override
    public void onItemSelected(String itemId) {
        // Reagisci alla selezione (es. apri dettaglio)
        Toast.makeText(this, "Selezionato: " + itemId, Toast.LENGTH_SHORT).show();
    }
}

In questo modo il Fragment non conosce i dettagli dell'Activity. Sa solo che esiste qualcuno interessato agli eventi di selezione. Per approfondire la gestione delle schermate, puoi leggere anche Cambiare Fragment Android.

4. Callback tra componenti Android

Il concetto di callback in Android è strettamente collegato al pattern Observer. Una callback non è altro che un metodo che verrà chiamato in risposta a un evento asincrono.

Qualche esempio tipico:

  • Avvii un'Activity per risultato e ricevi la risposta in onActivityResult() (o con il nuovo Activity Result API).
  • Mostri un DialogFragment che restituisce la scelta dell'utente tramite un'interfaccia callback.
  • Esegui una chiamata di rete e ricevi il risultato in un metodo onSuccess / onError.

Esempio pratico con DialogFragment che comunica il pulsante premuto all'Activity:

public class ConfirmDialogFragment extends DialogFragment {

    public interface OnConfirmListener {
        void onConfirmed();
        void onCancelled();
    }

    private OnConfirmListener listener;

    @Override
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
        if (context instanceof OnConfirmListener) {
            listener = (OnConfirmListener) context;
        }
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
        AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
        builder.setTitle("Sei sicuro?")
                .setPositiveButton("Sì", (dialog, which) -> {
                    if (listener != null) listener.onConfirmed();
                })
                .setNegativeButton("No", (dialog, which) -> {
                    if (listener != null) listener.onCancelled();
                });
        return builder.create();
    }
}

L'Activity si registra come osservatrice implementando OnConfirmListener. Per altri esempi avanzati su finestre di dialogo, vedi DialogFragment Android.

5. Interface come Observer: OnClickListener, OnItemClickListener

La maggior parte delle API Android usa il pattern Observer tramite interfacce. Quando imposti un listener, stai registrando un Observer che verrà notificato quando accade l'evento.

Esempio con OnClickListener su un pulsante:

Button myButton = findViewById(R.id.my_button);

myButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // Questo è il codice Observer che reagisce al click
        Toast.makeText(MainActivity.this, "Button cliccato", Toast.LENGTH_SHORT).show();
    }
});

Esempio con OnItemClickListener su una lista. Se stai usando le liste moderne con RecyclerView, ti consiglio di leggere RecyclerView Android.

ListView listView = findViewById(R.id.list_view);

listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        // Observer: reagisce alla selezione dell'elemento
        String item = (String) parent.getItemAtPosition(position);
        Toast.makeText(MainActivity.this, "Hai scelto: " + item, Toast.LENGTH_SHORT).show();
    }
});

In tutti questi casi:

  • Il widget UI è il Subject (Button, ListView, RecyclerView, ecc.).
  • L'interfaccia listener è il contratto Observer.
  • La tua implementazione dell'interfaccia è l'Observer concreto che riceve le notifiche.

6. LiveData come implementazione moderna del pattern Observer

Con l'introduzione di Architecture Components, Google ha fornito una implementazione pronta all'uso del pattern Observer: LiveData. È una classe osservabile che notifica automaticamente i suoi observer quando il dato cambia, rispettando il ciclo di vita delle Activity e dei Fragment.

Esempio con un ViewModel che espone una LiveData<List<String>>:

public class ItemViewModel extends ViewModel {

    private final MutableLiveData<List<String>> itemsLiveData = new MutableLiveData<>();

    public LiveData<List<String>> getItems() {
        return itemsLiveData;
    }

    public void loadItems() {
        // Simuliamo un caricamento
        List<String> items = new ArrayList<>();
        items.add("Item 1");
        items.add("Item 2");
        items.add("Item 3");
        itemsLiveData.setValue(items);
    }
}

In Activity o Fragment ti registri come Observer usando il metodo observe():

public class ItemListActivity extends AppCompatActivity {

    private ItemViewModel viewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_item_list);

        viewModel = new ViewModelProvider(this).get(ItemViewModel.class);

        viewModel.getItems().observe(this, new Observer<List<String>>() {
            @Override
            public void onChanged(List<String> items) {
                // Questo è il codice Observer che reagisce ai cambiamenti dei dati
                // Aggiorna l'UI (es. adapter della lista)
            }
        });

        viewModel.loadItems();
    }
}

La potenza di LiveData sta nel fatto che:

  • Rispetta il ciclo di vita (non chiama l'observer se l'Activity è in background o distrutta).
  • Gestisce automaticamente la sottoscrizione e il cleanup.
  • Riduce il rischio di memory leak rispetto a observer custom mal gestiti.

7. Mini app completa: lista aggiornabile con Observer pattern

Vediamo ora un esempio completo che combina ViewModel, LiveData e una lista. Faremo una Activity con una lista di stringhe e un pulsante "Aggiungi" che inserisce un nuovo elemento e aggiorna automaticamente la UI grazie al pattern Observer.

Supponiamo di usare una RecyclerView; per i dettagli sull'adapter ti rimando a RecyclerView Android. Qui ci concentriamo sul flusso Observer.

public class ListViewModel extends ViewModel {

    private final MutableLiveData<List<String>> itemsLiveData = new MutableLiveData<>();
    private int counter = 1;

    public ListViewModel() {
        List<String> initial = new ArrayList<>();
        initial.add("Elemento iniziale 1");
        initial.add("Elemento iniziale 2");
        itemsLiveData.setValue(initial);
    }

    public LiveData<List<String>> getItems() {
        return itemsLiveData;
    }

    public void addItem() {
        List<String> current = itemsLiveData.getValue();
        if (current == null) {
            current = new ArrayList<>();
        } else {
            current = new ArrayList<>(current); // copia per evitare side effects
        }
        current.add("Nuovo elemento " + counter++);
        itemsLiveData.setValue(current);
    }
}

Activity che osserva i cambiamenti e aggiorna l'adapter:

public class ListActivity extends AppCompatActivity {

    private ListViewModel viewModel;
    private MyAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_list);

        RecyclerView recyclerView = findViewById(R.id.recycler_view);
        Button addButton = findViewById(R.id.button_add);

        adapter = new MyAdapter();
        recyclerView.setAdapter(adapter);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));

        viewModel = new ViewModelProvider(this).get(ListViewModel.class);

        // Observer: reagisce ai cambiamenti della lista
        viewModel.getItems().observe(this, new Observer<List<String>>() {
            @Override
            public void onChanged(List<String> items) {
                adapter.submitList(items); // o adapter.setData(items);
            }
        });

        addButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                viewModel.addItem();
            }
        });
    }
}

Con questo approccio:

  • La Activity non gestisce direttamente la lista, ma osserva i dati nel ViewModel.
  • Quando premi il pulsante, addItem() modifica il dato nel Subject (LiveData).
  • La UI viene aggiornata automaticamente, rispettando il ciclo di vita.

8. Differenza tra Observer, Listener e Callback

In Android spesso questi termini vengono usati in modo quasi intercambiabile, ma è utile distinguerli:

  • Observer: è il concetto più generale. È un oggetto che si registra presso un Subject per essere notificato di cambiamenti di stato.
  • Listener: è un tipo specifico di Observer, usato soprattutto per eventi UI (click, touch, scroll). Di solito è una interfaccia con uno o più metodi, registrata con setOn...Listener().
  • Callback: è un metodo o un insieme di metodi passato come parametro ad un'altra funzione/oggetto da eseguire in un secondo momento (quando un'operazione asincrona è completata, quando l'utente prende una decisione, ecc.).

In pratica, in Android, un listener è quasi sempre un Observer implementato come interfaccia, e la callback è il metodo specifico chiamato quando l'evento si verifica.

9. Errori comuni

Usando il pattern Observer in Android è facile incorrere in alcuni errori tipici:

  • Memory leak: conservi un riferimento ad una Activity o Fragment in un Subject che vive più a lungo (es. singleton). Se non rimuovi l'observer quando la schermata viene distrutta, l'istanza non verrà mai liberata.
  • Registrazione multipla: chiami più volte addObserver() senza controllare se l'observer è già presente. Risultato: il callback viene eseguito più volte per lo stesso evento.
  • Notifiche fuori dal thread giusto: aggiorni la UI da un observer chiamato su un thread in background. In Android gli aggiornamenti UI devono sempre avvenire sul main thread.
  • Dipendenze troppo forti: il Subject conosce dettagli concreti degli observer (es. tipo specifico di Activity). Questo rende il codice rigido e difficile da testare.
  • Mancata gestione del ciclo di vita: observer che ricevono eventi anche quando la schermata non è visibile. LiveData aiuta a risolvere questo problema gestendo automaticamente il lifecycle.

Quando usi pattern come Activity->Activity o Activity->Fragment, ricordati di gestire bene il ciclo di vita. Per spostarti tra schermate in modo corretto, puoi seguire la guida a Richiamare un Activity Android.

10. Conclusione e articoli correlati

Il pattern Observer è uno dei pilastri della programmazione in Android: listener, callback, LiveData, eventi di sistema e di UI fanno tutti leva su questo concetto. Imparare a riconoscerlo e usarlo in modo consapevole ti permette di scrivere codice più pulito, modulare e facile da testare.

Per continuare ad approfondire, puoi dare un'occhiata anche a questi articoli correlati su apptutorial.link:

Partendo da questi esempi potrai creare strutture più complesse, gestire comunicazioni tra più componenti e sfruttare al meglio le API moderne di Android basate su osservabili.

Commenti

Lascia un commento