- How many times have you needed to return a result from one
Activityto 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 anActivitywaiting for its result in a method calledonActivityResult(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
Activityof 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 call
Activity.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 do
Activity.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 clicks
backin 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 withstartActivityForResult, so when the nextActivityis finished our activities will receive a call inonActivityResult, and we do check if we already got a result there from the nextActivity, so if theActivityreceives the result, we set the result for the currentActivityand the previous one will do the same until anyActivityhas itsonActivityResultoverriden and it is waiting for therequestCode.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.
Before I Knew Less
The goal is to share things I learned along the way.
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 una
Activitya 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 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 una
Activityimplicada en el proceso? - Ah, también es muy fácil, cuando lanzas la segunda
Activityimplicada 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 un
Activity.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 a
Activity.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 pulsa
backcuando 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 unaActivityesperando 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 constartActivityForResult, cuando estas terminen laActivityanterior será llamada enonActivityResult, y lo que estamos haciendo es comprobar si laActivityde la que viene ha añadido el resultado ya, si lo ha hecho, pasamos el resultado a laActivityanterior y esto se hará progresivamente hasta que algunaActivityhaya sobreescrito el métodoonActivityResulty esté actualmente esperando por elrequestCode.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)