Patrón Master-Detail En Android

Si necesitas aprovechar el espacio sobrante que dejan tus listas en una tablet, este tutorial es para ti. Donde aprenderás a emplear el patrón master-detail en dispositivos con dimensiones amplias.

Para ello te explicaré los pasos necesarios para generar una aplicación Android de ejemplo llamada «Tips de salud», la cual contiene un fragmento con un RecyclerView, cuyo contenido son artículos con hábitos de vida sana.

Por cada elemento de la lista tendremos un detalle que será extendido en la misma actividad si se trata de tablets (dispositivos con más de 900dp) o se iniciará una nueva actividad de detalle con dicha información (teléfonos).

Descargar Proyecto Android Studio De «Tips De Salud»

A continuación te dejo el resultado final en un video del proyecto en Android Studio que terminarás creando:

Para desbloquear el link de descarga sigue estas instrucciones:

[sociallocker id=»7121″]Descargar Gratis[/sociallocker]

Patrón Master-Detail En Tablets

El mecanismo master-detail hace referencia a un escenario de interfaz de usuario donde se posee una lista de elementos homogéneos y se permite obtener una vista detallada del elemento en una porción más extensa de UI.

En teléfonos tendremos la capacidad de acomodar un solo panel para esta interacción. Donde al seleccionar un ítem de la lista la app te dirigirá a otra pantalla con el detalle:

Patrón Master-Detail con un solo panel

Por otro lado, las tablets debido a sus amplias dimensiones permiten usar ambos paneles en conjunto. Donde la selección de un ítem, actualiza el panel de detalle en tiempo real:

Patrón Master-Detail con multipanel

A esto se le llama una planeación para múltiples tamaños de pantallas al momento de abordar la estructura de una aplicación Android. Este enfoque nos evita caer en un pobre uso del espacio y la distribución excesiva de los elementos.

Malas prácticas en múltiples tipos de pantallas en Android

Un buen ejemplo para obtener ideas de inspiración es la aplicación móvil de Evernote, la cual muestra la lista de marcadores guardados y contiguamente se encuentra el flujo de su detalle.

Ejemplo master-detail en Android Aplicación Evernote

Ahora la gran pregunta es: ¿Cómo implementaré este patrón master-detail en mis apps?

Si tu intuición es aguda, ya sabrás que el uso de secciones de UI dentro de una actividad se maneja con fragmentos. Estas ‘subactividades’ te permiten proporcionar un ciclo de vida a una porción de interfaz de usuario con el fin de reducir complejidad y añadir personalización a tus apps.

La idea de este tutorial es mostrarte el camino para crear un layout con dos paneles en una tablet y condicionar a que solo sea una actividad de detalle cuando se trate de teléfonos. La siguiente ilustración muestra el esquema general:

Master-detail con fragmentos en Android

Para ello necesitamos:

  1. Crear actividad de lista
  2. Crear fragmento de lista
  3. Crear un POJO para los artículos
  4. Crear un adaptador para la lista
  5. Crear actividad del detalle
  6. Crear fragmento del detalle

Todos esos elementos los crearás basado en el siguiente modelo final de la aplicación «Tips de salud»:

Aplicación Android de salud para tablet con master-detail

Como ves, el panel izquierdo contiene una lista de artículos de salud y el panel derecho contiene el detalle de un elemento seleccionado. Veamos cómo abordar este diseño.

1. Configurar Un Nuevo Proyecto En Android Studio

Paso 1. Como siempre, abre Android Studio y crea un nuevo proyecto llamado «Tips De Salud». Cuando el asistente te permita elegir la actividad principal que usarás, selecciona el tipo «Blank Activity» y luego llámala ActividadListaArticulos.java. Además cambia su layout por actividad_lista_articulos.xml.

Nueva actividad en Android Studio Tips de salud

Para teléfonos modifica el layout original por el siguiente:

actividad_lista_articulos.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            app:popupTheme="@style/AppTheme.PopupOverlay">

        </android.support.v7.widget.Toolbar>

    </android.support.design.widget.AppBarLayout>

    <FrameLayout
        android:id="@+id/contenedor_lista"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />

</android.support.design.widget.CoordinatorLayout>

En cuanto a tablets es necesario crear la variación de este layout pero en la carpeta values-w900dp. En esencia es el mismo layout, solo que añadiremos el segundo panel del detalle, el cual debe superponerse a la app bar principal.

Además agregaremos un ImageView para simular el icono de búsqueda en la app bar principal.

actividad_lista_articulos.xml (w900dp)

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:gravity="center_vertical"
            app:popupTheme="@style/AppTheme.PopupOverlay">

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="320dp"
                android:layout_marginStart="320dp"
                android:src="@drawable/icono_busqueda" />
        </android.support.v7.widget.Toolbar>

    </android.support.design.widget.AppBarLayout>

    <FrameLayout
        android:id="@+id/contenedor_lista"
        android:layout_width="@dimen/ancho_panel_lista"
        android:layout_height="match_parent"
        android:layout_marginTop="?attr/actionBarSize"
        android:background="@android:color/white" />

    <FrameLayout
        android:id="@+id/contenedor_detalle_articulo"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginLeft="@dimen/ancho_panel_lista"
        android:layout_toRightOf="@+id/contenedor_lista"
        android:background="@android:color/white"
        android:elevation="8dp" />

</android.support.design.widget.CoordinatorLayout>

Paso 2. Ve a la aplicación Material Design Colors y selecciona un esquema principal con los colores Rosa 700, Rosa 400, Rosa 100. Para el acento usa el color Cyan 400.

Esquema Pink En Material Design Colors App

Ahora abre tu archivo colors.xml y establece los colores en generados en los ítems colorPrimary, colorPrimaryDark y colorAccent.

colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#EC407A</color>
    <color name="colorPrimaryDark">#C2185B</color>
    <color name="colorAccent">#26C6DA</color>
</resources>

Paso 3. Abre tú archivo build.gradle del módulo principal y agrega las siguientes librerías:

build.gradle

dependencies {
    ...
    compile 'com.android.support:recyclerview-v7:23.1.1'    
    compile 'com.github.bumptech.glide:glide:3.6.1'
}

La primera línea se trata de la librería para crear listas con el RecyclerView. La segunda es Glide para la carga de imágenes desde urls con caching integrado. Asegúrate de añadir un permiso de conexión a internet dentro del AndroidManifest.xml para acceder a las imágenes.

<uses-permission android:name="android.permission.INTERNET"/>

Paso 4. Para terminar la configuración general de los aspectos del proyecto, abre el archivo de dimensiones y agrega las siguientes medidas dentro de él.

dimens.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="margen_estandar">16dp</dimen>    
    <dimen name="altura_item">122dp</dimen>
    <dimen name="margen_izquierda_contenido">72dp</dimen>
    <dimen name="ancho_miniatura">120dp</dimen>
    <dimen name="ancho_panel_lista">512dp</dimen>
</resources>

Los significados vienen así:

  • La margen estándar hace referencia al espaciado que se usa con mayor frecuencia en Android.
  • La altura del ítem es el tamaño de cada ítem de la lista.
  • La margen izquierda del contenido se usa como alineación en el detalle.
  • El ancho de la miniatura hace referencia al tamaño de la imagen en los ítems de la lista.
  • El ancho del panel de lista hace referencia a su tamaño.

2. Crear Fragmento Con RecyclerView

Paso 1. El siguiente movimiento es crear el fragmento que contendrá la lista basada en un Recycler View. Esto solo requiere que vayas a File > New >Fragment > Fragment(Blank) con las siguientes características.

Android Studio: Nuevo Fragment(Blank)

Paso 2. Abre el layout del fragmento y añade un nodo <RecyclerView> para representar la lista. Puedes encerrar la lista dentro de un contenedor por si deseas personalizar mucho más el fragmento.

fragmento_lista_articulos.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.herprogramacion.tipsdesalud.FragmentoListaArticulos">

    <android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/reciclador"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginLeft="16dp"
        android:layout_marginRight="16dp"
        app:layoutManager="LinearLayoutManager"
        tools:listitem="@layout/item_lista_articulos" />

</FrameLayout>

Si eres buen observador verás que he incrustado el layout manager a través del atributo app:layoutManager para generar una lista. Incluso también podemos visualizar el diseño de los ítems en la vista de previsualización con el atributo auxiliar tools:listitem.

Paso 3. En el paso anterior viste la referencia al layout de los ítems llamado item_lista_articulos.xml, pero como aún no lo tienes creado, entonces añade un nuevo layout e intenta replicar el siguiente diseño.

Item de lista en RecyclerView Android

Es muy sencillo. Me pareció más cómodo usar un Relative Layout para ubicar los elementos con referencia al padre. Sin embargo un Grid Layout también vendría bien.

Observa:

item_lista_articulos.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="@dimen/altura_item"
    android:orientation="horizontal"
    android:layout_marginTop="@dimen/margen_estandar"
    android:layout_marginBottom="@dimen/margen_estandar">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Título"
        android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
        android:id="@+id/titulo"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_toLeftOf="@+id/miniatura" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Resumen"
        android:id="@+id/resumen"
        android:layout_below="@+id/titulo"
        android:layout_alignLeft="@+id/titulo"
        android:layout_alignRight="@+id/titulo"
        android:layout_above="@+id/fecha" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Fecha"
        android:id="@+id/fecha"
        android:layout_alignLeft="@+id/resumen"
        android:layout_alignRight="@+id/titulo"
        android:textAppearance="@style/TextAppearance.AppCompat.Caption"
        android:layout_alignParentBottom="true" />

    <ImageView
        android:layout_width="@dimen/ancho_miniatura"
        android:layout_height="match_parent"
        android:id="@+id/miniatura"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:layout_marginLeft="@dimen/margen_estandar" />
</RelativeLayout>

3. Crear POJO Para Los Artículos

Ya sabes que la lista requiere una fuente de datos para alimentar a sus views. Por motivos de agilidad e ilustración crearé un modelo de datos que refleje someramente el contenido de un artículo.

Sin embargo, si es necesario, detente en este lugar para pensar la creación de tu propia base de datos SQLite; el parsing de elementos JSON o el parsing XML.

Paso 1. La vista de la sección anterior te muestra una gran cantidad de atributos que requiere el modelo de datos:

  • título
  • resumen|contenido
  • fecha
  • miniatura|imagen principal

Esto haría ver la clase POJO de la siguiente forma:

/**
 * Un 'articulo' representa la estructura general de cada tip de salud
 */
public static class Articulo {

    public final String id;

    public final String titulo;

    public final String descripcion;

    public final String fecha;

    public final String urlMiniatura;

    public Articulo(String id, String titulo, String descripcion,
                    String fecha, String urlMiniatura) {
        this.id = id;
        this.titulo = titulo;
        this.descripcion = descripcion;
        this.fecha = fecha;
        this.urlMiniatura = urlMiniatura;
    }
}

Paso 2. Ahora crea un nuevo paquete Java y agrega una clase llamada ModeloArticulo.java. Luego añade como miembro a la clase Articulo. El objetivo de esta envoltura es representar una fuente de datos que entrega grandes bloques de datos y además de ello permite realizar una búsqueda entre todos ellos.

ModeloArticulo.java

import android.support.annotation.NonNull;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * Modelo de datos para los artículos que se inflarán en la lista
 */
public class ModeloArticulos {

    /**
     * Arreglo de objetos {@link Articulo} que simula una fuente de datos
     */
    public static final List<Articulo> ITEMS = new ArrayList<Articulo>();

    /**
     * Mapa simulador de búsquedas de articulos por id
     */
    public static final Map<String, Articulo> MAPA_ITEMS = new HashMap<String, Articulo>();

    static {
        // Añadir elementos de ejemplo
        agregarItem(new Articulo(generarId(),
                "10 Plantillas Para Determinar Tus Necesidades Calóricas",
                "Descarga nuestras plantillas para Microsoft Excel que te permitirán, calcular exactamente...",
                "10 de Enero", "https://www.develou.com/wp-content/uploads/2016/01/articulo1.jpg"));
        agregarItem(new Articulo(generarId(),
                "¿Qué Tan Malo Es Consumir Chocolate?",
                "Si aún no has podido resolver tus dudas sobre comer chocolate, entonces este artículo es para ti...",
                "11 de Enero", "https://www.develou.com/wp-content/uploads/2016/01/articulo2.jpg"));
        agregarItem(new Articulo(generarId(),
                "Guía Para Identificar Alimentos Con Trigo Perjudiciales",
                "Aprende a distinguir aquellos alimentos que están hechos de trigo mal procesado...",
                "12 de Enero", "https://www.develou.com/wp-content/uploads/2016/01/articulo3.jpg"));
        agregarItem(new Articulo(generarId(),
                "Descubre Qué Harían 10 Minutos De Ejercicio Diario En Tu Cuerpo",
                "Entra y descubre los beneficios de realizar una rutina de 10 minutos los 7 días...",
                "13 de Enero", "https://www.develou.com/wp-content/uploads/2016/01/articulo4.jpg"));
        agregarItem(new Articulo(generarId(),
                "Aumentando Las Defensas Del Cuerpo Con Frutos Rojos",
                "Los frutos rojos frecuentemente son ignorados por la mayoría de las personas, pero hoy te mostraremos...",
                "14 de Enero", "https://www.develou.com/wp-content/uploads/2016/01/articulo5.jpg"));
        agregarItem(new Articulo(generarId(),
                "5 Recetas Bajas En Grasa Para El Almuerzo",
                "Variar las recetas a la hora de alimentarse permite obtener distintos beneficios dependiendo del alimento, ...",
                "15 de Enero", "https://www.develou.com/wp-content/uploads/2016/01/articulo6.jpg"));
        agregarItem(new Articulo(generarId(),
                "Combina Cardio + Fuerza Para Obtener Verdaderos Resultados",
                "Muchos creen que solo realizar ejercicios de cardio los hará perder de peso y...",
                "16 de Enero", "https://www.develou.com/wp-content/uploads/2016/01/articulo7.jpg"));
        agregarItem(new Articulo(generarId(),
                "Planificador Semanal Para Optimizar Tu Dieta",
                "Descarga nuestro nueva plantilla para prácticar buenos hábitos alimenticios. Se trata de...",
                "17 de Enero", "https://www.develou.com/wp-content/uploads/2016/01/articulo8.jpg"));
        agregarItem(new Articulo(generarId(),
                "Las 30 Razones De Por Qué Superman Come Cacahuates",
                "Los frutos secos han sido desde la antiguedad la principal fuente de proteínas para muchas civilizaciones...",
                "18 de Enero", "https://www.develou.com/wp-content/uploads/2016/01/articulo9.jpg"));

    }

    @NonNull
    private static String generarId() {
        return UUID.randomUUID().toString();
    }


    private static void agregarItem(Articulo item) {
        ITEMS.add(item);
        MAPA_ITEMS.put(item.id, item);
    }


    /**
     * Un 'articulo' representa la estructura general de cada tip de salud
     */
    public static class Articulo {

        public final String id;

        public final String titulo;

        public final String descripcion;

        public final String fecha;

        public final String urlMiniatura;

        public Articulo(String id, String titulo, String descripcion,
                        String fecha, String urlMiniatura) {
            this.id = id;
            this.titulo = titulo;
            this.descripcion = descripcion;
            this.fecha = fecha;
            this.urlMiniatura = urlMiniatura;
        }
    }
}

La lista de artículos estática ITEMS actuará como origen de datos para el adaptador de la lista que se creará. El mapa MAPA_ITEMS será el índice de búsqueda del cuál podrás obtener datos a través de un identificador.

4. Crear Adaptador Para La Lista

Una vez que tengas listo el modelo, ya es posible generar el adaptador. Para ello crea una nueva clase y nómbrala AdaptadorArticulos.java. Haz que extienda de RecyclerView.Adapter y sobrescribe los controladores onCreateViewHolder(), onBindViewHolder() y getCount() cómo ya has hecho infinidad de veces.

AdaptadorArticulos.java

import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import com.bumptech.glide.Glide;
import com.herprogramacion.tipsdesalud.modelo.ModeloArticulos;

import java.util.List;

/**
 * {@link RecyclerView.Adapter} que alimenta la lista con
 * instancias {@link ModeloArticulos.Articulo}
 */
public class AdaptadorArticulos extends RecyclerView.Adapter<AdaptadorArticulos.ViewHolder> {

    private final List<ModeloArticulos.Articulo> valores;

    public AdaptadorArticulos(List<ModeloArticulos.Articulo> items,
                              OnItemClickListener escuchaClicksExterna) {
        valores = items;
        this.escuchaClicksExterna = escuchaClicksExterna;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_lista_articulos, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(final ViewHolder holder, int position) {
        holder.item = valores.get(position);
        holder.viewTitulo.setText(valores.get(position).titulo);
        holder.viewResumen.setText(valores.get(position).descripcion);
        holder.viewFecha.setText(valores.get(position).fecha);
        Glide.with(holder.itemView.getContext())
                .load(holder.item.urlMiniatura)
                .thumbnail(0.1f)
                .centerCrop()
                .into(holder.viewMiniatura);

    }

    @Override
    public int getItemCount() {
        if (valores != null) {
            return valores.size() > 0 ? valores.size() : 0;
        } else {
            return 0;
        }
    }


    private String obtenerIdArticulo(int posicion) {
        if (posicion != RecyclerView.NO_POSITION) {
            return valores.get(posicion).id;
        } else {
            return null;
        }
    }

    public class ViewHolder extends RecyclerView.ViewHolder
            implements View.OnClickListener {
        public final TextView viewTitulo;
        public final TextView viewResumen;
        public final TextView viewFecha;
        public final ImageView viewMiniatura;

        public ModeloArticulos.Articulo item;

        public ViewHolder(View view) {
            super(view);
            view.setClickable(true);
            viewTitulo = (TextView) view.findViewById(R.id.titulo);
            viewResumen = (TextView) view.findViewById(R.id.resumen);
            viewFecha = (TextView) view.findViewById(R.id.fecha);
            viewMiniatura = (ImageView) view.findViewById(R.id.miniatura);

            view.setOnClickListener(this);
        }

        @Override
        public void onClick(View v) {
            escuchaClicksExterna.onClick(this, obtenerIdArticulo(getAdapterPosition()));
        }
    }


    public interface OnItemClickListener {
        public void onClick(ViewHolder viewHolder, String idArticulo);
    }

    private OnItemClickListener escuchaClicksExterna;
}

Recuerda que la interfaz que añades llamada OnItemClickListener (obviamente puedes usar otro nombre) es una interfaz que transmite los eventos de click al fragmento o actividad contenedora del recycler view.

El método obtenerIdArticulo() retorna en el atributo idArticulo de cada objeto Articulo asociado a los ítems de la lista. Este permite enviar por el controlador onClick() el identificador para determinar el detalle de cada artículo.

Punto de ejecución — Abre el fragmento FragmentoListaArticulos y prepara el RecyclerView de la siguiente forma:

import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.herprogramacion.tipsdesalud.modelo.ModeloArticulos;


/**
 * Fragmento especializado para la lista de artículos
 */

public class FragmentoListaArticulos extends Fragment
        implements AdaptadorArticulos.OnItemClickListener {

    private EscuchaFragmento escucha;

    public FragmentoListaArticulos() {

    }

    public static FragmentoListaArticulos crear() {
        return new FragmentoListaArticulos();
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            // Manejo de argumentos
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.fragmento_lista_articulos, container, false);

        View recyclerView = v.findViewById(R.id.reciclador);

        assert recyclerView != null;
        prepararLista((RecyclerView) recyclerView);

        return v;
    }


    private void prepararLista(@NonNull RecyclerView recyclerView) {
        recyclerView.setAdapter(new AdaptadorArticulos(ModeloArticulos.ITEMS, this));
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (context instanceof EscuchaFragmento) {
            escucha = (EscuchaFragmento) context;
        } else {
            throw new RuntimeException(context.toString()
                    + " debes implementar EscuchaFragmento");
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        escucha = null;
    }

    @Override
    public void onClick(AdaptadorArticulos.ViewHolder viewHolder, String idArticulo) {

    }

    public interface EscuchaFragmento {
        void alSeleccionarItem(String idArticulo);
    }
}

Obtén el recycler view en onCreateView() y luego asignar su adaptador con prepararLista().

prepararLista() recibe la lista estática ModeloArticulo.ITEMS y al propio fragmento como escucha(recuerda implementar la interfaz del adaptador e implementar el controlador onClick().

Ahora ve a tu actividad de lista de artículos y agrega el fragmento en el método onCreate():

ActividadListaArticulos.java

import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;

import com.herprogramacion.tipsdesalud.modelo.ModeloArticulos;

/**
 * Actividad con la lista de artículos. Si el ancho del dispositivo es mayor o igual a 900dp, entonces
 * se incrusta el fragmento de detalle {@link FragmentoDetalleArticulo} para generar el patrón
 * Master-detail
 */
public class ActividadListaArticulos extends AppCompatActivity
        implements FragmentoListaArticulos.EscuchaFragmento {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.actividad_lista_articulos);

        ((Toolbar) findViewById(R.id.toolbar)).setTitle(getTitle());

        // Agregar fragmento de lista
        getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.contenedor_lista, FragmentoListaArticulos.crear())
                .commit();

    }

    @Override
    public void alSeleccionarItem(String idArticulo) {
        
    }
}

Es necesario implementar la interfaz del fragmento para que este sea inflado. Con esto parcialmente construido ya puedes ejecutar la app y obtener el siguiente resultado.

RecyclerView En Aplicación Android Para Tablet

Hasta el momento ya tienes el primer panel de la aplicación. Ahora solo falta el detalle.

5. Crear Actividad De Detalle

Lo siguiente es crear la actividad para el detalle de los artículos que se mostrará en los teléfonos. Tan solo ve a File > New > Activity > Blank Activity. Usa el nombre de ActividadDetalleArticulo y denomina al layout cómo actividad_detalle_articulo.xml.

Por el momento solo actualizaremos el layout para que mantenga una toolbar simple junto a un FrameLayout donde se incrustará el fragmento del detalle.

actividad_detalle_articulo.xml

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context=".ActividadDetalleArticulo"
    tools:ignore="MergeRootFrame">

    <FrameLayout
        android:id="@+id/contenedor_detalle_articulo"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar_detalle"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@android:color/transparent"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

</android.support.design.widget.CoordinatorLayout>

6. Crear Fragmento Para Detalle

El fragmento del detalle tendrá un diseño donde se encuentra una imagen de cabecera en la parte superior, la cual se contrasta con una Toolbar transparente. La siguiente sección es un view insignia donde va el título del artículo y la fecha de publicación. Finalmente en la tercera sección tendremos el contenido total del texto del artículo.

Aplicación android con fragmento de detalle

Paso 1. Para diseñar esta interfaz en teléfonos, primero crea un fragmento sin interfaz de comunicación llamado FragmentoDetalleArticulo.java junto a un layout llamado fragmento_detalle_articulo.xml.

Para crear el diseño puedes usar un LinearLayout vertical, donde distribuyas las tres secciones normales. Puedes usar una proporción 50% y 50% entre la sección de la imagen  para resaltar la cabecera.

fragmento_detalle_articulo.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/articulo_detail"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".FragmentoDetalleArticulo">

    <ImageView
        android:id="@+id/imagen"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="50"
        android:scaleType="centerCrop" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?colorPrimary"
        android:elevation="2dp"
        android:orientation="vertical"
        android:paddingBottom="@dimen/margen_estandar"
        android:paddingEnd="@dimen/margen_estandar"
        android:paddingRight="@dimen/margen_estandar"
        android:paddingStart="@dimen/margen_izquierda_contenido"
        android:paddingLeft="@dimen/margen_izquierda_contenido"
        android:paddingTop="@dimen/margen_estandar">

        <TextView
            android:id="@+id/titulo"
            style="@style/TextAppearance.AppCompat.Title.Inverse"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:ellipsize="end"
            android:maxLines="2"
            android:textIsSelectable="true" />

        <TextView
            android:id="@+id/fecha"
            style="@style/TextAppearance.AppCompat.Subhead.Inverse"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </LinearLayout>

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="50"
        android:clipToPadding="false"
        android:paddingBottom="@dimen/margen_estandar"
        android:paddingTop="@dimen/margen_estandar"
        android:scrollbarStyle="outsideOverlay">

        <TextView
            android:id="@+id/contenido"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingLeft="@dimen/margen_izquierda_contenido"
            android:paddingStart="@dimen/margen_izquierda_contenido"
            android:paddingEnd="@dimen/margen_estandar"
            android:paddingRight="@dimen/margen_estandar"
            android:text="Contenido"
            android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
    </ScrollView>

</LinearLayout>

Paso 2. El layout del fragmento para tablets es exactamente lo mismo, solo que se requiere una Toolbar con respecto a la imagen de cabecera.

Fragmento de detalle en tablet

Una de las formas más sencillas de conseguir este efecto es envolver la imagen y la Toolbar dentro de un FrameLayout para superponer los elementos. Luego usar el valor @android:color/transparent en el atributo android:background de la Toolbar.

Dirígete a la pestaña Design del layout fragmento_detalle_articulo.xml y luego selecciona el primer icono de la barra de herramientas del diseñador. Elige la opción Create Other…

Android Studio: Create Other Layout Variation

Esto iniciará una ventana llamada Select Resource Directory, al cual te permite añadir variaciones de tus recursos dependiendo de los calificadores. En este caso usarás el calificador Screen Width con un valor de 900dp para indicar que el nuevo layout es solo para dispositivos que tengan como mínimo ese ancho.

Android Studio: Crear Layout Con Screen Width 900dp

Así se duplicará el código de fragmento_detalle_articulo.xml dentro de la carpeta res/layout-w900dp. En dicho archivo, debes añadir la nueva definición con Toolbar:

fragmento_detalle_articulo.xml (w900dp)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/articulo_detail"
    style="?android:attr/textAppearanceLarge"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".FragmentoDetalleArticulo">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="50"
        android:background="@color/backgroundActividad">


        <ImageView
            android:id="@+id/imagen"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="centerCrop" />

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar_detalle"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="@android:color/transparent"
            android:gravity="center_vertical"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </FrameLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?colorPrimary"
        android:elevation="2dp"
        android:orientation="vertical"
        android:paddingBottom="@dimen/margen_estandar"
        android:paddingEnd="@dimen/margen_estandar"
        android:paddingLeft="@dimen/margen_izquierda_contenido"
        android:paddingRight="@dimen/margen_estandar"
        android:paddingStart="@dimen/margen_izquierda_contenido"
        android:paddingTop="@dimen/margen_estandar">

        <TextView
            android:id="@+id/titulo"
            style="@style/TextAppearance.AppCompat.Title.Inverse"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:ellipsize="end"
            android:maxLines="2"
            android:textIsSelectable="true" />

        <TextView
            android:id="@+id/fecha"
            style="@style/TextAppearance.AppCompat.Subhead.Inverse"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </LinearLayout>

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="50"
        android:clipToPadding="false"
        android:paddingBottom="@dimen/margen_estandar"
        android:paddingTop="@dimen/margen_estandar"
        android:scrollbarStyle="outsideOverlay">

        <TextView
            android:id="@+id/contenido"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingEnd="@dimen/margen_estandar"
            android:paddingLeft="@dimen/margen_izquierda_contenido"
            android:paddingRight="@dimen/margen_estandar"
            android:paddingStart="@dimen/margen_izquierda_contenido"
            android:text="Contenido"
            android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
    </ScrollView>
</LinearLayout>

El código Java del fragmento debe permitir crear una instancia con un parámetro que sea el identificador del artículo. Recuerda que los argumentos de los fragmentos puedes obtenerlos con el método getArguments().

Con el id puedes cargar los datos del artículo en onCreateView() y setearlos en cada view como se muestra a continuación:

FragmentoDetalleArticulo.java

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v7.widget.Toolbar;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import com.bumptech.glide.Glide;
import com.herprogramacion.tipsdesalud.modelo.ModeloArticulos;

/**
 * Fragmento que representa el panel del detalle de un artículo.
 */
public class FragmentoDetalleArticulo extends Fragment {

    // EXTRA
    public static final String ID_ARTICULO = "extra.idArticulo";

    // Artículo al cual está ligado la UI
    private ModeloArticulos.Articulo itemDetallado;

    public FragmentoDetalleArticulo() {
    }

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

        if (getArguments().containsKey(ID_ARTICULO)) {
            // Cargar modelo según el identificador
            itemDetallado = ModeloArticulos.MAPA_ITEMS.get(getArguments().getString(ID_ARTICULO));

        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.fragmento_detalle_articulo, container, false);

        if (itemDetallado != null) {
            // Toolbar en master-detail
            Toolbar toolbar = (Toolbar) v.findViewById(R.id.toolbar_detalle);
            if (toolbar != null)
                toolbar.inflateMenu(R.menu.menu_detalle_articulo);

            ((TextView) v.findViewById(R.id.titulo)).setText(itemDetallado.titulo);
            ((TextView) v.findViewById(R.id.fecha)).setText(itemDetallado.fecha);
            ((TextView) v.findViewById(R.id.contenido)).setText(getText(R.string.lorem));
            Glide.with(this)
                    .load(itemDetallado.urlMiniatura)
                    .into((ImageView) v.findViewById(R.id.imagen));
        }

        return v;
    }
}

Paso 3. Infla el fragmento en el contenedor de la actividad de detalle a través de una transacción. Recuerda habilitar el up button si se trata de teléfonos.

ActividadDetalleArticulo.java

import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.NavUtils;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;

/**
 * Actividad que muestra el detalle del artículo seleccionado en {@link ActividadListaArticulos}
 */
public class ActividadDetalleArticulo extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.actividad_detalle_articulo);

        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar_detalle);
        setSupportActionBar(toolbar);

        // Verificación: ¿La app se está ejecutando en un teléfono?
        if (!getResources().getBoolean(R.bool.esTablet)) {
            // Mostrar Up Button
            ActionBar actionBar = getSupportActionBar();
            if (actionBar != null) {
                actionBar.setDisplayHomeAsUpEnabled(true);
            }
        }

        if (savedInstanceState == null) {
            // Añadir fragmento de detalle
            Bundle arguments = new Bundle();
            arguments.putString(FragmentoDetalleArticulo.ID_ARTICULO,
                    getIntent().getStringExtra(FragmentoDetalleArticulo.ID_ARTICULO));
            FragmentoDetalleArticulo fragment = new FragmentoDetalleArticulo();
            fragment.setArguments(arguments);
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.contenedor_detalle_articulo, fragment)
                    .commit();
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_detalle_articulo, menu);
        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == android.R.id.home) {
            NavUtils.navigateUpTo(this, new Intent(this, ActividadListaArticulos.class));
            return true;
        }
        return super.onOptionsItemSelected(item);
    }
}

Para determinar si estás en una teléfono o una tablet, puedes usar un recurso bool que sea false en la carpeta values general y que sea true en values-w900dp.

<bool name="esTablet">false</bool>

Luego puedes obtener la referencia a través de getResources().getBoolean() y condicionar un comportamiento (en este caso, el mostrar el up button si es teléfono).

7. Manejar Eventos De Master-Detail

Paso 1. Para que generes la interacción donde se selecciona un ítem de la lista e inmediatamente se carga su detalle en el panel (o se inicia la actividad detalle si es un teléfono) es necesario añadir las acciones en el controlador de la interfaz del fragmento de la lista.

public void cargarDetalle(String idArticulo) {
    if (escucha != null) {
        escucha.alSeleccionarItem(idArticulo);
    }
}

@Override
public void onClick(AdaptadorArticulos.ViewHolder viewHolder, String idArticulo) {
    cargarDetalle(idArticulo);
}

Solo es necesario que se ejecute el controlador EscuchaFragmento.alSeleccionarItem() dentro del controlador OnItemClickListener.onClick(). Esto propaga el click en cada ítem de la lista hacia la actividad.

Paso 2. Dentro de onCreate() en ActividadListaArticulos determina si la app corre en teléfono o tablet. Guarda dicho resultado en una variable global y luego úsala a tu favor para condicionar el flujo de ejecución.

ActividadListaArticulos.java

import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;

import com.herprogramacion.tipsdesalud.modelo.ModeloArticulos;

/**
 * Actividad con la lista de artículos. Si el ancho del dispositivo es mayor o igual a 900dp, entonces
 * se incrusta el fragmento de detalle {@link FragmentoDetalleArticulo} para generar el patrón
 * Master-detail
 */
public class ActividadListaArticulos extends AppCompatActivity
        implements FragmentoListaArticulos.EscuchaFragmento {

    // ¿Hay dos paneles?
    private boolean dosPaneles;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.actividad_lista_articulos);

        ((Toolbar) findViewById(R.id.toolbar)).setTitle(getTitle());

        // Verificación: ¿Existe el detalle en el layout?
        if (findViewById(R.id.contenedor_detalle_articulo) != null) {
            // Si es asi, entonces confirmar modo Master-Detail
            dosPaneles = true;

            cargarFragmentoDetalle(ModeloArticulos.ITEMS.get(0).id);
        }

        // Agregar fragmento de lista
        getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.contenedor_lista, FragmentoListaArticulos.crear())
                .commit();

    }

    private void cargarFragmentoDetalle(String id) {
        Bundle arguments = new Bundle();
        arguments.putString(FragmentoDetalleArticulo.ID_ARTICULO, id);
        FragmentoDetalleArticulo fragment = new FragmentoDetalleArticulo();
        fragment.setArguments(arguments);
        getSupportFragmentManager().beginTransaction()
                .replace(R.id.contenedor_detalle_articulo, fragment)
                .commit();
    }


    @Override
    public void alSeleccionarItem(String idArticulo) {
        if (dosPaneles) {
            cargarFragmentoDetalle(idArticulo);
        } else {
            Intent intent = new Intent(this, ActividadDetalleArticulo.class);
            intent.putExtra(FragmentoDetalleArticulo.ID_ARTICULO, idArticulo);

            startActivity(intent);
        }
    }
}

La variable dosPaneles tiene el valor de true si contenedor_detalle_articulo existe, de lo contrario es false. Aunque también puedes usar el recurso bool esTablet.

Si hay dos paneles, entonces se inserta un nuevo fragmento de detalle con el identificador que envía el adaptador. De lo contrario se inicia ActividadDetalleArticulo.

El primer ítem es seleccionado cuando se determina que es una tableta a través del método cargarFragmentoDetalle(), donde el parámetro el id del elemento que se encuentra en la posición 0 dentro de ModeloArticulos.ITEMS.

Punto de ejecución — Al relacionar los ítems de la lista por su identificador con el detalle del fragmento ya te es posible ejecutar la app en un emulador de tableta (yo usé el Nexus 10) y contemplar el siguiente resultado:

Aplicación Android Sobre Tips De Salud En Tablet

8. Android Studio: Template Para Master-Detail

Android Studio también posee un template que te permite generar el master-detail con tan solo un click.

Una de las formas de hacerlo es al momento de crear nuevo proyecto, donde puedes elegir como actividad principal un patrón master-detail.

Android Studio: Actividad Master Detail

Dentro de este te permitirán elegir el nombre de los elementos que listarás y la forma de referirte a ellos en plural.

Android Studio: Personalizar actividad master detail

Otra forma es ir a New > Activity > Master/Detail Flow. Esta vez puedes configurar un padre de la jerarquía para la actividad.

New > Activity > Master/Detail Flow

Saber que existe esta plantilla te ahorrará gran cantidad de tiempo la próxima vez que inicies un proyecto que tenga un patrón master-detail. Aunque el contenido del template es básico, es una buena base para realizar modificaciones.

Conclusión

Este artículo te mostró algunas ideas para aprovechar el espacio en blanco en tablets para crear un patrón master-detail.

Claramente se ve la diferencia entre dejar solo los ítems en una extensa pantalla y combinar dos fragmentos para construir una nueva interfaz.

Hay innumerables formas de crear el master-detail dependiendo de cada app en específico. Algunos no usarán una toolbar por cada fragmento como hicimos y algotros incluso tendrán más elementos de interacción. Todo depende de tus necesidades.

Recuerda que Android Studio tiene varios templates para optimizar tus desarrollos, así que haz uso de estos elementos para generar elementos básicos que puedan ser extendidos. Esto te ahorrará gran cantidad de tiempo.

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!