Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial
  • Lezione 27 di 97
  • livello avanzato
Indice lezioni

Layout adattabili Smartphone/Tablet in Android

Un esempio pratico di utilizzo dei Fragment: un layout adattabile multi-pannello per smarphone e tablet.
Un esempio pratico di utilizzo dei Fragment: un layout adattabile multi-pannello per smarphone e tablet.
Link copiato negli appunti

I Fragments possono essere visti come blocchi componibili che permettono di rendere il layout adattabile al dispositivo. Se la frammentazione dei dispositivi rappresenta una problematica di rilievo per i programmatori Android, i Fragments rappresentano in buona parte una soluzione.

Layout smartphone e tablet per Android

La figura (fonte: documentazione ufficiale Android) mostra due dispositivi di tipo diverso ed in configurazioni differenti:

  • uno smartphone in portrait. Immaginiamolo con uno schermo piccolo, anche 3 pollici;
  • un tablet, quindi schermo almeno da 7 pollici, posizionato in landscape.

I layout presenti su entrambi sono costituiti da due fragments, gli stessi due Fragment: FragmentA e FragmentB.

Con adeguate configurazioni delle risorse e qualche aggiunta al codice visto nel capitolo precedente possiamo creare anche noi un layout adattabile che riesca a mostrarsi in one-pane su smartphone e two-pane su tablet in landscape.

Configurazione delle risorse

Parlando delle risorse, avevamo accennato alla loro gestione multipiattaforma. È arrivato il momento di vederla al lavoro, costituirà il punto di partenza del nostro layout adattabile.

Creiamo due cartelle di risorse layout:

  • layout-large-land che verrà usato solo per dispositivi con display large in posizione landscape;
  • layout, la cartella di default. Verrà chiamata in causa per tutte le altre situazioni.

La configurazione multipla ha successo se in entrambe le cartelle mettiamo il file di layout con il medesimo nome, activity_main.xml.

Da questo momento, l'Activity cercherà sempre la risorsa R.layout.activity_main ma questa, in base alla configurazione del dispositivo, corrisponderà ora al file res/layout-large-land/activity_main.xml ora al file res/layout/activity_main.xml.

Vediamo entrambi i file di layout.

File 1: res/layout/activity_main.xml:

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

Questo primo layout è identico a quello visto nella lezione relativa ai Fragment. È un FrameLayout che ospiterà un fragment singolo assegnato dinamicamente con FragmentTransactions.

File 2: res/layout-large-land/activity_main.xml:

<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment android:name="it.html.guida.gui.fragments.CountryFragment"
android:id="@+id/countryfrag"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<fragment android:name="it.html.guida.gui.fragments.CityFragment"
android:id="@+id/cityfrag"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"/>
</LinearLayout>

Nel layout two-pane, i Fragments appaiono insieme sin dall'inizio mentre le FragmentTransactions non dovranno più intervenire. Come segnaposti, abbiamo i tag <fragment>. Il loro attributo android:name indica quale tipo di Fragment dovrà posizionarsi in ogni collocazione.

Comunicazione tra Fragments

L'Activity svolge il ruolo di snodo funzionale e di comunicazione tra i due Fragments sia che essi appaiano contemporaneamente sia che si alternino sul display.

Affinchè i Fragments siano riutilizzabili in più contesti è necessario che non si conoscano tra loro né che conoscano l'Activity alla quale, comunque, devono essere collegati. “Massima coesione, minimo accoppiamento”: questo potrebbe essere lo slogan dei Fragments.

All'interno dei Fragment non verrà mai menzionata esplicitamente la classe di appartenenza dell'Activity .

Piuttosto verrà definita un'interfaccia che sarà implementata dall'Activity. Questa interfaccia costituirà il “protocollo” di comunicazione Fragment-Activity.

L'esempio: Paesi e città

L'esempio mette in pratica il classico modello master/detail. Il master è un fragment, classe CountryFragment, che mostra una lista di Paesi. La scheda detail invece è un CityFragment che mostra un elenco di città appartenenti al Paese selezionato nel master.

In base alle premesse iniziali, vogliamo che i Fragments si presentino accoppiati su schermi large in landscape e si alternino in tutti gli altri casi.

A scopo di esempio, la sorgente dati è fittizia. È totalmente contenuta in una classe CountryList, già usata nel capitolo sugli
Spinner che riportiamo qui per comodità:

public class CountryList
{
    private HashMap<String, ArrayList<String>> list;
    public CountryList()
    {
        list=new HashMap<String, ArrayList<String>>();
        ArrayList<String> cities=new ArrayList<String>();
        cities.add("Roma");
        cities.add("Torino");
        cities.add("Firenze");
        list.put("Italia", cities);
        cities=new ArrayList<String>();
        cities.add("Parigi");
        cities.add("Lione");
        cities.add("Marsiglia");
        list.put("Francia", cities);
        cities=new ArrayList<String>();
        cities.add("Madrid");
        cities.add("Barcellona");
        list.put("Spagna", cities);
    }
    public Collection<String> getCountries()
    {
        return list.keySet();
    }
    public Collection<String> getCitiesByCountry(String c)
    {
        return list.get(c);
    }
}

Se ne invocheranno i metodi:

  • Collection<String> getCountries(): restituisce l'elenco dei Paesi;
  • Collection<String> getCitiesByCountry(String country): restituisce un elenco di città situate nel Paese.

Selezionando un elemento nella lista dei Paesi, è necessario che l'elenco di città presente nell'altro fragment venga aggiornato. In tutto questo, l'Activity svolgerà il ruolo di mediatore.

public class CountryFragment extends ListFragment
{
    public interface OnFragmentEventListener
    {
        void selectCountry(String c);
    }
    private OnFragmentEventListener listener=null;
    private CountryList l=new CountryList();
    private String[] countries=null;
    public CountryFragment()
    {
        countries=new String[l.getCountries().size()];
        l.getCountries().toArray(countries);
    }
    @Override
    public void onAttach(Context context)
    {
        super.onAttach(context);
        listener=(OnFragmentEventListener) context;
    }
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
    {
        ArrayAdapter<String> adapter=new ArrayAdapter<String>((Context) listener,android.R.layout.simple_list_item_1,countries);
        setListAdapter(adapter);
        return super.onCreateView(inflater, container, savedInstanceState);
    }
    @Override
    public void onListItemClick(ListView lv, View v, int position, long id)
    {
        listener.selectCountry(countries[position]);
    }
}

Il CountryFragment mostra alcune particolarità:

  • contiene un'interfaccia OnFragmentEventListener. Se si osserva il metodo onAttach si vede che tale interfaccia verrà usata per riferirsi al Context;
  • è stato esteso ListFragment che funziona in maniera simile al ListActivity. All'interno dell'onCreateView viene impostato l'Adapter;
  • onListItemClick rappresenta il momento in cui si notifica all'Activity che è stato selezionato un Paese.

Il codice dell'Activity è il seguente:

public class MainActivity extends AppCompatActivity implements CountryFragment.OnFragmentEventListener {
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (findViewById(R.id.container) != null)
        {
/* Se è presente il FrameLayout con id container,
 vuol dire che siamo in SINGLE-PANE perciò
 è necessario aggiungere il Fragment con la transazione.
 Se savedInstanceState non è nullo, non siamo alla
 prima visualizzazione perciò non serve aggiungere il Fragment*/
            if (savedInstanceState != null)
                return;
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.container, new CountryFragment()).commit();
        }
    }
    @Override
    public void selectCountry(String c)
    {
        CityFragment cityFrag = (CityFragment)
                getSupportFragmentManager().findFragmentById(R.id.cityfrag);
        if (cityFrag != null && cityFrag.isInLayout())
        {
/* Il Fragment delle città è già nel layout quindi
 ne chiediamo solo l'aggiornamento*/
            cityFrag.onSelectedCountry(c);
        }
        else
        {
/* Siamo in SINGLE-PANE, quindi le FragmentTransaction
   operano lo switch tra Fragment*/
            CityFragment frag= new CityFragment();
            Bundle b=new Bundle();
            b.putString("country", c);
            frag.setArguments(b);
            FragmentTransaction ft=getSupportFragmentManager().beginTransaction();
            ft.replace(R.id.container, frag);
            ft.addToBackStack(null);
            ft.commit();
        }
    }
}

L'Activity si troverà esplicitamente a dover gestire l'esatta composizione del layout. Se si notano i commenti nel codice si vede come si procede:

  • onCreate: se nel layout è reperibile il layout con id R.id.container si è per forza in one-pane;
  • selectCountry: se si trova presente nel layout un CityFragment allora siamo in two-pane altrimenti si fa uso delle FragmentTransaction per reperirlo.

L'ultimo codice da mostrare è il CityFragment:

public class CityFragment extends ListFragment
{
private ArrayAdapter<String> adapter=null;
private CountryList l=new CountryList();
public CityFragment() {
}
@Override
public void onActivityCreated(Bundle savedInstanceState)
{
super.onActivityCreated(savedInstanceState);
adapter=new ArrayAdapter<String>(getActivity(),android.R.layout.simple_list_item_1);
setListAdapter(adapter);
Bundle b=getArguments();
if (b!=null)
{
String c=b.getString("country");
onSelectedCountry(c);
}
}
public void onSelectedCountry(String country)
{
adapter.clear();
adapter.addAll(l.getCitiesByCountry(country));
}
}

Al suo interno, la richiesta di aggiornamento del layout viene fatta mediante l'invocazione di onSelectedCountry. Notare inoltre l'utilizzo degli arguments per passare valori nella comunicazione Fragment-Activity.

Conclusioni

Sicuramente questo è un esempio molto basilare ma che vuole fornire i rudimenti per poter creare applicazioni più complesse rese flessibili dall'attuazione delle regole viste sinora:

  • Fragments collegati tra loro il minimo possibile per permetterne un maggior riutilizzo;
  • Activity usata per lo più come snodo di comunicazione tra Fragments;
  • risorse in grado di saparare all'origine i layout distinguendo con id diversi i contenitori dei Fragments;
  • ogni classe Fragment deve avere sin dalla nascita uno scopo ben preciso e contenere in sé tutta la logica necessaria per raggiungerlo in maniera indipendente.

Ti consigliamo anche