Android Tabs con Fragment y RecyclerView

By | 02/14/2017

Vamos a crear un ejemplo para este tutorial en el cual nuestro activity tendrá tabs haciendo uso de fragment y este fragment utilizará un RecyclerView.

Vayamos paso por paso:

Crearemos el Activity que definirá los tab pasandole la lista de nuestro modelo a mostrar en cada tab. Después el Fragment contendrá el RecyclerView; entre ambos trabajrán para mostrar cada ítem adecuadamente y devolverán al Activity la acción sobre un item, enviando el modelo sobre el que se hizo click.

Las dependencias que necesitas en tu archivo build

Estas son las dependencias que incluyes

dependencies {
   compile 'com.android.support:appcompat-v7:25.1.1'
   compile 'com.android.support:design:25.1.1'
   compile 'com.android.support:support-v4:25.1.1'
   compile 'com.android.support:recyclerview-v7:25.1.1'
}

El modelo POJO

Este el un modelo bastante tonto pero que nos servirá bien para que crees tu app de ejemplo con tabs usando fragment. Extiende de Parcelable a fin de enviar este modelo al fragment como parámetro.

public class DummyModel implements Parcelable {
    private final String id;
    private final String title;
    private final String detail;
....
}

El activity principal con el TabLayout

Tu Activity principal con dos elementos, el widget TabLayout que provee un layout horizontal para mostrar tabs y el ViewPager que te permitirá desplazarte entre los tabs deslizando con un gesto horizontalmente.

Tu xml debe lucir de este modo para definir el TabLayout y el ViewPager a fin de mostrar tabs.


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:id="@+id/activity_main"
   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"
   tools:context="com.gustavopeiretti.tabexample.TabExampleMainActivity">

   <android.support.design.widget.TabLayout
       android:id="@+id/tab_layout"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:background="?attr/colorPrimary"
       android:elevation="6dp"
       android:minHeight="?attr/actionBarSize"
       android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"/>


   <android.support.v4.view.ViewPager
       android:id="@+id/pager"
       android:layout_width="match_parent"
       android:layout_height="fill_parent"
       android:layout_below="@id/tab_layout"/>


</RelativeLayout>

Para el TabLayout agregarás en este ejemplo tres tabs con sus respectivos títulos.

TabLayout tabLayout = (TabLayout) findViewById(R.id.tab_layout);
tabLayout.addTab(tabLayout.newTab().setText(R.string.title_tab0));
//... otros tabs

Luego para el ViewPager debes agregarle un adapter que se encargará entre otras cosas de devolver el Fragment que corresponda a la posición del tab elegido.

final ViewPager viewPager = (ViewPager) findViewById(R.id.pager);
final PagerAdapter adapter = new PagerAdapter(...)
viewPager.setAdapter(adapter);

El Activity también implementará un Listener ItemFragment.OnListFragmentInteractionListener. Verás mas adelante que recibirá la info desde el RecyclerView cuando se haga click sobre un elemento y enviará el Modelo que se ha clickeado al Activity TabExampleMainActivity.

    public void onListFragmentInteraction(DummyModel model) {
        Toast.makeText(TabExampleMainActivity.this, DummyModel.class.getSimpleName() + ":" + model.getId() + " - "  +model.getTitle(), Toast.LENGTH_LONG).show();
    }

Puedes definir otro listener para cuando se pasa de un tab a otro

   private TabLayout.OnTabSelectedListener getOnTabSelectedListener(final ViewPager viewPager) {
        return new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                viewPager.setCurrentItem(tab.getPosition());
                Toast.makeText(TabExampleMainActivity.this, "Tab selected " +  tab.getPosition(), Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onTabUnselected(TabLayout.Tab tab) {
                // nothing now
            }

            @Override
            public void onTabReselected(TabLayout.Tab tab) {
                // nothing now
            }
        };
    }

El código completo para mostar tab en un Activity te queda así.


public class TabExampleMainActivity extends AppCompatActivity implements ItemFragment.OnListFragmentInteractionListener {

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

        // define TabLayout
        TabLayout tabLayout = (TabLayout) findViewById(R.id.tab_layout);
        tabLayout.addTab(tabLayout.newTab().setText(R.string.title_tab0));
        tabLayout.addTab(tabLayout.newTab().setText(R.string.title_tab1));
        tabLayout.addTab(tabLayout.newTab().setText(R.string.title_tab2));
        tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);

        // define ViewPager
        final ViewPager viewPager = (ViewPager) findViewById(R.id.pager);

        // fake list model... for test
        DummyModel[] listModel0 = createDummyListModel("tab 0");
        DummyModel[] listModel1 = createDummyListModel("tab 1");
        DummyModel[] listModel2 = createDummyListModel("tab 2");


        //  ViewPager need a PagerAdapter
        final PagerAdapter adapter = new PagerAdapter
                (getSupportFragmentManager(), tabLayout.getTabCount(), listModel0, listModel1, listModel2 );

        viewPager.setAdapter(adapter);

        // Listeners
        viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout));
        tabLayout.addOnTabSelectedListener(getOnTabSelectedListener(viewPager));

    }

    /**
     * Listener for tab selected
     * @param viewPager
     * @return
     */
    @NonNull
    private TabLayout.OnTabSelectedListener getOnTabSelectedListener(final ViewPager viewPager) {
        return new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                viewPager.setCurrentItem(tab.getPosition());
                Toast.makeText(TabExampleMainActivity.this, "Tab selected " +  tab.getPosition(), Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onTabUnselected(TabLayout.Tab tab) {
                // nothing now
            }

            @Override
            public void onTabReselected(TabLayout.Tab tab) {
                // nothing now
            }
        };
    }


    /**
     * Listener that comunicate fragment / recycler with this activity
     * @param model
     */
    @Override
    public void onListFragmentInteraction(DummyModel model) {
        // the user clicked on this item over the list
        Toast.makeText(TabExampleMainActivity.this, DummyModel.class.getSimpleName() + ":" + model.getId() + " - "  +model.getTitle(), Toast.LENGTH_LONG).show();
    }


    // model for test purpose
    private DummyModel[] createDummyListModel(String msj) {
        List<DummyModel> l = new ArrayList<>();
        for(int i = 0; i < 20; i++  ) {
            l.add(new DummyModel(String.valueOf(i), "Title " + i , "Descrip. " + i + " -" + msj));
        }
        return l.toArray(new DummyModel[l.size()]);
    }

}

Creas el PagerAdapter

El PagerAdapter debe extender de FragmentStatePagerAdapter. Observa como recibe la info de nuestro modelo para cada tab y como devuelve un fragment para cada posición del tab según su selección.

public class PagerAdapter extends FragmentStatePagerAdapter {

   private int numTabs;
   private DummyModel[] dummyModels0;
   private DummyModel[] dummyModels1;
   private DummyModel[] dummyModels2;

   public PagerAdapter(FragmentManager fm, int numTabs, DummyModel[] dummyModels0,
                       DummyModel[] dummyModels1, DummyModel[] dummyModels2) {
       super(fm);
       this.numTabs = numTabs;
       this.dummyModels0 = dummyModels0;
       this.dummyModels1 = dummyModels1;
       this.dummyModels2 = dummyModels2;
   }

   @Override
   public Fragment getItem(int position) {
       switch (position) {
           case 0:
               ItemFragment tab1 = ItemFragment.newInstance(dummyModels0);
               return tab1;
           case 1:
               ItemFragment tab2 = ItemFragment.newInstance(dummyModels1);
               return tab2;
           case 2:
               ItemFragment tab3 = ItemFragment.newInstance(dummyModels2);
               return tab3;
           default:
               throw new RuntimeException("Tab position invalid " + position);
       }
   }

   @Override
   public int getCount() {
       return numTabs;
   }

}

El fragment que mostrará la información de nuestro modelo

El fragment lo creas con los datos del model que deseas mostrar y harás uso de un RecyclerView a fin de displayarlos luego.


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:orientation="horizontal">

   <TextView
       android:id="@+id/id"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_margin="@dimen/text_margin"
       android:textAppearance="?attr/textAppearanceListItem" />

   <TextView
       android:id="@+id/title"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_margin="@dimen/text_margin"
       android:textAppearance="?attr/textAppearanceListItem" />

   <TextView
       android:id="@+id/detail"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_margin="@dimen/text_margin"
       android:textAppearance="?attr/textAppearanceListItem" />


</LinearLayout>


Como crear RecyclerView

El fragment utilizará un RecyclerView


<android.support.v7.widget.RecyclerView 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/dummyModels"
   android:name="com.gustavopeiretti.tabexample.ItemFragment"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:layout_marginLeft="16dp"
   android:layout_marginRight="16dp"
   app:layoutManager="LinearLayoutManager"
   tools:context="com.gustavopeiretti.tabexample.ItemFragment"
   tools:listitem="@layout/fragment_item" />

Recuerda que antes creaste el PagerAdapter que recibe este array y luego crea las instancias de ItemFragment para cada tab pasandole el respectivo array del modelo a mostrar.

Tu clase ItemFragment recibirá como argumento el array del modelo que deseas mostrar en este fragment y lo dejará como parámetro para cuando se cree el Fragment efectivamente en onCreate().


public static ItemFragment newInstance(DummyModel[] dummyModels) {
   ItemFragment fragment = new ItemFragment();
   Bundle args = new Bundle();
   args.putParcelableArray(KEY_MODEL, dummyModels);
   fragment.setArguments(args);
   return fragment;
}

onCreate() se ejecuta cuando efectivamente se crea el fragment y allí obtendremos la lista que dejamos previamente en el Bundle.


public void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   if (getArguments() == null) {
       throw new RuntimeException("You must to send a dummyModels ");
   }
   dummyModels = (DummyModel[]) getArguments().getParcelableArray(KEY_MODEL);

}

onCreateView() se ejecuta cuando se “pinta” la interface, allí debes crear el RecyclerView que usarás definiendo el Adapter y le pasaremos el listener que al final recibirá la acción click sobre el item y la comunicará al Activitity TabExampleMainActivity
Te recomiendo leer luego este otro post a fin de comprender mejor RecyclerView


public View onCreateView(LayoutInflater inflater, ViewGroup container,
                        Bundle savedInstanceState) {
   View view = inflater.inflate(R.layout.fragment_item_list, container, false);

   Context context = view.getContext();
   RecyclerView recyclerView = (RecyclerView) view;
   recyclerView.setLayoutManager(new LinearLayoutManager(context));
   recyclerView.setAdapter(new ItemRecyclerViewAdapter(dummyModels, interactionListener));
   return view;
}


Observa que las Activities (en este ejemplo ‘TabExampleMainActivity’) que usen este Fragment están obligadas a implementar OnListFragmentInteractionListener . Esto es a fin de poder enviarle luego a la Activity el ítem que fue elegido al hacer click. Por lo que TabExampleMainActivity debe implementar este Listener.

Así queda tu clase ItemFragment completa


public class ItemFragment extends Fragment {

   private static final String KEY_MODEL = "KEY_MODEL";

   private DummyModel[] dummyModels;
   private OnListFragmentInteractionListener interactionListener;

   public ItemFragment() {
   }

    /**
     * Receive the model list
     */
   public static ItemFragment newInstance(DummyModel[] dummyModels) {
       ItemFragment fragment = new ItemFragment();
       Bundle args = new Bundle();
       args.putParcelableArray(KEY_MODEL, dummyModels);
       fragment.setArguments(args);
       return fragment;
   }

   @Override
   public void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       if (getArguments() == null) {
           throw new RuntimeException("You must to send a dummyModels ");
       }
       dummyModels = (DummyModel[]) getArguments().getParcelableArray(KEY_MODEL);

   }

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

       Context context = view.getContext();
       RecyclerView recyclerView = (RecyclerView) view;
       recyclerView.setLayoutManager(new LinearLayoutManager(context));
       recyclerView.setAdapter(new ItemRecyclerViewAdapter(dummyModels, interactionListener));
       return view;
   }


   @Override
   public void onAttach(Context context) {
       super.onAttach(context);
       // activity must implement OnListFragmentInteractionListener
       if (context instanceof OnListFragmentInteractionListener) {
           interactionListener = (OnListFragmentInteractionListener) context;
       } else {
           throw new RuntimeException(context.toString()
                   + " must implement OnListFragmentInteractionListener");
       }
   }

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

   /**
    * This interface must be implemented by activities that contain this
    * fragment to allow an interaction in this fragment to be communicated
    * <p/>
    */
   public interface OnListFragmentInteractionListener {
       void onListFragmentInteraction(DummyModel item);
   }
}


El RecyclerView.Adapter

Crea esta clase ItemRecyclerViewAdapter que extienda de RecyclerView.Adapter la que será al final la encargada de setear los valores que recibe y de informar haciendo uso del listener ItemFragment.OnListFragmentInteractionListener cuando se haga click sobre un item.


public class ItemRecyclerViewAdapter extends RecyclerView.Adapter<ItemRecyclerViewAdapter.ViewHolder> {

   private final DummyModel[] dummyModels;
   private final ItemFragment.OnListFragmentInteractionListener interactionListener;

   public ItemRecyclerViewAdapter(DummyModel[] items, ItemFragment.OnListFragmentInteractionListener listener) {
       dummyModels = items;
       interactionListener = listener;
   }

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

   @Override
   public void onBindViewHolder(final ViewHolder holder, int position) {
       DummyModel dm = dummyModels[position];
       holder.dummyModelItem = dm;
       holder.idView.setText(dm.getId());
       holder.titleView.setText(dm.getTitle());
       holder.detailView.setText(dm.getDetail());

       holder.mView.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               if (null != interactionListener) {
                   // Notify the active callbacks interface (the activity, if the
                   // fragment is attached to one) that an item has been selected.
                   interactionListener.onListFragmentInteraction(holder.dummyModelItem);
               }
           }
       });
   }

   @Override
   public int getItemCount() {
       return dummyModels.length;
   }

   public class ViewHolder extends RecyclerView.ViewHolder {
       public final View mView;
       public final TextView idView;
       public final TextView titleView;
       public final TextView detailView;
       public DummyModel dummyModelItem;

       public ViewHolder(View view) {
           super(view);
           mView = view;
           idView = (TextView) view.findViewById(R.id.id);
           titleView = (TextView) view.findViewById(R.id.title);
           detailView = (TextView) view.findViewById(R.id.detail);
       }
   }
}

Descarga este código completo para visualizar en Android Studio

Compartir esto:

4 thoughts on “Android Tabs con Fragment y RecyclerView

  1. Carlos Salvador

    Hola Gustavo, gracias por este magnífico tutorial. Tengo una duda, ¿por qué el adaptador extiende de FragmentStatePagerAdapter en vez de FragmentPagerAdapter?, pensaba que el primero solo se usaba cuando hubiese gran número de páginas. Un saludo. Carlos

    Reply
    1. Gustavo Post author

      Hola. Es correcto se usa cuando hay un mayor numero de paginas. Hace un uso más eficiente de la memoria por lo que es un recurso recomendado para tal fin. saludos!

      Reply
  2. rubenn121

    Hola he aumentado el siguiente codigo para poder eliminar una fila haciendo click en la fila
    notifyItemRemoved(position);
    notifyItemRangeChanged(position, dummyModels.length);

    tambien elimino la fila del vector

    Elimina bien las filas de los 3 tabs pero cuando regreso al tab1 o al tab3 se reponen todas las filas eliminadas como puedo hacer para que esto no suceda porq mi intencion es que se guarden las filas que ya he eliminado y no se esten reconstruyendo.

    Reply
    1. Gustavo Post author

      Hecha un vistazo al método notifyDataSetChanged() del adapter. Saludos!

      Reply

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *