Generics in Java per Android: Adapter Generico, Repository e ApiResponse con Esempi

Generics in Java per Android: guida completa con esempi pratici

I Generics in Java sono uno degli strumenti più potenti per scrivere codice Android riutilizzabile, sicuro e facile da mantenere. In questo articolo vediamo come usarli in modo pratico in un progetto Android, dalla sintassi base fino a una mini app che utilizza un adapter generico per visualizzare sia prodotti che utenti in una RecyclerView Android.

1. Introduzione: cos’è un Generic e perché usarlo in Android

Un Generic in Java è un modo per scrivere classi e metodi che lavorano con tipi di dato parametrici, ovvero tipi che puoi specificare dall’esterno (come String, Integer, una tua classe Product, ecc.). Il vantaggio principale è che puoi riutilizzare lo stesso codice per tipi diversi mantenendo il type safety, cioè il controllo dei tipi a compile-time.

In Android questo è particolarmente utile per:

  • adapter riutilizzabili per RecyclerView Android
  • classi di risposta API generiche (ad esempio risultati di Retrofit)
  • repository generici per accedere ai dati (API, database locale, cache)
  • utility riutilizzabili per la gestione di risultati (success/failure) e validazioni

Se hai già dimestichezza con la Programmazione ad Oggetti in Android, i Generics sono il passo successivo naturale per rendere il tuo codice più pulito.

2. Sintassi base: <T>, <T extends …>, wildcard <?>

La sintassi dei Generics ruota tutta intorno alle parentesi angolari <>. Vediamo i casi più comuni.

<T>: tipo generico di base

T è il nome convenzionale per un tipo generico. Potresti usare qualsiasi nome (es. E, U, R), ma T è lo standard. Esempio:

public class Box<T> {
    private T value;

    public Box(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

// Uso
Box<String> stringBox = new Box<>("Ciao");
Box<Integer> intBox = new Box<>(10);

<T extends …>: limiti sul tipo (bounded types)

Se vuoi che il parametro generico sia solo di un certo tipo o sottotipo, usi <T extends MyClass>. Ad esempio, vuoi una lista che accetti solo elementi che implementano Parcelable (utile in Android per passare dati tra Activity, come visto in Richiamare un Activity Android):

public class ParcelableBox<T extends android.os.Parcelable> {
    private T value;

    public ParcelableBox(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

Wildcard <?>: tipo sconosciuto

La wildcard <?> rappresenta un tipo sconosciuto. È utile quando vuoi accettare una collezione generica senza specificare esattamente il tipo:

public void stampaLista(List<?> items) {
    for (Object item : items) {
        Log.d("Lista", item.toString());
    }
}

Più avanti, nella sezione sugli errori comuni, vedremo anche <? extends T> e <? super T>.

3. Classe generica: esempio con ApiResponse<T> (success, data, message)

Un esempio tipico in Android è incapsulare la risposta di una chiamata API in una classe generica. Invece di avere una classe per ogni tipo di risposta, usi ApiResponse<T>:

public class ApiResponse<T> {
    private boolean success;
    private T data;
    private String message;

    public ApiResponse(boolean success, T data, String message) {
        this.success = success;
        this.data = data;
        this.message = message;
    }

    public boolean isSuccess() {
        return success;
    }

    public T getData() {
        return data;
    }

    public String getMessage() {
        return message;
    }
}

// Esempi di utilizzo
ApiResponse<List<Product>> productsResponse;
ApiResponse<User> userResponse;
ApiResponse<Void> logoutResponse; // Nessun dato, solo successo/messaggio

In questo modo puoi definire una sola volta la logica di gestione della risposta (successo/errore) e riutilizzarla per qualsiasi tipo di dato.

4. Metodo generico: esempio pratico

Oltre alle classi, anche i metodi possono essere generici. Un caso utile in Android è un metodo utility che converte una Response<T> di Retrofit nella nostra ApiResponse<T> generica.

public class ApiUtils {

    // <T> definisce il tipo generico del metodo
    public static <T> ApiResponse<T> fromRetrofitResponse(retrofit2.Response<T> response) {
        if (response.isSuccessful()) {
            return new ApiResponse<>(
                true,
                response.body(),
                "OK"
            );
        } else {
            return new ApiResponse<>(
                false,
                null,
                "Errore HTTP: " + response.code()
            );
        }
    }
}

Ora puoi usare lo stesso metodo per qualsiasi chiamata Retrofit, indipendentemente dal tipo di dato restituito.

5. Adapter generico per RecyclerView con <T>

Gli adapter della RecyclerView Android sono un candidato perfetto per i Generics. L’idea è creare un adapter riutilizzabile che non dipenda da un tipo specifico (es. Product), ma lavori con un <T> generico e deleghi il binding al chiamante tramite un callback.

public class GenericAdapter<T> extends RecyclerView.Adapter<GenericAdapter.GenericViewHolder> {

    public interface Binder<T> {
        void onBind(View itemView, T item, int position);
    }

    private List<T> items;
    private int layoutId;
    private Binder<T> binder;

    public GenericAdapter(List<T> items, int layoutId, Binder<T> binder) {
        this.items = items;
        this.layoutId = layoutId;
        this.binder = binder;
    }

    @NonNull
    @Override
    public GenericViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(layoutId, parent, false);
        return new GenericViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull GenericViewHolder holder, int position) {
        T item = items.get(position);
        binder.onBind(holder.itemView, item, position);
    }

    @Override
    public int getItemCount() {
        return items != null ? items.size() : 0;
    }

    public static class GenericViewHolder extends RecyclerView.ViewHolder {
        public GenericViewHolder(@NonNull View itemView) {
            super(itemView);
        }
    }
}

Questo adapter non conosce il tipo concreto degli oggetti, ma si limita a passarlo al callback. Sarà il codice dell’Activity o del Fragment a dire come effettuare il binding.

6. Repository generico: BaseRepository<T>

Un Repository in architetture come MVVM serve a incapsulare l’accesso ai dati (API, database, cache, ecc.). Un BaseRepository<T> può contenere logica comune per gestire errori, caching o mapping delle risposte, mentre i repository specifici ereditano da esso.

public abstract class BaseRepository<T> {

    protected MutableLiveData<ApiResponse<T>> liveData = new MutableLiveData<>();

    public LiveData<ApiResponse<T>> getLiveData() {
        return liveData;
    }

    protected <R> void handleCall(retrofit2.Call<R> call, Function<R, T> mapper) {
        call.enqueue(new Callback<R>() {
            @Override
            public void onResponse(Call<R> call, Response<R> response) {
                ApiResponse<R> apiResponse = ApiUtils.fromRetrofitResponse(response);
                if (apiResponse.isSuccess() && apiResponse.getData() != null) {
                    T mapped = mapper.apply(apiResponse.getData());
                    liveData.postValue(new ApiResponse<>(true, mapped, "OK"));
                } else {
                    liveData.postValue(new ApiResponse<>(false, null, apiResponse.getMessage()));
                }
            }

            @Override
            public void onFailure(Call<R> call, Throwable t) {
                liveData.postValue(new ApiResponse<>(false, null, t.getMessage()));
            }
        });
    }
}

Un repository concreto per i prodotti potrebbe estendere BaseRepository<List<Product>> e usare handleCall() per centralizzare la gestione della chiamata.

7. Bounded type parameters: <T extends Comparable<T>>

Un caso tipico di bounded type è quando vuoi lavorare solo con tipi che possono essere confrontati, ovvero che implementano Comparable. In Android questo è utile, ad esempio, per ordinare liste prima di mostrarle in una RecyclerView.

public class SortUtils {

    public static <T extends Comparable<T>> void sortList(List<T> list) {
        Collections.sort(list);
    }
}

// Uso
List<Integer> numbers = Arrays.asList(3, 1, 2);
SortUtils.sortList(numbers);

List<String> names = Arrays.asList("Luca", "Anna", "Marco");
SortUtils.sortList(names);

Se vuoi ordinare una tua classe (es. Product), ti basta implementare Comparable<Product> nella classe e puoi riutilizzare lo stesso metodo.

8. Mini app completa: lista generica di Prodotti e Utenti con lo stesso adapter

Vediamo ora un esempio completo in cui usiamo lo stesso GenericAdapter<T> per mostrare sia una lista di Product che una lista di User. Supponiamo di avere un layout di riga semplice con un TextView.

// Product.java
public class Product {
    private String name;
    private double price;

    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public double getPrice() {
        return price;
    }
}

// User.java
public class User {
    private String username;
    private String email;

    public User(String username, String email) {
        this.username = username;
        this.email = email;
    }

    public String getUsername() {
        return username;
    }

    public String getEmail() {
        return email;
    }
}

Layout di item (res/layout/item_simple.xml):

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:id="@+id/textTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="16sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/textSubtitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="14sp" />

</LinearLayout>

Activity con due RecyclerView che usano lo stesso adapter generico:

public class MainActivity extends AppCompatActivity {

    private RecyclerView recyclerProducts;
    private RecyclerView recyclerUsers;

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

        recyclerProducts = findViewById(R.id.recyclerProducts);
        recyclerUsers = findViewById(R.id.recyclerUsers);

        recyclerProducts.setLayoutManager(new LinearLayoutManager(this));
        recyclerUsers.setLayoutManager(new LinearLayoutManager(this));

        List<Product> products = Arrays.asList(
                new Product("Mouse", 19.99),
                new Product("Tastiera", 29.99)
        );

        List<User> users = Arrays.asList(
                new User("luca", "luca@example.com"),
                new User("anna", "anna@example.com")
        );

        GenericAdapter<Product> productAdapter = new GenericAdapter<>(
                products,
                R.layout.item_simple,
                (itemView, product, position) -> {
                    TextView title = itemView.findViewById(R.id.textTitle);
                    TextView subtitle = itemView.findViewById(R.id.textSubtitle);
                    title.setText(product.getName());
                    subtitle.setText(String.format(Locale.getDefault(), "%.2f €", product.getPrice()));
                }
        );

        GenericAdapter<User> userAdapter = new GenericAdapter<>(
                users,
                R.layout.item_simple,
                (itemView, user, position) -> {
                    TextView title = itemView.findViewById(R.id.textTitle);
                    TextView subtitle = itemView.findViewById(R.id.textSubtitle);
                    title.setText(user.getUsername());
                    subtitle.setText(user.getEmail());
                }
        );

        recyclerProducts.setAdapter(productAdapter);
        recyclerUsers.setAdapter(userAdapter);
    }
}

Con questo approccio hai scritto un solo adapter e lo stai usando per due tipi completamente diversi. Puoi facilmente estendere l’idea per gestire il click sugli elementi, ad esempio aprendo un DialogFragment Android o una nuova Activity (vedi Richiamare un Activity Android).

9. Errori comuni con i Generics

Lavorando con i Generics è facile incorrere in alcuni errori tipici. Eccone alcuni da tenere a mente nello sviluppo Android.

  • Raw types (es. List invece di List<T>): usare il tipo grezzo disabilita il controllo dei tipi e può portare a ClassCastException a runtime. Evitalo sempre, specifica sempre il tipo generico.
  • Erasure dei tipi: a runtime le informazioni sui generics vengono rimosse (type erasure). Questo significa che non puoi fare cose come new T() o instanceof List<String> in modo diretto. Spesso devi passare esplicitamente la Class<T> se ti serve a runtime.
  • Confusione tra <? extends T> e <? super T>: una regola utile è PECS (Producer Extends, Consumer Super). Usa <? extends T> quando la collezione produce valori (la leggi), e <? super T> quando la collezione consuma valori (ci scrivi dentro).
  • Metodi troppo generici: inserire troppi parametri generici nello stesso metodo o classe può rendere il codice difficile da leggere. Mantieni i Generics dove hanno un reale beneficio di riuso e chiarezza.
  • Cast non sicuri: se ti ritrovi spesso a fare cast espliciti all’interno di classi generiche, forse la tua modellazione non è ideale. Cerca di sfruttare il type system per evitare i cast.

10. Conclusione e link agli articoli correlati

I Generics in Java sono fondamentali per scrivere codice Android moderno, pulito e riutilizzabile. Dalle API ai repository, dagli adapter per RecyclerView alle utility di sorting, imparare a usarli bene ti aiuterà a ridurre la duplicazione e a prevenire molti errori di tipo.

Per approfondire concetti collegati e vedere altri esempi pratici, puoi leggere anche:

Integrare i Generics nei tuoi prossimi progetti Android ti permetterà di costruire una base solida e facilmente estendibile per le tue app.

Commenti

Lascia un commento