Quando si cerca di approfondire i propri studi sulle interfacce Android, il primo scoglio con cui ci si scontra, spesso, sono i Fragment. Il loro utilizzo non è così proibitivo ma ciò che li rende ostici è la necessità di comprenderne a fondo il ciclo di vita e le basi dell’interazione con l’ecosistema Android. Con questo articolo ed i successivi, ci interesseremo proprio a questi aspetti.
Cosa sono i Fragment?
Un Fragment può essere considerato una porzione dell’interfaccia utente, completa non solo di layout ma anche di tutto il codice necessario a rappresentarne la logica di gestione. La loro nascita – risalente ad Android 3.0 – ha offerto la possibilità di creare interfacce utente composte da porzioni riutilizzabili e alternabili senza la necessità di distruggere l’Activity. Un Fragment ha bisogno di un’Activity che gli faccia da contenitore pertanto l’uso di questi componenti facilita, da un lato, la gestione del ciclo di vita di un’Activity (visto che questa non cambia ma si alternano solo i Fragment) ma dall’altro aggiunge ulteriore complessità considerando che i Fragment hanno un proprio ciclo di vita piuttosto articolato.
Per prendere confidenza con la tematica e studiarne gli aspetti più delicati affronteremo il seguente esempio.
L’esempio
Il progetto (il cui codice è disponibile qui) è costituito da una sola Activity con due Fragment che si alternano costituendone l’interfaccia utente.
Nel primo troviamo tre pulsanti, ognuno dei quali etichettato con il nome di una città italiana.
Al click di uno di questi, il Fragment viene sostituito con un secondo che mostra una foto ritraente un luogo simbolo della città in questione accompagnato da un piccolo testo descrittivo (per il quale abbiamo attinto da Wikipedia).
Dai nostri Framgment vogliamo un comportamento completo ed efficiente che non trascuri alcun aspetto utile all’interazione utente.
I nostri obiettivi sono:
- quando appare il secondo Fragment (quello con la foto della città, per intenderci) vogliamo che premendo il tasto Back si torni al primo Fragment (non è il comportamento di default) e che appaia la freccia a sinistra per la navigazione Up sull’Action Bar: quest’ultima deve scomparire al ritorno al primo Fragment;
- se ruotiamo il dispositivo mentre visualizziamo il secondo Fragment, vogliamo che l’Activity continui a visualizzarlo a rotazione conclusa in modo che il cambio di configurazione abbia il minimo impatto possibile sulla user experience.
Activity e primo Fragment
Il layout dell’Activity è composto da un FrameLayout:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
Questo è un tipo di layout che serve a mostrare un singolo elemento nell’interfaccia. In questo FrameLayout, verranno collocati i nostri Fragment e la loro alternanza sarà gestita dal FragmentManager, messo a disposizione dall’Activity stessa.
I dati utilizzati saranno forniti dalla classe ElencoCitta che simula una sorgente dati che nella realtà sarebbe rappresentata da un database, un ContentProvider o un’altra componente che dialoga con la Rete:
public class ElencoCitta {
private HashMap<String, Citta> citta=new HashMap<String, Citta>();
public static class Citta
{
public String nome;
public String presentazione;
public int immagine;
public Citta(String nome, String presentazione, int immagine) {
this.nome = nome;
this.presentazione = presentazione;
this.immagine=immagine;
}
}
public ElencoCitta()
{
citta.put("milano", new Citta(
"Milano",
"....",
R.drawable.milano
));
citta.put("roma", new Citta(
"Roma",
"....",
R.drawable.roma
));
citta.put("palermo", new Citta(
"Palermo",
"....",
R.drawable.palermo
));
}
public Citta trovaCitta(String nomeCitta)
{
return citta.get(nomeCitta);
}
}
Nell’Activity, gestiremo il passaggio tra un Fragment e l’altro mediante una FragmentTransaction. Questa è costituita dalle seguenti fasi:
- viene creata una nuova FragmentTransaction dal FragmentManager;
- viene eseguita l’operazione che si vuole svolgere che consiste tipicamente nell’aggiunta di un Fragment ad un FrameLayout (metodo add), nella sua sostituzione (metodo replace) o nella sua rimozione (metodo remove);
- chiediamo di rendere operativa la modifica con l’invocazione del metodo commit.
Ciò lo faremo nel metodo cambiaFragment che risponde al click su uno dei tre pulsanti presenti nel primo Fragment. Questo è il codice completo dell’Activity.
public class MainActivity extends AppCompatActivity {
private ElencoCitta elenco;
@Override
public void onBackPressed() {
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
super.onBackPressed();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
elenco=new ElencoCitta();
FragmentManager fm = getSupportFragmentManager();
Fragment trovato=fm.findFragmentByTag(CittaFragment.CITTAFRAGMENT_TAG);
FragmentTransaction ft = fm.beginTransaction();
if (trovato!=null) {
ft.replace(R.id.fragment, trovato);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
else
ft.replace(R.id.fragment, new MainFragment());
ft.commit();
}
public void cambiaFragment(View v)
{
CittaFragment fragment=null;
switch(v.getId())
{
case R.id.roma:
fragment=CittaFragment.newInstance(elenco.trovaCitta("roma"));
break;
case R.id.milano:
fragment=CittaFragment.newInstance(elenco.trovaCitta("milano"));
break;
case R.id.palermo:
fragment=CittaFragment.newInstance(elenco.trovaCitta("palermo"));
}
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.addToBackStack(null);
ft.replace(R.id.fragment, fragment,CittaFragment.CITTAFRAGMENT_TAG);
ft.commit();
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
getSupportFragmentManager().popBackStack();
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
break;
}
return false;
}
}
Il primo Fragment, classe MainFragment, ha un codice molto semplice:
public class MainFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_main, container, false);
}
}
La classe è un’estensione di Fragment e al suo interno implementa il metodo onCreateView, facente parte del ciclo di vita di un Fragment. Come approccio iniziale, preferiamo non mostrare subito tutte le fasi del ciclo di vita ma affrontare i metodi man mano che ne abbiamo bisogno lasciando ad articoli successivi una visione più d’insieme.
Nel metodo onCreateView deve essere caricato il layout del Fragment con l’inizializzazione dell’interfaccia. In questo caso, carichiamo tramite LayoutInflater il layout che include i tre pulsanti.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="300dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_centerInParent="true">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Roma"
android:onClick="cambiaFragment"
android:id="@+id/roma"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Milano"
android:id="@+id/milano"
android:onClick="cambiaFragment"
android:layout_marginTop="20dp"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Palermo"
android:id="@+id/palermo"
android:onClick="cambiaFragment"
android:layout_marginTop="20dp"/>
</LinearLayout>
</RelativeLayout>
Per quanto riguarda la gestione del loro evento di pressione avremmo potuto gestirlo internamente al Fragment con gli appositi Listener ma visto che le operazioni richiamate riguardano l’Activity nel suo complesso abbiamo preferito utilizzare l’attributo onClick che punta al metodo cambiaFragment dell’Activity. A seconda dei casi, ci si potrà regolare personalmente se si vuole gestire gli eventi all’interno del Fragment o meno ma questo dipenderà spesso dal tipo di operazioni che si devono affrontare.
L’Activity, aspetti rilevanti
Dopo aver introdotto il codice dell’Activity, notiamo alcuni aspetti particolari. Al momento di eseguire la FragmentTransaction nel metodo cambiaFragment, invochiamo il metodo addToBackStack. Questo serve a fare in modo che il Fragment sostituito venga aggiunto in una sorta di storico chiamato BackStack cosicchè, al momento della pressione del pulsante Back mentre il secondo Fragment è visualizzato, l’Activity non venga chiusa ma si inverta l’ultima transazione eseguita riportando alla luce il primo Fragment. Senza l’uso di addToBackStack, al momento della pressione del tasto Back, l’Activity verrebbe chiusa.
Notiamo ancora l’invocazione del metodo replace:
ft.replace(R.id.fragment, fragment,CittaFragment.CITTAFRAGMENT_TAG);
I tre parametri passati sono l’id del FrameLayout che ospiterà il Fragment, l’oggetto Fragment che subentrerà ed infine – cosa che ci interessa in questo discorso – una costante stringa che svolgerà il ruolo di tag del Fragment. Il tag permetterà in seguito di identificare il Fragment chiedendo al FragmentManager il suo ripristino e ciò tornerà utile nel metodo onCreate al momento della ricostruzione dell’Activity dopo la rotazione del dispositivo:
FragmentManager fm = getSupportFragmentManager();
Fragment trovato=fm.findFragmentByTag(CittaFragment.CITTAFRAGMENT_TAG);
FragmentTransaction ft = fm.beginTransaction();
if (trovato!=null) {
ft.replace(R.id.fragment, trovato);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
else
ft.replace(R.id.fragment, new MainFragment());
ft.commit();
Senza il precedente codice, ruotando il dispositivo durante la visualizzazione del secondo Fragment, alla ricostruzione dell’Activity verrebbe mostrato il primo Fragment costringendo l’utente a dover ripristinare il contenuto che stava leggendo un attimo prima.
Altro aspetto che gestiamo è l’apparizione dell’icona a forma di freccia verso sinistra sull’ActionBar che si utilizza per la navigazione Up. Essa apparirà solo mentre il secondo Fragment è sullo schermo e gestiremo la sua apparizione con l’invocazione:
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mentre l’evento di click su di essa andrà gestito come una normale action:
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
getSupportFragmentManager().popBackStack();
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
break;
}
return false;
}
Il metodo popBackStack esegue l’inverso di addToBackStack visto prima: estrae dal BackStack l’ultimo Fragment archiviato.
Inoltre aggiungiamo l’overrride del metodo onBackPressed che permette di personalizzare la pressione del pulsante Back: quello che vi inseriremo noi sarà la disabilitazione della frecciolina di navigazione Up:
public void onBackPressed() {
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
super.onBackPressed();
}
Il secondo Fragment
Il secondo Fragment mostra ancora il metodo onCreateView in cui viene preparata l’interfaccia ma troviamo anche un altro metodo del ciclo di vita, onCreate, che -analogamente a quello utilizzabile nelle Activity – gestisce la creazione del Fragment e viene invocato prima di onCreateView.
L’aspetto interessante di questo Fragment consiste nell’utilizzo del metodo newInstance. Vediamo il codice completo:
public class CittaFragment extends Fragment {
public static final String CITTAFRAGMENT_TAG="CITTAFRAGMENT_TAG";
private static final String ARG_ID_IMG = "img";
private static final String ARG_NOME_CITTA = "nome";
private static final String ARG_DESCR_CITTA = "descr";
private String nomeCitta;
private String descrizioneCitta;
private int idImg;
public static CittaFragment newInstance(ElencoCitta.Citta citta) {
CittaFragment fragment = new CittaFragment();
Bundle args = new Bundle();
args.putInt(ARG_ID_IMG, citta.immagine);
args.putString(ARG_NOME_CITTA, citta.nome);
args.putString(ARG_DESCR_CITTA, citta.presentazione);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
idImg = getArguments().getInt(ARG_ID_IMG);
nomeCitta = getArguments().getString(ARG_NOME_CITTA);
descrizioneCitta = getArguments().getString(ARG_DESCR_CITTA);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v=inflater.inflate(R.layout.fragment_citta, container, false);
ImageView imageView= (ImageView) v.findViewById(R.id.imageView);
TextView txt_nome= (TextView) v.findViewById(R.id.citta);
TextView txt_descr= (TextView) v.findViewById(R.id.descrizione);
imageView.setImageResource(idImg);
txt_nome.setText(nomeCitta);
txt_descr.setText(descrizioneCitta);
return v;
}
}
Quando creiamo il Fragment vogliamo passargli dall’Activity i dati da mostrare che consistono nell’id dell’immagine da visualizzare, il nome della città e la breve descrizione. Normalmente, lo faremmo con un costruttore con parametri ma questa pratica è sconsigliata e scoraggiata. Piuttosto si preferisce creare un metodo statico – per convenzione chiamato newInstance – che riceva i parametri necessari e restituisca un Fragment secondo il pattern Factory. Prima di restituire il Fragment il metodo newInstance avrà cura di inserirvi i dati necessari con setArguments. Questi potranno essere recuperati con getArguments nel metodo onCreate.
I parametri passati vengono inseriti in un Bundle. Nel nostro caso dovevamo passare un oggetto Citta che però non può diventare parte di un Bundle a meno che non venga reso Serializable o Parcelable. Quest’ultima opzione soprattutto sarebbe una buona prassi ma per non complicare troppo l’esempio abbiamo preferito estrarre dall’oggetto il numero intero e le due stringhe ed inserirle nel Bundle. In alternativa, avremmo potuto rinunciare all’uso degli arguments di un Fragment creando un metodo – tipo setter – cui passare direttamente l’oggetto di classe Citta.
Conclusioni
Benchè l’esempio mostrato sia piuttosto semplice ha il pregio di illustrare l’uso dei Fragment in una modalità che non si accontenta di presentare solo gli aspetti più essenziali ma di indagare le vere questioni legate alla navigazione attraverso l’applicazione che vanno affrontate necessariamente in casi reali. I Fragment non appaiono spesso una tematica molto intuitiva e proprio per questo abbiamo deciso di dedicarvi alcuni articoli di approfondimento: questo che vi abbiamo offerto è il primo ma non ne mancheranno altri.














No Responses to “Android: capire il funzionamento dei Fragment”