12 de abril de 2016

Android - Flow result instead of Activity result or how to get a result between activities

    • How many times have you needed to return a result from one Activity to another?
    • A lot of times, actually, I don't remember any app that I've writen without that functionality.
    • was that difficult? How did you do it?
    • It wasn't, in fact it was pretty easy. Android has a method startActivityForResult(Intent, int) that allows us to launch an Activity waiting for its result in a method called onActivityResult(int, int, Intent), no mistery there.
    • Well, and what happens when there is more than one Activityinvolved in the process?
    • ah, it is very easy too, when the second Activity of the flow is launched you have to add a the flag Intent.FLAG_ACTIVITY_FORWARD_RESULT to the Intent and then it will be your next Activity who will be in charge to call setResult to deliver the result.
    • is that all?
    • Well, it is not, you need to call Activity.finish in the first Activity of the flow, else when the user ends in the second one, he will get back to the first one and he will need to click back to leave the flow and receive the result.
    • That seems a little bit... odd?
    • But you can do Activity.finish as I said before, in that way when the user finishes the second Activity the flow will be finished as well.
    • and, what happens if the user clicks back in the last Activity of the flow?
    • That it leaves the flow. 
    • But then the user loses all the data provided during the flow, is that right?
    • Yes.
    • And what if instead of a flow with 2 activities we have a flow with 5, 6 or even 10 activities?
    • Then the user would need to start the flow from the beginning.
    • Well, that was the point from the beginning, I've also faced that problem and so far, the solution presented below is the one that I like more.

    How?

    This is very easy and in only a few steps.
    • First we need to start the first Activity of the flow like startActivityForResult:
    
        Intent intent = new Intent(this, ProfileNameStepActivity.class);
        startActivityForResult(intent, PROFILE_REQUEST_CODE_FLOW);
        
    
    • Then we wait the result like we always do when there is only one Activity in the flow.
    
      @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == PROFILE_REQUEST_CODE_FLOW && resultCode == RESULT_OK) {
          Profile profile = ExtrasUtils.getProfile(data);
          onProfileCompleted(profile);
        } else {
          super.onActivityResult(requestCode, resultCode, data);
        }
      }
      
    
    • Also in the last Activity of the flow we add (like always): 
    
        Intent intent = new Intent();
        intent.putExtra(ExtrasUtils.EXTRA_PROFILE,
            new Profile.Builder().withName(name).withAddress(address).build());
        setResult(RESULT_OK, intent);
        finish();
    
    
    So far we haven't done anything special thing, this code we've written hundreds of times.
    This is the interesting part...
    • Now we start any other Activity involved in the flow with startActivityForResult, while this time the requestCode is not important because we don't wait for it result:
    
          /*   
          We can pass any number less the ones that we
          listen here, in this case ADDRESS_REQUEST_CODE_FLOW 
          (unless we want to start a sub-flow, I'll explain that later
          */
          Intent intent = new Intent(this, ProfileFinalStepActivity.class);
          startActivityForResult(intent, 0);
    
    
    • Finally we need this piece of code in our BaseActivity and all our activities will inherit from it.
    
      @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        
        if (resultCode == RESULT_OK) {
          setResult(resultCode, data);
          finish();
        }
      }
    
    
    This last part of the code is the one that does the magic, if you pay attention, we have started all our activities in the flow with startActivityForResult, so when the next Activity is finished our activities will receive a call in onActivityResult, and we do check if we already got a result there from the next Activity, so if the Activity receives the result, we set the result for the current Activity and the previous one will do the same until any Activity has its onActivityResult overriden and it is waiting for the requestCode.
    This same technique can be used to include sub-flows inside a flow, imagine for example that the user can do shopping in your application and before the user pays it has to provide some basic info like the name or the address (or a complete profile).
    The same code that we have used to start/finish a flow, we can use it in any step of the flow to add a sub-flow and it will work just in the same way.

    What have we achieved with this?

    • Now we are able to start/finish a flow in the same place.
    • We can deliver a result of a flow (group of activities) very easy.
    • We can add additional flows inside a flow easily.

9 de abril de 2016

Android - Flow result en lugar de Activity result o como obtener un resultado entre actividades

    • ¿Cuantas veces te has encontrado que necesitas entregar un resultado de una Activity a otra?
    • Muchas, no recuerdo ninguna aplicación donde no lo haya tenido que hacer.
    • ¿Te resultó difícil? ¿Como lo hiciste?
    • No, de hecho fue muy fácil, Android siempre ha tenido un método startActivityForResult(Intent, int) para lanzar una Activity de la que esperas un resultado y otro método onActivityResult(int, int, Intent) para recibirlo, no tiene ningún misterio.
    • Bien ¿y que pasa si hay más de una Activity implicada en el proceso?
    • Ah, también es muy fácil, cuando lanzas la segunda Activityimplicada en el flow lo haces añadiendo el flag Intent.FLAG_ACTIVITY_FORWARD_RESULT al Intent y entonces será tu siguiente Activity la encargada de llamar a setResult para entregar el resultado.
    • ¿Eso es todo?
    • Bueno, en realidad no, en realidad tienes que hacer un Activity.finish de la primera Activity del flow porque sino cuando el usuario termine la segunda, volverá a la primera y tendrá que salir manualmente de esta para recibir el resultado.
    • Mmmm... ¿eso no es muy elegante no?
    • Bueno, puedes llamar a Activity.finish como he dicho antes, de esta forma cuando el usuario termina en la segunda Activity, va directamente a la que está esperando el resultado.
    • ¿Y que pasa si el usuario pulsa back cuando está al final del flow?
    • Que se sale del flow.
    • Pero entonces pierde toda la información que tenía entre medias ¿no?
    • Si.
    • ¿Y si en lugar de tener un flow de 2 actividades tienes un flow de 5 o 6 o incluso de 10?
    • Pues el usuario tendría que comenzar el flow desde el principio.
    • Bien, ahí es donde quería llegar, yo también me he enfrentado a este problema y de momento, esta es la solución que mas me hag gustado.

    ¿Como hacerlo?

    Esto es bastante sencillo de conseguir y lo más importante, con poco código:
    • Primero necesitamos lanzar la primera Activity del flow con startActivityForResult:
    
        Intent intent = new Intent(this, ProfileNameStepActivity.class);
        startActivityForResult(intent, PROFILE_REQUEST_CODE_FLOW);
        
    
    • Y esperamos el resultado como hariamos si el flow tuviese una sola Activity.
    
      @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == PROFILE_REQUEST_CODE_FLOW && resultCode == RESULT_OK) {
          Profile profile = ExtrasUtils.getProfile(data);
          onProfileCompleted(profile);
        } else {
          super.onActivityResult(requestCode, resultCode, data);
        }
      }
      
    
    • También en la última Activity del flow añadiremos:
    
        Intent intent = new Intent();
        intent.putExtra(ExtrasUtils.EXTRA_PROFILE,
            new Profile.Builder().withName(name).withAddress(address).build());
        setResult(RESULT_OK, intent);
        finish();
    
    
    De momento no hemos hecho nada especial, esté código lo hemos escrito cientos de veces cada vez que lanzamos una Activityesperando un resultado de ella.
    Esta es la parte interesante...
    • Ahora lanzamos el resto de actividades del flow con startActivityForResult también, aunque en este caso el requestCode no es importante ya que no esperamos su resultado:
    
          /*
          Podemos pasar cualquier número como request code aquí porque no lo escucharémos 
          (a no ser que queramos lanzar un sub-flow, después explico que sería un sub-flow y 
          como incluirlo)
          */
          Intent intent = new Intent(this, ProfileFinalStepActivity.class);
          startActivityForResult(intent, 0);
    
    
    • Finalmente necesitamos esta pieza de código en nuestra BaseActivity de la cual heredan todas nuestras actividades.
    
      @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        
        if (resultCode == RESULT_OK) {
          setResult(resultCode, data);
          finish();
        }
      }
    
    
    Esta última parte de código, es la que realmente hace la magia. Si te fijas, como hemos lanzado todas nuestras actividades con startActivityForResult, cuando estas terminen la Activity anterior será llamada en onActivityResult, y lo que estamos haciendo es comprobar si la Activity de la que viene ha añadido el resultado ya, si lo ha hecho, pasamos el resultado a la Activity anterior y esto se hará progresivamente hasta que alguna Activity haya sobreescrito el método onActivityResult y esté actualmente esperando por el requestCode.
    Está misma técnica puede ser utilizada para incluir sub-flows dentro de un flow, imagina por ejemplo que tu aplicación permite al usuario realizar compras y que para realizar esa compra el usuario tiene que proveer alguna información básica como el nombre y la dirección (o un perfil entero).
    El mismo código que hemos utilizado para empezar/acabar un flow, podemos incluirlo en cualquier paso del flow para añadir un sub-flow y funcionará exactamente igual.

    ¿Que hemos conseguido con esto?

    • Ser capaz de empezar y acabar un flow en el mismo sitio.
    • Entregar el resultado de un flow (grupo de actividades) de una forma sencilla.
    • Posibilidad de añadir flows adicionales dentro del flow.

2 de abril de 2016

Android - How to avoid onItemSelected to be called twice in Spinners

The other day the QA team reported us a bug about a blinking effect in one of our screens, specifically in the item detail screen.
It was just open that screen and I saw perfectly what they were talking about, but that wasn't happening before, what had changed?
The thing that had changed was that in that screen, now the user is able to modify the item's category, to be able to do that we added a Spinner to select the category from a given list, once the user select a category we redraw the screen with the new data related to that category.
The problem wasn't easy to be identified, it was happening that we were doing a "bad use" of Spinner.setSelection(), basically we weren't calling to Spinner.setSelection() in the same method where the categories were added to the adapter.
I'm sure that with a piece of code it will be clearer:
This code will call to OnItemSelectedListener.onItemSelected saying that the item 0 was selected.

    spinner = (Spinner) findViewById(R.id.spinner);
    spinner.setOnItemSelectedListener(this);
    adapter = new ArrayAdapter<String>(this, R.layout.simple_spinner_item, items);
    spinner.setAdapter(adapter);
    
But we didn't expect that thing to happen! what we expect is onItemSelected to be called when the user select a item from the Spinner, or maybe we could expect it to be called when spinner.setSelection(position);saying that the item position is selected.
In our app, the user could have selected a category previously, then at some point after the data is loaded from the database we do spinner.setSelection(position);"et voilà"onItemSelected is called twice, the first call is done saying that the item 0 was selected and the second one with the position item was selected.
At this point I decided to search in internet to see how other people had fixed that problem and I found some interesting approaches (and crazy ones):
  • To keep counters for each Spinner with OnItemSelectedListener that are in the screen to skip the first call to onItemSelected.
  • To create a custom adapter and to add spinner.setOnTouchListener() to manage when the user has touched the screen previously.
  • To create a custom view and override the method onClick and call CustomOnItemClickListener.onItemClickwithin it.
None of those options convinced me (I found some more that didn't either), so I decided to investigate a little more and I found that you cannot skip onItemSelected to be called when the adapter is added to the Spinner if the adapter has already items, but we can make it called with the correct element selected if spinnerView.setSelection(position) is called immediately after the items are added to the view/adapter. I've thought in two ways to do that:

First approach

We create the adapter with the items in it and we add it to the Spinner once that we know the item that has to be selected.

    adapter = new ArrayAdapter<>(this, R.layout.simple_spinner_item, items);
    spinner.setAdapter(adapter);
    spinner.setSelection(position);

Second approach

We create the adapter without the items in it and we add it to the Spinner.

    adapter = new ArrayAdapter<>(this, R.layout.simple_spinner_item);
    spinner.setAdapter(adapter);

And once that we know the item that has to be selected.

    spinner.setSelection(10);
    adapter.addAll(items);

In conclusion

It would be nice to set AdapterView.OnItemSelectedListener just after add the items to the adapter and to not see onItemSelected be called, but it isn't, so at least we can do this that is a smart approach.

Complete code for approach 1


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

    final Spinner spinner = (Spinner) findViewById(R.id.spinner);
    spinner.setOnItemSelectedListener(this);

    final ArrayAdapter<String> adapter =
        new ArrayAdapter<>(this, R.layout.simple_spinner_item, items);
    spinner.setAdapter(adapter);
    spinner.setSelection(10);
  }

Complete code for approach 2



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

    final Spinner spinner = (Spinner) findViewById(R.id.spinner);
    spinner.setOnItemSelectedListener(this);

    final ArrayAdapter<String> adapter =
        new ArrayAdapter<>(this, R.layout.simple_spinner_item);
    spinner.setAdapter(adapter);

    findViewById(R.id.select_item).setOnClickListener(new View.OnClickListener() {
      @Override public void onClick(View v) {
        spinner.setSelection(10);
        adapter.addAll(items);
      }
    });
  }

Android - Como evitar que onItemSelected se llame dos veces en Spinners

El otro día nos reportaron un bug diciendo que una de las pantallas de la aplicación hacía un pequeño parpadeo, en concreto esto pasaba en la pantalla de detalle de un item.
Nada más entrar en dicha pantalla, vi a lo que se referían, pero eso no pasaba antes ¿que había cambiado?
Lo que había cambiado es que ahora el usuario podía modificar la categoría del ítem desde esa pantalla, para hacer eso habíamos añadido a la vista un Spinner que permitía seleccionarla dentro de un listado y una vez seleccionada se repintaba la pantalla con nuevos datos relacionados con la categoría.
El problema no fue nada fácil de identificar, lo que estaba pasando es que estábamos haciendo un "mal uso" de Spinner.setSelection(), básicamente no estábamos llamando a Spinner.setSelection() al mismo tiempo que añadíamos las categorías al adapter.
Seguro que con algo de código queda más claro:
Este código llamará a OnItemSelectedListener.onItemSelecteddiciendo que el elemento 0 ha sido seleccionado.

    spinner = (Spinner) findViewById(R.id.spinner);
    spinner.setOnItemSelectedListener(this);
    adapter = new ArrayAdapter<String>(this, R.layout.simple_spinner_item, items);
    spinner.setAdapter(adapter);
    
¡Pero eso no es lo que esperamos! lo que esperamos es que onItemSelected séa llamado cuando el usuario selecciona un item del Spinner o en todo caso podríamos esperar que sea llamado cuando hacemos spinner.setSelection(position); diciendo que el elemento seleccionado fue el position.
En nuestro caso, el usuario podía haber elegido previamente una categoría, así que en algún momento tras cargar los datos de la base de datos hacíamos spinner.setSelection(position); y "et voilà"onItemSelected es llamado dos veces, la primera diciendo que se seleccionó el elemento 0 y la segunda que se seleccionó el elemento position.
Llegado a este punto decidí mirar en la red como solucionaba la gente este problema y encontré cosas muy interesantes (y locas):
  • Mantener contadores por cada elemento con OnItemSelectedListener que tengas en la pantalla para obviar la primera llamada a onItemSelected.
  • Crearse un custom adapter y añadir spinner.setOnTouchListener() para controlar cuando el usuario ha tocado la pantalla primero.
  • Crearte un custom view, sobre escribir el método onClick y llamar a CustomOnItemClickListener.onItemClicken él.
Ninguna de estas opciones me convencía (encontré más algunas más que tampoco), así que decidí investigar un poco más y descubrí que no se puede evitar que onItemSelected sea llamado cuando se añade el adapter al Spinner si el adapter ya tiene los items, pero si se podía hacer que se llamase con el elemento seleccionado correcto si spinnerView.setSelection(position) es llamado inmediatamente después de añadir los elementos a la vista/adapter. A mí se me ha ocurrido hacerlo de dos formas:

Primera solución

Creamos el adapter con los items y lo añadimos al Spinner cuando ya sabemos el elemento que debe ser seleccionado.

    adapter = new ArrayAdapter<>(this, R.layout.simple_spinner_item, items);
    spinner.setAdapter(adapter);
    spinner.setSelection(position);

Segunda solución

Creamos el adapter sin los items y lo añadimos al Spinner.

    adapter = new ArrayAdapter<>(this, R.layout.simple_spinner_item);
    spinner.setAdapter(adapter);

Y cuando ya sabemos el elemento que debe ser seleccionado.

    spinner.setSelection(10);
    adapter.addAll(items);

Conclusión

Sería perfecto poder poner el AdapterView.OnItemSelectedListener justo después de añadir los items al adapter y que no fuese disparado, pero no se puede, así que dentro de lo malo esto debería ser una solución aceptable.

Código completo solución 1


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

    final Spinner spinner = (Spinner) findViewById(R.id.spinner);
    spinner.setOnItemSelectedListener(this);

    final ArrayAdapter<String> adapter =
        new ArrayAdapter<>(this, R.layout.simple_spinner_item, items);
    spinner.setAdapter(adapter);
    spinner.setSelection(10);
  }

Código completo solución 2



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

    final Spinner spinner = (Spinner) findViewById(R.id.spinner);
    spinner.setOnItemSelectedListener(this);

    final ArrayAdapter<String> adapter =
        new ArrayAdapter<>(this, R.layout.simple_spinner_item);
    spinner.setAdapter(adapter);

    findViewById(R.id.select_item).setOnClickListener(new View.OnClickListener() {
      @Override public void onClick(View v) {
        spinner.setSelection(10);
        adapter.addAll(items);
      }
    });
  }