Android Lezione 14 – I fragment

In questa lezione vedremo una delle feature introdotte con le versioni più recenti di android, per essere precisi con la versione 3. Si tratta dei Fragment, un altro modo per gestire la parte grafica della nostra applicazione, utile sopratutto se abbiamo layout specifici per tablet (ma non solo come vedremo).

Si tratta in pratica (secondo la definizione ufficiale) di una porzione dell’interfaccia grafica di un activity. Possiamo combinare più Fragment all’interno della stessa activity se vogliamo, come possiamo riusare lo stesso fragment all’interno di diverse Activity. In pratica si tratta di un “modulo” dell’activity, di una sotto sezione.

Questa caratteristica lo rende appuntio molto utile per gestire diversi tipi di schermi, rendendo l’interfaccia grafica della nostra applicazione più modulare.

É importante notare, che anche se è stata introdotta a partire dalla versione te, le caratteristiche dei fragment sono sfruttabili anche in versioni precedenti di android, utilizzando le support-library, che sono dei pacchetti jar, che contengono alcune features delle nuove versioni di android, utilizzabili su dispositivi meno recenti (da android 1.6 in su, prossimamente farò una lezione su come includere le support library nel vostro progetto).

In questo capitolo vedremo come sviluppare una applicazione con due layout distinti, uno specifico per smartphone, e uno specifico per tablet. I due layout saranno strutturati nella seguente maniera:

  • La versione smartphone avrà una prima schermata di menù, e poi in base alla selezione dell’utente si apriranno schermate distinte
  • La versione tablet avrá un menù sulla sinistra fisso con tre elementi, e nella parte destra verranno caricate le varie schermate in base alla selezione dell’utente (che avranno al loro interno delle semplici textView).

Quello che avremo sará che il layout per smartphone avrà i due fragment in due schermate distinte, mentre la seconda li conterrá entrambi all’interno della stessa chermata. Graficamente:

Fig 1. Fragments

Ifragment si dichiarano in un file xml con i tag:

<fragment ...>
   ...
</fragment>

A livello di layout dichiarare un fragment, è in come inserire un placeholder per una parte di interfaccia grafica, che verrà riempita eventualmente in un secondo momento, o da qualcos’altro. Man mano che leggerete capirete cosa vuol dire.

A differenza di un activity, un fragment non va dichiarato all’interno del Manifest, ma direttamente nel layout, come qualsiasi altro componente dell’interfaccia grafica.

Il contenuto del fragment, è gestito in maniera del tutto analoga a quello dell’activity, avremo un file xml nel quale andremo a inserire i nostri layout, componenti, etc. Anche la scelta di eventuali layout in base alla dimensione dello schermo sono gestiti in maniera del tutto analoga a quello delle Activity.

Mentre le classi che gestiscono i fragment, saranno sotto classi del tipo Fragment:

public class MyFirstFragment extend Fragment {
...
}

Anche se i fragment seguono il ciclo di vita di un Activity, internamente hanno un loro ciclo di vita, che è riassunto nel seguente schema:

Fig 2. LifeCycle di un fragment

Il metodo che verrá chiamato quando il fragment è pronto per disegnare l’interfaccia grafica é onCreateView.

Iniziamo quindi a creare la nostra applicazione. Partiamo dalle tre schermate che possono essere mostrate all’utente, poi vedremo la lista.

Dovremo creare quindi tre classi che estendono fragment, chiamiamole per esempio MyFirstFragment, MySecondFragment, MyThirdFragment, e tre layout (nel mio esempio: my_first_fragment.xml, my_second_fragment.xml, my_third_fragment.xml) in entrambi i casi dato che l’unica differenza in queste è il nome della classe e del layout, e il layout ve ne mostro una soltanto.

Come abbiamo appena visto, il metodo nel quale il fragment è pronto per caricare l’interfaccia grafica è onCreateView,  implementiamolo quindi nella nostra classe:

public static class MyFirstFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.my_first_fragment, container, false);
    }
}

Vediamo gli argomenti del metodo:

  • LayoutInflater inflater: questo oggetto ci permette di istanziare un layout XML all’interno di una view
  • ViewGroup container: in questo caso si tratta del parent che conterrá il layout
  • Bundle savedInstanceBundle: contiene eventuali argomenti che sono stati passati al fragment.

La chiamata

inflater.inflate(R.layout.my_first_fragment, container, false);

carican un layout per il nostro fragment all’interno del parent container chiamato my_fragment, quindi se non lo abbiamo ancora fatto dobbiamo creare un file layout xml (o più di uno se dobbiamo gestire diversi tipi di schermo o abbiamo diversi fragment). Il layout dei fragment è uguale in tutto e per tutto a quello delle activity.

Ora creiamo un layout estremamente semplice, che conterrá una semplice casella di testo:

<!--?xml version="1.0" encoding="utf-8"?-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white"
    android:orientation="vertical" >
 
    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/first_f" />
 
</LinearLayout>

Ripetiamo la stessa operazione per MySecondFragment e MyThirdFragment, Ovviamente per ognuna di questi creiamo anche un relativo file xml per il layout.

Passiamo ora alla lista dell opzioni. Utilizziamo una ListView per creare la lista. Anche i fragment hanno una classe specializzata per gestire le ListView, il ListFragment, che funziona in maniera molto simile alla ListActivity. Quindi creiamo per ora una classe che estende ListFragment. In questo esempio la chiamo ChoiceFragment:

public class ChoiceFragment extends ListFragment {
	private boolean isTablet = true;
 
	public View onCreateView(LayoutInflater inflater, ViewGroup container,
			Bundle savedInstanceState) {
		return inflater.inflate(R.layout.fragment_list, container, false);
	}
}

Fra poco vedremo anche come gestire la transizione da un fragment all’altro. Ora pensiamo a creare il layout per questa classe, che chiameremo fragment_list:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin">
 
    <ListView
        android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:entries="@array/ListViewEntries" >
    </ListView>
</RelativeLayout>

Prima di proseguire facciamo un piccolo riassunto:

  • Abbiamo creato tre classi Fragment che gestiranno le schermate della nostra applicazione
  • Abbiamo creato un ulteriore classe Fragment che gestisce una ListView con la lista delle opzioni della nostra App
  • Per ognuna delle classi sopra abbiamo creato i relativi layout.

Fate attenzione a non cambiare l’id della ListView, questo serve.

Ma ci manca ancora qualcosa. Ricordiamo il nostro scopo iniziale! Ovvero avere due interfaccie grafiche, una per i tablet (vedi figura 1 lato sinistro) con menu sulla sinistra e schermata sulla destra, e l’altra  per i telefonini (Fig.1 lato destro).

Quello che abbiamo visto fino ad ora si può utilizzare per l’interfaccia degli smartphone. Ma come facciamo per i tablet? Prima di tutto creiamo una nuova cartella all’interno di res, chiamata layout-large. All’interno di questa andremo a mettere il layout per i tablet. Prima di proseguire una brevissima spiegazione di questa cartella.

Android automaticamente seleziona il layout più adatto al nostro schermo se disponibile, altrimenti carica quello che considera di default. Ma come fa a sapere quale è di default e quale non lo è?

  • Android suddivide gli schermi in 4 categorie: xsmall, small, large e xlarge. Quindi quando carica un layout prima lo cerchèra in una cartella che si chiama layout-categoriaeschermo (dove categoriaschermo è appunto una delle quattro specificate sopra). Se lo trova caricherá quello.Ovviamente per essere trovato il layout in queste cartelle deve avere lo stesso nome (e gli stessi oggetti molto probabilmente) di quello che si trova nella cartella layout se non volete correre il rischio di trovarvi svariate eccezioni.
  • Se non ha trovato nulla nella cartella sopra, allora si carica quello di default in /layout

Dobbiamo creare un quarto layout che sará il contenitore dell’interfaccia grafica della nostra app. Questo dovrà essere fatto in due versioni una per gli smartphone normali e una per i tablet (quindi il primo lo salveremo in /res/layout e il secondo in /res/layout-large). Nel nostro esempio il nome del file xml per il layout sará fragment_main.xml

Le due versioni ovviamente saranno leggermente diverse, vediamo prima quella per smartphone:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/fragment_container"
 />

A cosa serve questo layout vuoto? Questo conterrá i vari fragment man mano che vengono caricati.

Passiamo a quello per il formato tablet (ricordate va salvato in layout-large con lo stesso nome):

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:baselineAligned="false"
    android:layout_width="match_parent"
    android:layout_height="match_parent" 
    android:id="@+id/fragment_container">
 
	<fragment android:name="com.italialinux.fragmentexample.ChoiceFragment"
              android:id="@+id/menu_fragment"
              android:layout_weight="1"
              android:layout_width="0dp"
              android:layout_height="match_parent" />
	<fragment android:name="com.italialinux.fragmentexample.MyFirstFragment"
              android:id="@+id/container_fragment"
              android:layout_weight="1"
              android:layout_width="0dp"
              android:layout_height="match_parent" />
</LinearLayout>

Come possiamo vedere i due casi sono molto diversi. Nel primo abbiamo un layout vuoto, nel secondo invece abbiamo due tag “fragment”. Questo tag server caricare un istanza di un fragment all’interno di uno specifico layout.
L’attributo android:name, indica la classe che deve caricare quel fragment.

Restano ancora due cose per terminare l’applicazione:

  • Caricamento del contenuto del fragment
  • Gestione della selezione nella listview (che si occuperá della transizione da un fragment all’altro).

Vediamo il caricamento del fragment. Il caricamento viene fatto da una classe di tipo FragmentActivity (che come si può intuire dal nome è una specializzazione dell’Activity, che avrá dei metodi che ci aiuteranno nella gestione dei fragment). Se usate eclipse probabilmente vi avrá automaticamente creato una classe MainActivity, invece di crearne una nuova partiamo da questa, ma assicuriamoci che invece di estendere Activity estenda FragmentActivity:

public class MainActivity extends FragmentActivity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.fragment_main);
    }
 
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }
 
}

Ovviamente questa activity dovrá caricare il layout chiamato fragment_main, quindi sostituiamo il parametro di setContentView con R.layout.fragment_main.

Ora questa forse è la parte che genera più confusione, quindi cercherò di essere il più dettagliato possibile. Poco sopra abbiamo creato due layout differenti per i tablet e gli smartphone. Il primo ha due tag fragment al suo interno, il secondo invece ha un framelayout vuoto.

Vediamo i due casi distintamente:

  • Se siamo nel layout tablet, non dovremo fare nulla perchè, gia all’interno dell’xml abbiamo istruito l’applicazioni quali sono le classi che lui dovrá caricare per riempire lo spazio del relativo fragment (ricordate inflater.inflate?)
  • Se siamo nel caso smartphone invece dobbiamo istruire noi l’applicazione su cosa caricare, e in questo caso si tratterà ovviamente della lista delle opzioni.

Il problema potrebbe essere: come facciamo a sapere quale è il layout che stiamo utilizzando? Beh nel nostro caso è estremamente semplice: quello degli smartphone ha dichiartato un frameLayout che ha come attributo android:id:

android:id="@+id/fragment_container"

Il metodo findViewById ricordo che si usa per accedere agli oggetti del layout, e se l’oggetto non esiste ovviamente questo metodo torna null. Quindi per sapere se stiamo nel layout dello smartphone ci basta controllare che la findViewById su R.id.fragment_container non sia null.

Ma cosa dobbiamo fare una volta che sappiamo di stare nel caso smartphone? Semplicemente:

  1. Istanziamo la classe che si occupa di gestire il menu, in questo esempio si tratta di ChoiceFragment
  2. Chiediamo un istanza del FragmentManager e aggiungiamo il fragment che vogliamo caricare.

Modifichiamo quindi la nostra onCreate:

 

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.fragment_list);
        if(findViewById(R.id.fragment_container)!=null){
        	ChoiceFragment fragment = new ChoiceFragment();
        	Bundle bundle = new Bundle();
        	if(getArguments()!=null){
			this.isTablet = getArguments().getBoolean("isTablet", true);
		}
        	bundle.putBoolean("isTablet", false);
        	getSupportFragmentManager().beginTransaction().add(fragment, null).commit();
        }
    }

Notate che ho aggiunto un oggetto bundle, dove setto un parametro isTablet a false, questo mi servirá nel ListFragment durante la gestione delle transizioni da un fragment all’altro in quale modalitá sto, e scegliere il container giusto.
Siamo quasi al termine. Dobbiamo ora solo occuparci della gestione delle transazioni nel menu.
Di questo se ne occupa la classe ChoiceFragment, che ricordiamo estende ListFragment, che richiede semplicemente l’override del metodo onListItemClick, di seguito vi incollo il codice e poi ne spiegherò il funzionamento:

public void onListItemClick(ListView l, View v, int position, long id) {			
		String itemName = getListView().getItemAtPosition(position).toString();
		int container_id;
		if(isTablet){
			container_id = R.id.container_fragment;
		} else {
			container_id = R.id.fragment_container;
		}
		switch(position){
		case 0:
			getFragmentManager().beginTransaction()
			.replace(R.id.fragment_container, new MyFirstFragment())
			.addToBackStack("FirstFragment").commit();
			break;
		case 1:
			getFragmentManager().beginTransaction()
			.replace(R.id.fragment_container, new MySecondFragment())
			.addToBackStack("SecondFragment").commit();
			break;
		case 2:
			getFragmentManager().beginTransaction()
			.replace(R.id.fragment_container, new MyThirdFragment())
			.addToBackStack("ThirdFragment").commit();
			break;
		default:
			Toast.makeText(
			getActivity(), getListView().getItemAtPosition(position).toString(), 
			Toast.LENGTH_LONG).show();
			break;
		}		
		super.onListItemClick(l, v, position, id);
	}

Innanzi tutto controlliamo se siamo in un tablet o in uno smartphone, e decidiamo quale id usare.
Come si nota facilmente, mediante il costrutto switch/case individuiamo quale elemento della lista è stato selezionato, e scegliamo di conseguenza fra MyFirstFragment, MySecondFragment, e MyThirdFragment (o un caso non gestito).
Il comportamento è simile in tutti e tre i casi. La gestione della transizione da un fragment all’altro avviene nel seguente modo:

  • Prima di tutto otteniamo un istanza del FragmentManager, la classe che gestisce le transazioni fra i Fragment mediante getFragmentManager().
  • Chiediamo al manager di iniziare una nuova transazione  con beginTransaction() (questo metodo torna un oggetto di tipo FragmentTransaction.
  • Nella transazione corrente indichiamo che vogliamo sovrascrivere il contenuto di R.id.fragment_container (o R.id.container_fragment se siamo nel tablet) con il metodo replace
  • Indichiamo che vogliamo aggiungere questa transazione nel backstack (in pratica quando premeremo il pulsante indietro dal nostro device, questo ricorderà quale era l’ultimo fragment caricato)
  • Infine portiamo a termine la transazione con il metodo commit.

Abbiamo così creato la nostra prima applicazione che utilizza i fragment. Anche se ad un primo utilizzo sembrano ostici (anche io ancora ogni tanto mi confondo un pò) sono un ottimo strumento per gestire in maniera agevole diversi tipi di layout su diversi tipi di schermi.

Prima di chiudere un paio di considerazioni su questo articolo:

  • Dato che il mio scopo era spiegare i fragment, ho cercato di rendere il codice il più semplice possibile, usando qualche volta delle forzature (per esempio quando passo il parametro isTablet per indicare quale container scegliere per caricare il fragment.
  • Un altra forzatura che ho dovuto usare per evitare di rendere troppo complicato l’articolo è stato forzare lo sfondo nel layout tablet a bianco, altrimenti se non facevo così si vedevano i fragment sovrapposti (questo è causato dalla chiamata addToBackStack()).Molti esempi che ho trovato sulla rete, gestivano le transizioni fra fragments in due modi differenti, a seconda che si stava in un layout di tipo tablet o di tipo smartphone
  • I sorgenti li troverete su github, al seguente url: https://github.com/inuyasha82/ItalialinuxExample/

Alla prossima lezione.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.