Android Fragments - Creare interfacce utente dinamiche

La capacità di Android di invadere i mercati più variegati dell'elettronica di consumo - telefonia, fotocamere, tablet, TV - ha messo gli sviluppatori di fronte alla necessità di progettare interfacce utente più modulari e riutilizzabili per permetterne l'adattamento a dispositivi eterogenei. Molti interrogativi emersi in proposito hanno trovato risposta nei fragments.

Android Fragments: cosa sono e come si usano?

I fragments nel tempo si sono meritati un posto di spicco nel mondo della programmazione Android tanto da essere inseriti come parte integrante del framework nella versione 3.0.

La progettazione di un'interfaccia utente incentrata su questo meccanismo ha come primo obiettivo la modularità. Un fragment infatti occupa una porzione di Activity, ma non svolge il ruolo di semplice "raggruppamento di View", bensì appare come una sotto-applicazione dotata un suo ciclo di vita.

Ciò lo rende potenzialmente separabile dagli altri fragments della stessa interfaccia o riutilizzabile per conto proprio nonchè congiuntamente a nuove componenti.

Come primo approccio all'uso dei fragments, si pensi ad un'interfaccia utente come quella riportata in figura.

Il caso è semplice: sulla sinistra c'è una "pulsantiera" fatta di diversi "<Button>" ognuno dei quali riporta il nome di una grande metropoli del mondo -  tramite l'attributo android:text (vedi tutorial Anatomia di un linear layout con testo immagine e bottone cliccabile), sulla destra invece appare una scheda di dettaglio. Come presumibile, al click di un pulsante, la scheda sulla destra viene aggiornata con informazioni relative alla città prescelta.

A livello di layout, le due sezioni dell'Activity vengono rappresentate con dei fragments, rispettivamente detti LeftFragment e CentralFragment come evidenziato in figura dai rettangoli rossi. L'esempio non è dei più elaborati, ma mostra comunque tutti gli elementi fondamentali dell'utilizzo dei fragments: ciclo di vita, modularità e collaborazione reciproca.

Uno dei primi nodi da sciogliere nell'apprendimento dei fragments è proprio il rapporto che intercorre tra essi e le classiche Activity.

Fragments e Activity: rapporto e differenze

Un'interfaccia utente che mostra la presenza di più fragments nel layout continua pur sempre ad avere una sola Activity che svolge il ruolo di contenitore, cabina di regia della comunicazione tra fragments.

Un fragment, in quanto parte di un'Activity, non può avere un ciclo di vita separato da essa ma deve, in un certo senso, condividerne le sorti. Come sappiamo, le Activity attraversano vari stati: ACTIVE, PAUSED,STOPPED e INACTIVE.

Parallelamente, anche i fragments passano da uno stato all'altro ed ogni transazione significativa, viene notificata tramite eventi che il programmatore ha la possibilità di gestire mediante override dei metodi di callback.

Durante l'attivazione di un'Activity, i fragments vivono varie fasi denotate, tra gli altri, dai seguenti metodi:

  • onAttach(Activity a): avviene il collegamento funzionale tra il fragment e l'activity che lo possiede. E' il momento in cui il fragment può salvare il riferimento all'Activity passato come parametro formale. Ciò al fine di invocarne metodi come nel caso della comunicazione con gli altri fragments;
  • onCreateView: si sta creando l'aspetto grafico del fragment. Avvengono qui tutte le operazioni di configurazione delle varie View e dei listener per la gestione degli eventi: praticamente ciò che si fa normalmente nel metodo onCreate delle Activity;
  • onActivityCreated: il fragment viene notiziato del completamento dell'Activity. Da ora in poi è possibile invocarla tramite i metodi.

Per il resto il fragment segue il destino dell'Activity che lo contiene sia che essa stia transitando nello stato di PAUSE, di STOP o si stia procedendo alla sua distruzione.

Progettazione tramite fragments

Un'activity basata sui fragments ha bisogno di alcuni elementi ora definiti tramite estensione di classi Java ora tramite file XML:

  • un'Activity che svolge il ruolo di contenitore dei fragments;
  • un layout dell'interfaccia utente nel suo complesso, definito in XML, che descrive il posizionamento dei fragments tramite appositi tag;
  • un layout XML per ogni fragment;
  • una classe derivata da android.app.Fragment che contenga i metodi di callback per la gestione del ciclo di vita del fragment.

Il layout dell'Activity "padre" ossia il contenitore di entrambi i fragment, potrebbe essere del tipo:

<LinearLayout 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:orientation="horizontal"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >
    <fragment android:name="com.example.fragments.LeftFragment"
        android:layout_width="0dp"
    	android:layout_height="match_parent"
    	android:layout_weight="1"
    	android:id="@+id/leftfrag"/>
    <fragment android:name="com.example.fragments.CentralFragment"
        android:layout_width="0dp"
    	android:layout_height="match_parent"
    	android:layout_weight="2"
    	android:id="@+id/centralfrag"/>
</LinearLayout>

Ciò è quello che avviene di solito: un LinearLayout (già trattato in un precedente tutorial) contiene due elementi di tipo fragment, che puoi pensare come a 2 scatole, le cui caratteristiche sono specificate tramite i consueti attributi di layout.

Da notare come il "peso" di ogni fragment nella pagina - in quanto a spazio occupato - sia definito tramite gli attributi layout_weight. In questo caso, il centralfrag avrà una dimensione di 2/3 dello spazio a disposizione, mentre il leftfrag di 1/3.

Osserviamo il layout del LeftFragment, ossia del contenitore in cui andremo a inserire una serie di bottoni legati alle diverse città. Per prima cosa puoi notare come sia stato usato un RelativeLayout , sebbene lo stesso effetto lo si possa ottenere con LinearLayout o altre strutture.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
	<Button
            android:id="@+id/city1"
            android:layout_width="@dimen/button_width"
    		android:layout_height="wrap_content"
    		android:layout_alignParentLeft="true"
    		android:layout_marginTop="@dimen/margin_top_parent"
    		android:tag="GMT+2"
            android:text="Roma"
             />
    <Button
            android:id="@+id/city2"
            android:layout_width="@dimen/button_width"
    		android:layout_height="wrap_content"
    		android:layout_alignParentLeft="true"
    		android:layout_below="@+id/city1"
    		android:layout_marginTop="@dimen/margin_top"
    		android:tag="GMT-4"
            android:text="Washington"/>
    <Button
            android:id="@+id/city3"
            android:layout_width="@dimen/button_width"
    		android:layout_height="wrap_content"
    		android:layout_below="@+id/city2"
    		android:layout_alignParentLeft="true"
    		android:layout_marginTop="@dimen/margin_top"
    		android:tag="GMT+9"
            android:text="Tokyo"/>
    <Button
            android:id="@+id/city4"
            android:layout_width="@dimen/button_width"
    		android:layout_height="wrap_content"
    		android:layout_below="@+id/city3"
    		android:layout_alignParentLeft="true"
    		android:tag="GMT+4"
    		android:layout_marginTop="@dimen/margin_top"
            android:text="Mosca"/>
</RelativeLayout>

E' bene sottolineare che questa "pulsantiera" costruita con Button inseriti in un RelativeLayout non rappresenta la soluzione migliore da adottare in un fragment simile. Sicuramente più adeguato sarebbe stato l'utilizzo di ViewGroup per i dati come l'accoppiata ListView-Adapter. Non lo si è usato in questo esempio solo per semplicità.

NB: Osserva come in ogni Button sia stata inserita un'informazione "nascosta" in un attributo tag. Una sorta di campo input hidden come avviene nei classici moduli web delle pagine html. Questo dato ci servirà per inviare informazioni al fragment centrale ossia quello denominato "centralfrag".

Il fragment dedicato ai contenuti con identificatore "centralfrag", non riporta alcunchè di nuovo, solo alcune TextView ed l'elemento TextView per l'inserimento dell'immagine, aggiornata in dinamico tramite il metodo java setImageDrawable (vedi riga 30 del codice java successivo CentralFragment.js)

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
<TextView 
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="@dimen/margin_top"
    android:textSize="35sp"
    android:id="@+id/city"
    android:text=""
    />
<ImageView android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/photo"
    android:layout_alignLeft="@+id/city"
    android:layout_marginTop="@dimen/margin_top_parent"
    />
<TextView 
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignRight="@+id/city"
    android:textAlignment="center"
    android:layout_marginTop="@dimen/margin_over_time_info"
    android:layout_below="@+id/city"
    android:textSize="20sp"
    android:id="@+id/time"
    android:text=""/>
<TextView 
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignRight="@+id/city"
    android:layout_marginTop="@dimen/margin_top"
    android:layout_below="@+id/time"
    android:textSize="20sp"
    android:id="@+id/timezone"
    android:text=""/>
</RelativeLayout>

Il codice Java contenuto nelle classi per la gestione dei fragments, CentralFragment e LeftFragment, viene mostrato congiuntamente per offrire la possibilità di osservare assonanze e divergenze.

Qui sotto il codice che chiamerò CentralFragment.js, il quale non farà altro che iniettare nei campi predisposti nel layout, i valori che "qualcuno" fornirà:

// file CentralFragment.js
public class CentralFragment extends Fragment
{
	@Override
	  public View onCreateView(LayoutInflater inflater, ViewGroup container,
	      Bundle savedInstanceState) {
	    View view = inflater.inflate(R.layout.central, container, false);
	    return view;
	  }
	public void showCityDetails(String city)
	{
		Calendar cal=Calendar.getInstance();
		SimpleDateFormat simple=new SimpleDateFormat("HH:mm",Locale.ITALIAN);
		
        CityDetailsSingleton details=CityDetailsSingleton.getInstance();
		String timezone=details.get(city);
		
        simple.setTimeZone(TimeZone.getTimeZone(timezone));
		TextView txt=(TextView) getView().findViewById(R.id.city);
		txt.setText(city);
		txt.setBackgroundColor(getResources().getColor(R.color.grey));
		txt=(TextView) getView().findViewById(R.id.time);
		txt.setText("Orario attuale: "+simple.format(cal.getTime()));
		
        txt=(TextView) getView().findViewById(R.id.timezone);
		txt.setText("Fuso orario: "+timezone);
		
        int resID = getResources().getIdentifier(city.toLowerCase() , "drawable", getView().getContext().getPackageName());		
        Drawable drawable = getResources().getDrawable(resID );
		ImageView img=(ImageView) getView().findViewById(R.id.photo);
		img.setImageDrawable(drawable );
	}
}

Qui sotto il codice che chiamerò LeftFragment.js, il quale non farà altro che gestire il click sui diversi bottoni della pulsantiera (vedi tutorial Gestire il tocco su di un bottone), e inviare i dati della citta e il campo nascosto con attributo tag, ad un'apposito metodo, che ho chiamato "choose" definito successivamente, nel corpo dell'activity principale:

// file LeftFragment.js
public class LeftFragment extends Fragment
{
	private OnSelectionListener listener;
	
	public interface OnSelectionListener
	{
		void choose(String cityName);
	}	

	@Override
	public void onAttach(Activity activity) {
		super.onAttach(activity);
		MainActivity main=(MainActivity) activity;
		listener=main;
	}
	@Override
	  public View onCreateView(LayoutInflater inflater, ViewGroup container,
	      Bundle savedInstanceState) {
	    View view = inflater.inflate(R.layout.left,
	        container, false);
	    Button btn=(Button) view.findViewById(R.id.city1);
	    btn.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) 
			{
				manageClickEvent((Button) v);
			}
		});
	    btn=(Button) view.findViewById(R.id.city2);
	    btn.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) 
			{
				manageClickEvent((Button) v);
			}
		});
	    btn=(Button) view.findViewById(R.id.city3);
	    btn.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) 
			{
				manageClickEvent((Button) v);
			}
		});
	    btn=(Button) view.findViewById(R.id.city4);
	    btn.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) 
			{
				manageClickEvent((Button) v);
			}
		});
	    return view;
	  }
	private void manageClickEvent(Button btn)
	{
		String city=btn.getText().toString();
		String tag=btn.getTag().toString();
		CityDetailsSingleton details=CityDetailsSingleton.getInstance();
		details.put(city, tag);
		listener.choose(city);
	}	
}

In merito, osserviamo che:

  • il codice del metodo onCreateView è molto semplice in quanto svolge normali operazioni di inflating di layout e definizione della gestione degli eventi;
  • a completamento della classe CentralFragment, diciamo solo che il metodo showCityDetails serve a inserire le informazioni relative alla metropoli selezionata nelle varie TextView;
  • viene richiamata più volte una classe CityDetailsSingleton che non è altro che un Singleton incaricato di gestire un HashMap che funziona da cache per le informazioni già ricavate sulle metropoli selezionabili nel fragment sinistro.

Oltre a queste prime annotazioni è bene notare che LeftFragment include aspetti molto interessanti perchè definiscono un modo "pulito" di chiamare in causa l'Activity nel dialogo tra fragments.

Infatti, viene definita un'interfaccia interna alla classe per conferire il ruolo di "listener" a chi la implementa. Ciò perchè, ai fini della riutilizzabilità dei componenti, il fragment deve conoscere il meno possibile il resto dell'applicazione pertanto non è bene neanche che abbia i riferimenti per recuperare l'altro fragment, il centralfrag.

Quindi il ruolo di comunicazione tra i due componenti verrà svolto dall'Activity che li contiene che implementerà l'interfaccia OnSelectionListener e che verrà contattata dall'interno di LeftFragment ad ogni click su un pulsante. Il riferimento dell'Activity verrà catturato dal fragment all'interno del metodo onAttach preposto per l'appunto a mettere a contatto un fragment con l'Activity contenitore.

La notifica dell'evento click, dal fragment all'Activity, avverrà all'interno del metodo manageClickEvent e consisterà, secondo il paradigma del listener, nella semplice invocazione di un metodo, dichiarato nell'interfaccia ed implementato nell'Activity: choose in questo caso.

Infine, la classe MainActivity.js, l'unica Activity che svolge il ruolo complessivo dell'interazione con l'utente:

// file MainActivity.js
public class MainActivity extends Activity implements OnSelectionListener
{
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
	}
	public void choose(String cityName) 
	{	
		CentralFragment centralFragment=(CentralFragment) getFragmentManager().findFragmentById(R.id.centralfrag);
		centralFragment.showCityDetails(cityName);
	}
}

Anche questo codice appare molto semplice. Si vede come l'Activity implementi l'interfaccia OnSelectionListener dando corpo al metodo choose il quale riceve una stringa, riportante il testo del pulsante cliccato, e passandolo al fragment di classe CentralFragment. Per completare questa azione, viene chiamato in causa il cosiddetto FragmentManager, un componente che serve per recuperare riferimenti ai vari fragments in base all'id assegnato loro. 

Tiriamo le somme sui fragments: i vantaggi

La descrizione del semplicissimo esempio appena riportato mostra che i fragments introducono duttilità e riutilizzabilità delle componenti senza invasività.
Infatti:

  • non è stato in realtà necessario introdurre troppi elementi tecnici nuovi. Se escludiamo i tag XML e l'estensione delle classi Fragment per il resto si è trattato solo di gestire layout, eventi e widget Android nella consueta maniera delle Activity;
  • i layout ed i funzionamenti contenuti nei fragment sono del tutto riutilizzabili in altre applicazioni in quanto i fragment tra loro non "si conoscono". Il dialogo non lo instaurano direttamente ma affidano i messaggi funzionali all'Activity che infatti è l'unica a fare accesso al FragmentManager.

Conclusioni e sviluppi

I fragments sono un argomento ampio che non può essere esaurito in un tutorial solo. Sicuramente ciò che è stato mostrato, sebbene in termini semplificati, evidenzia i meccanismi base di definizione di un'interfaccia utente fragment-based e le vie di comunicazione tra fragment che non minino la riutilizzabilità dei componenti.

Per proseguire in maniera più approfondita lo studio, si consideri che i fragments, come sempre nella filosofia Android, non sono solo costruibili via XML ma anche, in modo programmatico, tramite classi Java. In questo caso, è interessante notare che i Fragments possono essere oggetto di varie operazioni di manipolazione quali sostituzione, cancellazione, inserimento da svolgere in maniera coerente tramite un meccanismo di transazioni, simile a quello che difende l'integrità dei dati in un database.

Infine, si consideri che una pratica utile da applicare, sempre per risolvere il problema della frammentazione dello scenario di dispositivi Android, è che i layout impiegati nei fragments possono essere riutilizzati in Activity diverse invocate tramite Intent. Quest'ultimo caso si presta molto all'adattabilità di applicazioni su display più piccoli quali quelli degli smartphone più semplici o per gestire la variazione di orientation anche per i tablet.

Tipo/Autore: Pubblicato da: CorsoAndroid.it

© 2011-2024 CorsoAndroid.it - Tutti i diritti riservati. Corso Base Android per rompere il ghiaccio Creare app per android
NB: Tutti i marchi citati sono di proprietà dei rispettivi proprietari. Android is a trademark of Google Inc.