- 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 anActivity
waiting for its result in a method calledonActivityResult(int, int, Intent)
, no mistery there. - Well, and what happens when there is more than one
Activity
involved in the process? - ah, it is very easy too, when the second
Activity
of the flow is launched you have to add a the flagIntent.FLAG_ACTIVITY_FORWARD_RESULT
to theIntent
and then it will be your nextActivity
who will be in charge to callsetResult
to deliver the result. - is that all?
- Well, it is not, you need to call
Activity.finish
in the firstActivity
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 secondActivity
the flow will be finished as well. - and, what happens if the user clicks
back
in the lastActivity
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 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
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 withstartActivityForResult
, while this time therequestCode
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 withstartActivityForResult
, so when the nextActivity
is finished our activities will receive a call inonActivityResult
, and we do check if we already got a result there from the nextActivity
, so if theActivity
receives the result, we set the result for the currentActivity
and the previous one will do the same until anyActivity
has itsonActivityResult
overriden 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
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 unaActivity
de 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
Activity
implicada en el proceso? - Ah, también es muy fácil, cuando lanzas la segunda
Activity
implicada en el flow lo haces añadiendo el flagIntent.FLAG_ACTIVITY_FORWARD_RESULT
alIntent
y entonces será tu siguienteActivity
la encargada de llamar asetResult
para entregar el resultado. - ¿Eso es todo?
- Bueno, en realidad no, en realidad tienes que hacer un
Activity.finish
de la primeraActivity
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 segundaActivity
, 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 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
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 unaActivity
esperando 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 elrequestCode
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 constartActivityForResult
, cuando estas terminen laActivity
anterior será llamada enonActivityResult
, y lo que estamos haciendo es comprobar si laActivity
de la que viene ha añadido el resultado ya, si lo ha hecho, pasamos el resultado a laActivity
anterior y esto se hará progresivamente hasta que algunaActivity
haya sobreescrito el métodoonActivityResult
y 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
Spinner
withOnItemSelectedListener
that 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
onClick
and callCustomOnItemClickListener.onItemClick
within 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.onItemSelected
diciendo 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 aCustomOnItemClickListener.onItemClick
en é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:
Entradas (Atom)