BasicNavigationDrawer – ActivityData – RecyclerView ItemClickListener at Fragment

En esta entrada vamos a extender con un par de técnicas el proyecto de BasicNavigationDrawer.

  • Utilizar datos, alojados en la Activity como datos compartidos por los fragments (para leerlos y modificarlos)
  • Conseguir que un Fragment sea el Listener de los ItemClicks en un RecyclerView (y no la Activity)

Con los ViewModels y con LiveData (que veremos posteriormente) podemos compartir datos entre activities y fragmentes y además recibir eventos cuando los datos cambian, sin necesidad de tener que ir a buscar los datos.

Para ver las ventajas de los ViewModels y LiveData, lo haremos mejor si primero intentamos conseguir lo mismo con la forma tradicional de definir interfaces en los fragments que implementa la activida y a través de los métodos de la interfaz, llevar y traer datos entre los fragments y la actividad que los hospeda.

Por otro lado, hasta ahora cuando hemos utilizado un RecyclerView con Adapter y ViewHolder, hemos tratado los eventos click en los items del RecyclerView, bien en el Holder directamente o hemos propagado el evento hacia la activity. Pero también se puede propagar el evento a un Fragment.

Podéis descargar el proyecto de esta entrada desde GitHub

Clonar el proyecto desde el BasicNavigationDrawer para continuar desde el.

Un RecyclerView en las Preferencias

En la imagen animada podemos ver lo nuevo que hace la aplicación.

Básicamente en el Fragment de Preferencias tenemos un RecyclerView y al seleccionar un color de la lista se marca como seleccionado.

Al volver al Wellcome Fragment tenemos el color como fondo del mismo.

El esquema

Para poder hacer eso (sin ViewModel ni LiveData) tenemos que mantener el color seleccionado como dato de la Activity que hospeda a los dos fragments, Preferencias y Wellcome.

La selección se hace en el Fragment de Preferencias y ahora el click lo tratamos directamente en el Fragment Preferencias y desde el actualizamos el dato en la actividad, para que cuando el Fragment Wellcome arranque lea el color seleccionado actual y pinte su fondo de ese color.

En la imagen (pincha en ella para agrandarla) vemos el esquema con el que vamos a trabjar en esta entrada, vamos a repsarla:

Por un lado tenemos el Interfaz FragmentListener que ofrece dos funciones getDato y setDato para leer y escribir un dato respectivamente. Este interfaz es implementado por la HostActivity (la que hospeda a los fragments) por lo que tiene esos métodos para acceder al dato a compartir entre/con los fragments.

Cuando un fragment quiere acceder a los datos compartidos que están ubicados en la Activity tiene que acceder a ella, a traves de la referencia que recibe en en onAttach() y utilizar su método getDato o setDato.

Por otro lado tenemos un RecyclerView con patrón ViewHolder. Queremos que al seleccionar un elemento del RecyclerView, cuyo dato gestiona el Adapter, sea enviado a un fragment o una activity.

En este ejemplo, el RecyclerView lo instanciamos en el Fragment A. El Adapter del RecyclerView declara el interfaz onItemClickListener, para que la vista que quiera capturar los eventos clicks del RecyclerView lo implemente. En este ejemplo vamos a implementarlo (el interfaz) en el FragmentA. El click realmente se produce en el ViewHolder de cada elemento del RecyclerView pero el dato al que corresponde lo tenemos en el adapter, por eso, en el onBindViewHolder del Adapter (que es donde sabemos qué dato es) definimos el setOnClickListener, es decir, quién es el que procesará el click. En nuestro ejemplo hemos decidido propagar el evento al Fragment A, no a la Host Activity, por lo que éste implementa onItemClickListener y por tanto en el setOnClickListener llamamos al método onItemClick del Fragment A (el que implementa por la interfaz).

Para que desde el Adapter tengamos referencia al Fragment A y poder por tanto llamar a su método onItemClick, lo que hacemos es obtener una referencia a la misma en el constructor del Adapter, por lo que como parámetro el constructor del Adapter recibirá un OnItemClickListener. Cuando desde el Fragment A creamos el Adapter para asignarlo al RecyclerView le pasamos en el constructor this como referencia. Como el Fragment A implementa la interfaz es un OnItemClickListener.

El código

Como partimos del ejemplo BasicNavigationDrawer los nombres del ejemplo anterior realmente son los siguientes:

  • La HostActivity es MainActivity
  • El Fragment A es PreferenciasFragment
  • El Fragment B es WellcomeFragment
  • El interfaz OnItemClickListener es ColorItemClickListener

Lo que queremos hacer es mostrar un RecyclerView con colores en las preferencias y que al seleccionar el color se coloque éste como color de fondo del WellcomeFragment.

Primero definimos el color, vamos a tener un objeto Pair(<F><S>), una tupla formada por dos objetos, el primero un String (el nombre del color) y el segundo un Integer (el código de color). Pero en el Adapter creamos una lista de colores, realmente un ArrayList de Pairs(String,Integer).

ColorRVAdapter

Veamos el código del ColorRVAdapter primero:

public class ColorRVAdapter extends RecyclerView.Adapter<ColorRVAdapter.ColorViewHolder> {
    private ColorsItemClickListener colorsItemClickListener;
    private ArrayList<Pair<String,Integer>> bgColors;

    public interface ColorsItemClickListener {
        void onColorItemClicked(Pair<String,Integer> color);
    }

    public ColorRVAdapter(ColorsItemClickListener listener) {
        super();
        this.colorsItemClickListener = listener;
        SetupColors();
    }

    private void SetupColors(){
        bgColors = new ArrayList<Pair<String,Integer>>();
        bgColors.add(new Pair<>("RED", Color.RED));
        bgColors.add(new Pair<>("GREEN",Color.GREEN));
        bgColors.add(new Pair<>("BLUE",Color.BLUE));
        bgColors.add(new Pair<>("YELLOW",Color.YELLOW));
        bgColors.add(new Pair<>("BLACK",Color.BLACK));
        bgColors.add(new Pair<>("MAGENTA",Color.MAGENTA));
        bgColors.add(new Pair<>("WHITE",Color.WHITE));
    }

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

    @Override
    public void onBindViewHolder(@NonNull ColorViewHolder holder, int position) {
        final Pair color = bgColors.get(position);

        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                colorsItemClickListener.onColorItemClicked(color);
            }
        });

        if (color.first.toString().equals("BLACK") || color.first.toString().equals("BLUE")){
            holder.tvColor.setTextColor(Color.WHITE);
        }
        holder.tvColor.setText(color.first.toString());
        holder.tvColor.setBackgroundColor((Integer) color.second);
    }

    @Override
    public int getItemCount() {
        return bgColors.size();
    }

    // --------- IMPLEMENTACION DE NUESTRO VIEW HOLDER ESPECÍFICO ----------------------------
    // stores and recycles views as they are scrolled off screen
    public class ColorViewHolder extends RecyclerView.ViewHolder{
        TextView tvColor;

        ColorViewHolder(View itemView) {
            super(itemView);
            tvColor = itemView.findViewById(R.id.tvColor);
        }
    }
    // ---------------------------------------------------------------------------------------
}

Declaramos el interface ColorItemClickListener y declaramos una variable colorsItemClickListener de este tipo, para saber quien va a escuchar ese evento.

Declaramos el constructor del adapter que recibe la referencia al listener que va a escuchar y que asignamos a colorsItemClickListener. Además llamamos a un método que nos crea un ArrayList (bgColors) de colores. Esto podría venir de otro sitio (base de datos, preferencias, etc…), pero lo hacemos aquí por simplicidad.

Como el ColorRVAdapter extiende de un RecyclerView.Adapter<VH> (clase abstracta) tenemos que instanciar un ViewHolder y pasárselo como nombre en el extends, y además implementar los métodos onCreateViewHolder, onBindViewHolder y getItemCount como hicimos en BasicRecyclerView.

Como vemos el ViewHolder no implementa el OnClick, eso lo dejamos para el Adapter, puesto que tiene el dato que hay que enviar con la propagación del evento hacia el Fragment.

Cuando el ViewHolder se vincula al adapter en onBindViewHolder recibimos el ViewHolder en si (parámetro holder) y la posición que ocupa el viewHolder en el RecyclerView (parámetro position). En el onBindViewHolder, obtenemos el color apropiado del ArrayList bgColors mediante el parámetro position. Establecemos el onClickListener en el itemView del holder:

        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                colorsItemClickListener.onColorItemClicked(color);
            }
        });

En laa interfaz OnColorItemClicked hemos definido el método onColorItemClicked donde le pasamos el Pair color seleccionado, es decir, le pasamos en un objeto Pair el nombre del color y el color en si mismo.

El siguiente código en el onBindViewHolder:

        if (color.first.toString().equals("BLACK") || color.first.toString().equals("BLUE")){
            holder.tvColor.setTextColor(Color.WHITE);
        }
        holder.tvColor.setText(color.first.toString());
        holder.tvColor.setBackgroundColor((Integer) color.second);

coloca el color y el nombre en el holder, de forma que la vista de cada Item del RecyclerView salga con su color de fondo, además si el color es negro o blanco la letra la ponemos en blanco porque si no no tiene contraste suficiente. Un objeto Pair tiene dos referencias a sus objetos que lo componen, first y second. A través de ellas llegamos al objeto en si. En este caso a un String (first) y a un Intener (second).

PreferenciasFragment

Para el funcionamiento del ejemplo, los métodos que vamos a utilizar son onAttach(), onViewCreated y onColorItemClicked. El resto son el código genérico de un Fragment cuando se crea desde New->Fragment.

    @Override
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
        fragmentListener = (FragmentListener) context;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        //Instanciamos el recycler view
        rvColors = (RecyclerView) getView().findViewById(R.id.rvColors);

        ColorRVAdapter adapter = new ColorRVAdapter(this);
        rvColors.setLayoutManager(new LinearLayoutManager(requireContext()));
        rvColors.setAdapter(adapter);

        selectedColor = fragmentListener.getSelectedColor();
        if (selectedColor != null){
            TextView tvSelectedColor = view.findViewById(R.id.tvSelectedColor);

            tvSelectedColor.setBackgroundColor(selectedColor.second);
            tvSelectedColor.setText(selectedColor.first.toString());

            if (selectedColor.second == Color.BLUE || selectedColor.second == Color.BLACK){
                tvSelectedColor.setTextColor(Color.WHITE);
            }
            else tvSelectedColor.setTextColor(Color.BLACK);
        }
    }

    @Override
    public void onColorItemClicked(Pair<String,Integer> color) {
        TextView tvSelectedColor = getView().findViewById(R.id.tvSelectedColor);

        tvSelectedColor.setBackgroundColor(color.second);
        tvSelectedColor.setText(color.first.toString());

        if (color.second == Color.BLUE || color.second == Color.BLACK){
            tvSelectedColor.setTextColor(Color.WHITE);
        }
        else tvSelectedColor.setTextColor(Color.BLACK);
        //Toast.makeText(getActivity(), "Item clicked: " + color.first.toString(), Toast.LENGTH_SHORT).show();

        fragmentListener.setSelectedColor(color);
    }

En onAttach() simplemente nos quedamos con la referencia a la HostActivity, en este caso la MainActivity que es una FragmentListener. Una aclaración, podríamos haber declarado la interfaz FragmentListener en el Fragment, pero como es genérica para todos los fragments la declaramos fuera.

En onviewCreated se instancia el RecyclerView, se crea al adaptador y se vinculan. Después en la variable selectedColor que se ha definido a nivel de clase, recogemos el dato compartido por la Activity, el selectedColor utilizando el método get de la interfaz FragmentListener que implementa la Activity (el código de la Activity lo comentamos después). fragmentListener es la referencia a la Activity que recibe el fragment en el onAttach(). Si el color (objeto Pair) no es nulo, es que ya han seleccionado un color y ponemos el color de fondo del textView que nos sirve de chivato del color seleccionado (ver animación inicial).

En onColorItemClicked que es el método que sobrecargamos para recibir la propagación del evento recibimos un Pair con el color seleccionado por el usuario y hacemos lo mismo con el cuadrado chivato. Pero además y lo más importante es que el dato (el Pair color) lo actualizamos en la copia compartida que tenemos en la MainActivity, para lo que usamos el médoto fragmentListner.setSelectedColor(color)

MainActivity

De cara a este ejemplo, de la MainActivity nos interesa el código de cómo define el dato a compartir, en este caso selectedColor y cómo implementa la interfaz FrangmentListener mediante la cual proporciona acceso con get y set al dato compartido:

public class MainActivity extends AppCompatActivity implements FragmentListener{

    private DrawerLayout drawerLayout;
    private AppBarConfiguration appBarConfiguration;

    private Pair<String,Integer> selectedColor = null;

    //Implementando las funciones de FragmentListener ------------------
    public Pair<String,Integer> getSelectedColor(){
        return selectedColor;
    }

    public void setSelectedColor(Pair<String,Integer> selectedColor){
        this.selectedColor = selectedColor;
    }
    //------------------------------------------------------------------

Conslusión

Hemos visto un ejemplo de como compartir datos entre la actividad y los fragments que se hospedan en ella.

Como vemos es un poco artificioso, pero una forma en la que podemos conseguir esto.

Pero en siguientes entradas veremos este mismo ejemplo utilizando técnicas más avanzadas, ViewModel y LiveData que permitirán un códido más compacto y no tener que definir interfaces para propagar los eventos ni para acceder a los datos compartidos.