martes, 15 de diciembre de 2015

Hablemos de Fragmentos I

Esta entrada no tiene por objeto ser un tutorial mas de los que arrancan de cero diciéndote que "un fragmento puede ser entendido como una subactividad, cuyo ciclo de vida depende de... bla, bla, bla".

La idea es que ya conoces lo que son los fragmentos, que has hecho alguna app usandolos, pero que tratando de dominarlos sientes que sabes menos que al principio.
A mi me paso tener que convertir una aplicacion basada en Tabs y Activities a una aplicacion basada en un Side Menu con Fragmentos. La conversion en si es bastante directa, lo que mas tienes que tener en cuenta es que ahora tienes un metodo onCreateView que es el responsable de hacer lo que en una Activity normalmente hace el metodo onCreate. Luego adaptar los metodos de callback (onStart, onResume, etc) a sus homologos para fragmentos y poco mas.
No es big deal, como se dice en ingles. Los problemas arrancan cuando ya tienes todo eso solucionado, pero tienes que hacer funcionar la navegacion.
Desde mi perspectiva los dos problemas principales que vas a encontrar y que te puede a llevar a darte la cabeza contra el monitor repetidamente son la navegacion y su derivado, el mantener estado en los fragmentos mientras ocurre la navegacion.

Navegacion entre fragmentos (sin tener en cuenta el estado de las vistas)

Por ejemplo supongamos que tenes una app con dos actividades y dentro de una de las actividades llamamos a tres fragmentos (A, B y C).

Es decir que iniciamos con la Activity 1 y pasamos a la Activity 2 que comienza mostrando el fragmento A, luego el B y luego el C:



Si el usuario viendo el Fragmento C le diera al boton Back, esperaria ver el Fragmento B, pero nones. Veria la IU de la Activity 1. Y no, esto no es lo esperado.
La clave para la navegacion con Fragmentos pasa por un objeto que se llama BackStack. Suele ser mas incomprendido  que una película de Fellini, pero es una de las claves de la felicidad en el mundo Fragment. La fuente de la incomprehension es que la gente normalmente piensa en el como en un Stack donde los objetos fragmentos se apilan. Y no se trata de apilar Fragmentos sino de apilar transacciones.

Suponte que quieres agregar esos tres fragmentos:


        FragmentA fa = new FragmentA();

        FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();

        fragmentTransaction.add(R.id.fragments, fa, "A");

        fragmentTransaction.addToBackStack("addA");

        fragmentTransaction.commit();

        .....



        FragmentB fb = new FragmentB();

        FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();

        fragmentTransaction.add(R.id.fragments, fb, "B");

        fragmentTransaction.addToBackStack("addB");

        fragmentTransaction.commit();

        ......



        FragmentC fc = new FragmentC();

        FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();

        fragmentTransaction.add(R.id.fragments, fc, "C");

        fragmentTransaction.addToBackStack("addC");

        fragmentTransaction.commit();



Si el container (R.id.fragments) fuera un FrameLayout (luego veremos que con un LinearLayout no verias lo mismo) verias aparecer la sequencia
A -> B -> C
La clave aqui es si usas o no addToBackStack. Si usas, tus transacciones pasaran a la pila y por ende seran reversibles. Es decir que al ir hacia atras el usuario, podra recorrer el camino inverso, navegando hacia el origen del Stack, re-ejecutando las transacciones que va encontrando en su camino descendente.
El BackStack es una pila de transacciones no una pila de Fragmentos.



Total que para navegar hacia atras entre fragmentos lo que necesitamos es interceptar el boton Back y navegar hacia atras por el BackStack.

La forma de ir hacia atras en el backstack es utilizando el metodo popBackStack() y conectandolo con la accion del boton Back. Es decir que popBackStack() se comportara como el boton Back, en el sentido de que navega hacia atras por la pila. Hacia la anterior transaccion inferior siguiente si es que no lleva parametro y hacia una transaccion determinada si se le pasa como parametro el ID de la transaccion.

Para ver todo esto funcionando te dejo este Proyecto BackNav en Github. Es una app que consta de dos actividades y tres fragmentos que se van agregando en la segunda actividad. Al tratar de navegar hacia atras la aplicacion solo se movera entre Activities. Si descomentas el bloque de codigo onBackPressed() podras navegar hacia atras entre los fragmentos.

 @Override
    public void onBackPressed() {
        // your code.
        int t = mFragmentManager.getBackStackEntryCount();
        if(t > 1){
            FragmentManager.BackStackEntry bse = mFragmentManager.getBackStackEntryAt(t-1);
            Log.i("MAIN2","Estaba en: "+bse.getName());
            mFragmentManager.popBackStack();//retrocede en la pila
        }else{
            super.onBackPressed();//como si apretaran el back entre actividades
        }
    }
    


En onBackPressed() tenemos dos metodos importantes;

getBackStackEntryCount(); que devuelve la cantidad de entradas que tenemos en la pila.
getBackStackEntryAt(x); que devuelve la entrada de la posición x

Con estos dos metodos comprobamos si tenemos entradas en la pila y, si tenemos, retrocedemos con popBackStack(), si ya no tenemos mas ejecutamos super.onBackPressed() que causa el mismo efecto que el boton Back entre actividades, llevandonos a la Actividad 1.

Entendiendo mejor el BackStack

En el Proyecto BackStack Experiments puedes bajarte un ejemplo de codigo que te permitira ver el estado del backstack a medida que ejecutas transacciones con tus fragmentos.

Se trata de una Activity con dos fragmentos y varios metodos para operar con ellos:

public class MainActivity extends Activity implements FragmentManager.OnBackStackChangedListener, FragmentA.OnFragmentInteractionListener, FragmentB.OnFragmentInteractionListener{



    FragmentManager mFragmentManager;

    TextView mBackStack;



    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

        setContentView(R.layout.activity_main);

        mBackStack = (TextView)findViewById(R.id.backstack);

        mFragmentManager = getFragmentManager();

        mFragmentManager.addOnBackStackChangedListener(this);



    }



    public void addA(View v){

        FragmentA fa = new FragmentA();

        FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();

        fragmentTransaction.add(R.id.fragments, fa, "A");

        fragmentTransaction.addToBackStack("addA");

        fragmentTransaction.commit();

    }

    public void addB(View v){

        FragmentB fb = new FragmentB();

        FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();

        fragmentTransaction.add(R.id.fragments, fb, "B");

        fragmentTransaction.addToBackStack("addB");

        fragmentTransaction.commit();

    }

    public void removeA(View v) {

        FragmentA fa = (FragmentA) mFragmentManager.findFragmentByTag("A");

        FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();

        if (fa != null) {

            fragmentTransaction.remove(fa);

            fragmentTransaction.addToBackStack("removeA");

            fragmentTransaction.commit();

       } else {

            Toast.makeText(this, "Fragment A not added", Toast.LENGTH_LONG).show();

        }

    }

    public void removeB(View v) {

        FragmentB fb = (FragmentB) mFragmentManager.findFragmentByTag("B");

        FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();

        if (fb != null) {

            fragmentTransaction.remove(fb);

            fragmentTransaction.addToBackStack("removeB");

            fragmentTransaction.commit();

        } else {

            Toast.makeText(this, "Fragment B not added", Toast.LENGTH_LONG).show();

        }

    }

    public void replaceA(View v){

        FragmentA fa = new FragmentA();

        FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();

        fragmentTransaction.replace(R.id.fragments, fa, "A");

        fragmentTransaction.addToBackStack("replaceWithA");

        fragmentTransaction.commit();

    }

    public void replaceB(View v){

        FragmentB fb = new FragmentB();

        FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();

        fragmentTransaction.replace(R.id.fragments, fb, "B");

        fragmentTransaction.addToBackStack("replaceWithB");

        fragmentTransaction.commit();

    }

    public void attachB(View v){

        FragmentB fb = (FragmentB) mFragmentManager.findFragmentByTag("B");

        FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();

        if(fb != null){

            fragmentTransaction.attach(fb);

            fragmentTransaction.addToBackStack("attachB");

            fragmentTransaction.commit();

        }else{

            Toast.makeText(this, "Can't attach Fragment B because does not exist", Toast.LENGTH_LONG).show();

        }



    }

    public void detachB(View v){

        FragmentB fb = (FragmentB) mFragmentManager.findFragmentByTag("B");

        FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();

        if(fb != null){

            fragmentTransaction.detach(fb);

            fragmentTransaction.addToBackStack("detachB");

            fragmentTransaction.commit();

        }else{

            Toast.makeText(this, "Can' detach Fragment B because does not exist", Toast.LENGTH_LONG).show();

        }

    }

    public void pop_add_B(View v){

        mFragmentManager.popBackStack("addB", 0);

    }



    public void back(View v){

        mFragmentManager.popBackStack();

    }





    @Override

    public boolean onCreateOptionsMenu(Menu menu) {

        // Inflate the menu; this adds items to the action bar if it is present.

        getMenuInflater().inflate(R.menu.menu_main, menu);

        return true;

    }



    @Override

    public boolean onOptionsItemSelected(MenuItem item) {

        // Handle action bar item clicks here. The action bar will

        // automatically handle clicks on the Home/Up button, so long

        // as you specify a parent activity in AndroidManifest.xml.

        int id = item.getItemId();



        //noinspection SimplifiableIfStatement

        if (id == R.id.action_settings) {

            return true;

        }



        return super.onOptionsItemSelected(item);

    }



    @Override

    public void onBackStackChanged() {

        mBackStack.setText(mBackStack.getText()+"\n");

        mBackStack.setText("BackStack Status:\n");

        mBackStack.setText("-----------------\n");

        int count = mFragmentManager.getBackStackEntryCount();

        for(int i=count-1; i>= 0 ; i--){

            FragmentManager.BackStackEntry entry = mFragmentManager.getBackStackEntryAt(i);

            mBackStack.setText(mBackStack.getText()+" "+entry.getName()+" \n");

        }

    }



    @Override

    public void onFragmentInteraction(Uri uri) {

        Log.i("MainActivity", uri.toString());

    }

}

Es recomendable que compruebes la diferencia entre utilizar como host view de tus fragmentos un FrameLayout (que los apila en el eje Z) y un LinearLayout que los instala en un mismo plano de manera que el primero tiene prioridad sobre el segundo. Y si usa;

 android:layout_width="match_parent"
 android:layout_height="match_parent"
,

directamente no veras el segundo y te parecera que nada ha pasado al agregar el segundo fragmento.

En la proxima entrada vemos como mantener estado en las vistas. Espero que te sirva. Tus comentarios seran bienvenidos.