- How many times have you needed to return a result from oneActivityto 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 methodstartActivityForResult(Intent, int)that allows us to launch anActivitywaiting for its result in a method calledonActivityResult(int, int, Intent), no mistery there.
- Well, and what happens when there is more than oneActivityinvolved in the process?
- ah, it is very easy too, when the secondActivityof the flow is launched you have to add a the flagIntent.FLAG_ACTIVITY_FORWARD_RESULTto theIntentand then it will be your nextActivitywho will be in charge to callsetResultto deliver the result.
- is that all?
- Well, it is not, you need to callActivity.finishin the firstActivityof 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 doActivity.finishas I said before, in that way when the user finishes the secondActivitythe flow will be finished as well.
- and, what happens if the user clicksbackin the lastActivityof 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 Activityof the flow likestartActivityForResult:
 - 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 Activityin 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 Activityof 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 Activityinvolved in the flow withstartActivityForResult, while this time therequestCodeis 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 BaseActivityand 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- Activityis 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- Activityreceives the result, we set the result for the current- Activityand the previous one will do the same until any- Activityhas its- onActivityResultoverriden 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.
 
12 de abril de 2016
Android - Flow result instead of Activity result or how to get a result between activities
Etiquetas:
Activity,
Activity Result,
Android,
Development,
FLAG_ACTIVITY_FORWARD_RESULT,
Flow,
Flow Result,
onActivityResult,
Result,
startActivityForResult
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 unaActivitya 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étodostartActivityForResult(Intent, int)para lanzar unaActivityde la que esperas un resultado y otro métodoonActivityResult(int, int, Intent)para recibirlo, no tiene ningún misterio.
- Bien ¿y que pasa si hay más de unaActivityimplicada en el proceso?
- Ah, también es muy fácil, cuando lanzas la segundaActivityimplicada en el flow lo haces añadiendo el flagIntent.FLAG_ACTIVITY_FORWARD_RESULTalIntenty entonces será tu siguienteActivityla encargada de llamar asetResultpara entregar el resultado.
- ¿Eso es todo?
- Bueno, en realidad no, en realidad tienes que hacer unActivity.finishde la primeraActivitydel 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 aActivity.finishcomo he dicho antes, de esta forma cuando el usuario termina en la segundaActivity, va directamente a la que está esperando el resultado.
- ¿Y que pasa si el usuario pulsabackcuando 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 Activitydel flow constartActivityForResult:
 - 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 Activitydel 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 startActivityForResulttambién, aunque en este caso elrequestCodeno 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 BaseActivityde 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- Activityanterior será llamada en- onActivityResult, y lo que estamos haciendo es comprobar si la- Activityde la que viene ha añadido el resultado ya, si lo ha hecho, pasamos el resultado a la- Activityanterior y esto se hará progresivamente hasta que alguna- Activityhaya sobreescrito el método- onActivityResulty 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.
 
Etiquetas:
Activity,
Activity Result,
Android,
Development,
FLAG_ACTIVITY_FORWARD_RESULT,
Flow,
Flow Result,
onActivityResult,
Result,
startActivityForResult
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 SpinnerwithOnItemSelectedListenerthat are in the screen to skip the first call toonItemSelected.
- 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 onClickand callCustomOnItemClickListener.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 onClicky llamar aCustomOnItemClickListener.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);
      }
    });
  }
Suscribirse a:
Comentarios (Atom)
