Master-Detail. Ventana master. List View con layout default y custom.

Queremos visualizar una lista de películas y al hacer click sobre una nos interesa ver su información completa.

Crear un proyecto Master/Detail

Generamos un nuevo proyecto: File > New > New Project... elegimos un nombre representativo "PeliculasApp", el company name. Luego elegimos el dispositivo destino (Phone and Tablet). 

Entonces elegimos como tipo de proyecto un "Master / Detail Flow" y configuramos:
  • Object Kind: "Pelicula"
  • Object Kind Plural: "Peliculas"
  • Title: "Películas"

Activities y Fragments

Al finalizar la actividad, vemos que se generaron 4 vistas:
  • PeliculaListActivity
  • PeliculaListFragment
  • PeliculaDetailActivity
  • PeliculaDetailFragment
El fragment permite bajar la granularidad de la actividad en partes más pequeñas. La activity puede contener uno o más fragments. De esa manera podemos trabajar los componentes visuales de diferente manera para un smartphone o una tablet. Por el momento sabemos que 
  • la activity PeliculaList define 
    • el título, 
    • los action buttons, en principio ninguno, 
    • y la navegación. Por el momento pensemos en una aplicación para smartphones, entonces la navegación consistiría en que cuando el usuario selecciona una película eso dispara una actividad nueva donde se muestra el detalle de la película (PeliculaDetailActivity + PeliculaDetailFragment). Más adelante veremos que esta separación actividad / fragmento permite combinarlos para diferentes dispositivos.
  • el fragment PeliculaList define la vista con la lista de películas

Lista de Películas

De movida podemos ejecutar la aplicación gracias a todo el código boilerplate generado:



La lista se define en la activity_peliculas_list.xml:

<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:id="@+id/pelicula_list"
    android:name="org.uqbar.peliculasapp.PeliculaListFragment" android:layout_width="match_parent"
    android:layout_height="match_parent" android:layout_marginLeft="16dp"
    android:layout_marginRight="16dp" tools:context=".PeliculaListActivity"
    tools:layout="@android:layout/list_content" />
activity_peliculas_list.xml

El layout default es list_content, que tiene una ListView.

¿Dónde se llena la lista de elementos de la ListView? En el Fragment de la lista de películas, que extiende especialmente de ListFragment:

public class PeliculaListFragment extends ListFragment {

Y específicamente en el método onCreate:

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // TODO: replace with a real list adapter.
        setListAdapter(new ArrayAdapter<DummyContent.DummyItem>(
                getActivity(),
                android.R.layout.simple_list_item_activated_1,
                android.R.id.text1,
                DummyContent.ITEMS));
    }

Reemplazamos entonces DummyContents.ITEMS por una lista de películas de un Repositorio creado para la ocasión:

        setListAdapter(new ArrayAdapter<Pelicula>(
                getActivity(),
                android.R.layout.simple_list_item_activated_1,
                android.R.id.text1,
                RepoPeliculas.getInstance().getPeliculas(null, 10)));

En esta versión muy sencilla, cada línea muestra el toString() redefinido en película:


(pueden probar cambiar el método toString() en Pelicula).

Otra variante

Podemos construir nuestro propio fragmento custom, para lo cual hacemos algunos cambios:

En la actividad reemplazamos al fragmento con el layout predefinido por un fragment incluido en un Linear Layout:

<?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:orientation="vertical" >

    <fragment
        android:id="@+id/fragment1"
        android:name="org.uqbar.peliculasapp.PeliculaListFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>
activity_pelicula_list.xml

En un xml separado definimos el layout del fragmento:

<?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:orientation="vertical" >

    <ListView
        android:id="@android:id/list"
        android:background="@android:color/holo_blue_dark"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >
    </ListView>

    <TextView
        android:id="@android:id/empty"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >
    </TextView>

</LinearLayout>
pelicula_list_fragment.xml

El ListView tiene la propiedad background en azul sólo por fines didácticos. 

Para poder utilizar esta vista en el fragment, tenemos que "inflar" (bindear) el layout en el código Java del Fragment:

   @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.pelicula_list_fragment, null, false);
    }
PeliculaListFragment.xml

Visualizamos el cambio:


Layout custom con dos filas

Vamos a modificar el layout default para mostrar 2 filas:
  • en la primera mostramos el título de la película con un tamaño grande
  • en la segunda se visualiza la lista de actores
Qué tenemos que hacer
  • definir un layout específico para cada fila que va a reemplazar el layout default: esto es un xml
  • generar un adapter, para "inflar" el layout custom de cada ítem: esto es código Java que recibe la lista de películas y transforma cada fila
  • y en el Contrroller (ListFragment) reemplazar el ArrayAdapter de películas default por el nuevo adapter

Layout específico

Definimos el layout con dos filas, cada una con un TextView que muestra todo el contenido del texto:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/lblPelicula"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@+id/label"
        android:textAppearance="@android:style/TextAppearance.DeviceDefault.Large.Inverse">
    </TextView>

    <TextView
        android:id="@+id/lblActores"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@+id/label"
        android:textSize="40px"
        android:textAppearance="@android:style/TextAppearance.DeviceDefault.Medium.Inverse">
    </TextView>

</LinearLayout>
pelicula_row.xml

Los textos se visualizan en inversa, porque vamos a cambiar el color de fondo del List View a negro:

    <ListView
        android:id="@android:id/list"
        android:background="@android:color/black"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >
    </ListView>
pelicula_list_fragment.xml

Adapter de películas

PeliculaAdapter hereda de ArrayAdapter, aunque también hay otras variantes: BaseAdapter o SimpleAdapter.

Veamos el constructor del adapter...

   public PeliculaAdapter(Context context, List<Pelicula> peliculas) {
        super(context, R.layout.pelicula_row, peliculas);
    }
PeliculaAdapter.java

Es importante respetar algunas cosas:
  • el context suele ser la actividad en la que está contenido el ListView
  • asociamos como formato la fila anteriormente definida: R.layout.pelicula_row
  • el tercer parámetro (la lista de películas) es muy importante pasarlo al constructor de la superclase. De lo contrario la list view quedará vacía, por más que almacenemos el argumento peliculas en una variable de instancia.
Ahora sí por cada uno de los elementos se invoca al método getView, donde se arma el binding entre el row y los valores de cada película:

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        LayoutInflater inflater = (LayoutInflater) getContext()
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View rowView = inflater.inflate(R.layout.pelicula_row, parent, false);
        final Pelicula pelicula = getItem(position);

        TextView tvPelicula = (TextView) rowView.findViewById(R.id.lblPelicula);
        tvPelicula.setText(pelicula.toString());

        TextView tvActores = (TextView) rowView.findViewById(R.id.lblActores);
        tvActores.setText(pelicula.getActores());
        return rowView;
    }
PeliculaAdapter.java

El modelo que propone Android y el lenguaje Java no nos ayudan para bajar la cantidad de líneas que necesitamos, pero creemos que se entiende el concepto.

Reemplazar el Adapter en el Fragment

Sólo nos falta reemplazar el ArrayAdapter por nuestro PeliculaAdapter, en el Fragment:

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setListAdapter(new PeliculaAdapter(
                getActivity(),
                RepoPeliculas.getInstance().getPeliculas(null, 10)));

    }
PeliculaListFragment.java

Y ahora sí, podemos ver el cambio al reiniciar la aplicación: