You are on page 1of 26

CURSO DE DESARROLLO DE APLICACIONES ANDROID

Tema 9

Actividades, Fragmentos y Loaders


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

Actividades

En temas anteriores ya se han implementado actividades que proveen pantallas sobre las que
el usuario puede interactuar, y se han utilizado algunos de sus métodos fundamentales.
Además, se ha mostrado cómo invocar una actividad desde otra por medio de un Intent.

En los próximos temas se van a estudiar estos componentes profundamente, haciendo


hincapié en su ciclo de vida y en los tipos de operaciones que se deben realizar en cada uno de
los diferentes estados del ciclo de vida.

En general, una aplicación incluirá varias actividades, débilmente ligadas entre sí, cada una de
las cuales tendrá asignada una ventana en la cual mostrar su interfaz de usuario. Esta ventana
normalmente ocupará toda la pantalla, aunque también podrá presentarse de forma flotante
sobre otras ventanas. En general, cada aplicación tendrá una actividad principal, cuya ventana
será mostrada cuando se invoque a la aplicación por primera vez.

Cada vez que una actividad inicie otra actividad, la primera será parada, aunque el sistema la
mantendrá en memoria, en una pila de actividades llamada “back stack” y que es similar, por
ejemplo, al historial de páginas visitadas en un navegador web. Cada vez que una nueva
actividad es iniciada, el sistema la añadirá a esta pila como el elemento superior 1 y, cuando el
usuario pulse el botón “atrás”, el sistema destruirá esta actividad y reactivará la nueva
actividad superior de la pila.

Cuando el sistema detiene una actividad porque está iniciando otra, notifica los cambios de
estado de las actividades a través de sus métodos callback. Existen varios métodos callback en
cada actividad que pueden ser invocados cuando la misma cambia de estado, siendo el
principal, y ya conocido, el método onCreate(), invocado cuando la actividad es creada por
primera vez. El sistema invocará a otros métodos concretos cuando la actividad sea parada,
reiniciada o destruida, métodos en los cuales podrá ser necesario realizar operaciones
concretas tales como liberar conexiones o recuperarlas, eliminar de memoria objetos
“pesados”, o grabar datos del usuario en algún contenedor persistente.

1
Esta pila sigue el método de encolamiento LIFO, Last In First Out: la última actividad que entra en la pila será la
primera en salir de la misma.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 2


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

Conceptos básicos sobre actividades

Todas las actividades de las aplicaciones que se desarrollen deberán extender de la clase
Activity o de una subclase de la misma (como, por ejemplo, ListActivity). Además, es
necesario implementar, como mínimo, el método onCreate(), método que será invocado por
el sistema en el momento en el que se instancie por primera vez la actividad. Es importante
tener en cuenta que este método no volverá a ser invocado a menos que el objeto instanciado
(la actividad) sea borrado de la memoria del sistema.

En el método onCreate() se deberán inicializar los componentes esenciales de la actividad


entre los que destaca la propia interfaz de usuario, el layout, que se inicializará por medio del
método setContentView().

La interfaz de usuario, como ya se ha visto anteriormente, se crea a través de una jerarquía de


vistas (objetos que extienden de la clase View). Cada vista define un rectángulo que responde
a la interacción con el usuario. Las vistas se agrupan en dos tipos: layouts y widgets. Los
layouts no tienen representación visual, solamente organizan el espacio disponible en el
rectángulo que definen (extienden de la clase ViewGroup como, por ejemplo, LinearLayout),
mientras que los widgets sí tienen representación visual (como, por ejemplo, una TextView o
un Button). Cualquier vista que provee la API de Android puede ser heredada para crear vistas
personalizadas que definan el layout de cada una de las actividades de una aplicación.

Para mantener el diseño de la interfaz de usuario separado del código que define el
comportamiento de la actividad, es conveniente definir los layouts en archivos XML en la
carpeta de recursos de la aplicación (carpeta “res/”) y después asociar cada layout a su
actividad a través del método setContentView(), que puede recibir como parámetro el
identificador del layout 2. No obstante, también es posible, y a veces necesario, crear vistas de
forma jerárquica directamente en el código de la actividad (en tiempo de ejecución), asociarlas
a un ViewGroup y utilizarlas para crear la interfaz de la actividad pasando dicho ViewGroup al
método setContentView().

Cada una de las actividades de la aplicación deberá estar definida en el archivo de manifiesto
de la misma. Este archivo, también XML, declara, entre otras cosas, las actividades, como
elementos <activity> anidados en el elemento <application>. Entre los atributos del
elemento <activity> destacan propiedades de la actividad tales como su nombre o el estilo
visual que aplicará sobre el layout de la misma.

Tal y como se verá con más profundidad en el tema 11, el elemento <activity> puede definir
varios filtros de intents, <intent-filter>, para declarar cómo puede ser activada por otros
componentes de otras aplicaciones. En general, la actividad principal contendrá un filtro intent
que declara que la actividad responderá a la acción principal (“main”) y que estará ubicada en
la categoría “launcher”:

2
Este identificador equivale al nombre del archivo XML de dicho layout, quedando así definido en la subclase id de
la clase R.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 3


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

<activity android:name=…>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

En la porción de código anterior, el elemento <action> especifica que la actividad es el punto


principal de entrada en la aplicación (aunque en general no será el único), mientas que el
elemento <category> especifica que la actividad deberá mostrarse en la lista de aplicaciones
del dispositivo (en el Launcher). Cualquier actividad que no se quiera poner a disposición de
otras aplicaciones no tendrá ningún filtro de intents y sólo podrá ser invocada a través de un
Intent explícito 3.

Existen sólo dos formas de iniciar una actividad: o bien se invoca a través de un Intent
explícito, o bien a través de un Intent implícito, que describe qué tipo de acción se quiere
realizar. En este caso, el sistema buscará entre todas las actividades de todas las aplicaciones
aquellas que encajen, a través de los filtros de intents que tengan declarados, con lo descrito
en el Intent.

Además de la descripción de la acción a realizar, en los intents también se puede añadir


información que pueda ser usada por la actividad que va a ser iniciada, por medio de objetos
de tipo Bundle.

Para iniciar una actividad , se invocará al método startActivity() pasando como parámetro
el objeto Intent creado. Si se necesita obtener un resultado de la actividad iniciada se
invocará a startActivityForResult(). Para no bloquear la aplicación que invoca a la
segunda actividad, en vez de esperar que el método startActivityForResult() devuelva el
resultado deseado, se implementará el método callback onActivityResult(), que será
invocado cuando la segunda actividad finalice y que recibirá como parámetro un Intent con
la información deseada.

Ciclo de vida de una actividad

Como ya se ha anticipado al principio del tema, la gestión del ciclo de vida de las actividades es
crucial para desarrollar una buena aplicación, debiéndose implementar los métodos callback
que serán invocados cuando la actividad cambie de estado.

Una actividad puede adoptar uno de los siguientes estados:

• Ejecutándose o reanudada. La actividad está en un primer plano y tiene el foco.

3
Un Intent explícito invoca directamente al nombre de la clase de la actividad tal y como se ha mostrado en el
tema 3.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 4


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

• En pausa. La actividad está en un segundo plano, visible pero parcialmente oculta por
otra actividad que ha obtenido el foco. La actividad sigue viva, manteniéndose su
información en memoria, y sigue unida al gestor de ventanas, pero podría ser
finalizada en casos de necesidad extrema de memoria por parte del sistema.
• Parada. La actividad está en background, oculta. La actividad ya no está unida al gestor
de ventanas, aunque sigue manteniéndose en memoria hasta que sea necesaria más
memoria por parte de cualquier otra aplicación.

Una vez pausada o parada la actividad, el sistema podrá eliminarla de la memoria bien
invocando a finish() 4 o bien eliminando directamente su proceso. En cualquiera de los
casos, cuando la actividad se vuelva a iniciar, deberá ser creada de nuevo (invocando a su
método callback onCreate()).

En el siguiente diagrama de bloques se muestra el ciclo de vida completo de una actividad.

4
No se debe invocar explícitamente a los métodos finish() o finishActivity() ya que se pueden producir
comportamientos extraños. Sólo se utilizarán en el caso de que no se desee que el usuario vuelva a la misma
instancia de la actividad.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 5


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

A continuación, se realiza una descripción exhaustiva de los métodos callback del ciclo de vida.

onCreate() Método invocado al crearse la actividad por primera vez.


Inicializa la parte estática de la actividad (layout, conexiones con fuentes de
datos…).
Puede recibir el estado previo de la actividad si se grabó, vía Bundle.
Laactividad no es visible aún.
No se puede eliminar la actividad después de este método.

onRestart() Método invocado después de haberse parado la actividad (oculta).


La actividad no es visible aún.
No se puede eliminar la actividad después de este método.

onStart() Método invocado justo antes de que la actividad se haga visible.


No se puede eliminar la actividad después de este método.
Sí se puede ocultar a continuación (onStop()).

onResume() Método invocado justo antes de que la actividad interactúe con el usuario.
La actividad es la primera en la pila de actividades.
La actividad es visible.
No se puede eliminar la actividad después de este método.
A este método siempre le sigue onPause().

onPause() Método invocado justo antes de que otra actividad se visualice.


La actividad es visible aún.
En este método se deberán liberar recursos y grabar datos rápidamente.
La siguiente actividad no se iniciará hasta que este método finalice.
Sí se puede eliminar la actividad después de este método.

onStop() Método invocado cuando la actividad ya no es visible.


Sí se puede eliminar la actividad después de este método.

onDestroy() Método invocado justo antes de que la actividad sea destruida (eliminada).
Es destruida para liberar memoria o porque se ha invocado finish().
Se pueden distinguir estos escenarios mediante isFinishing().

Es importante recordar que, cuando se implementen todos o alguno de estos métodos, será
necesario invocar a la implementación del correspondiente método de la superclase
(super.onMetodo()) para que el sistema pueda gestionar correctamente el ciclo de vida.

Por otro lado, es necesario tener en cuenta que el sistema sólo puede eliminar el proceso que
aloja la actividad después de que se haya ejecutado uno de los tres métodos onPause(),
onStop() u onDestroy(), siendo onPause() el primer método después del cual el sistema,
en casos de extrema necesidad, puede eliminar la actividad. Es por ello muy importante que
sea en este método en donde se grabe toda la información crítica en algún contenedor
persistente. También es necesario remarcar que, puesto que la actividad nunca será eliminada

CURSO DE DESARROLLO DE APLICACIONES ANDROID 6


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

antes de finalizar la ejecución de este método, las operaciones que se realicen deberán ser las
mínimas posibles, ya que mientras tanto la interfaz de usuario estará “congelada”.

Grabación del estado de una actividad

Ya se ha mencionado que el método onCreate() puede recibir de parámetro un objeto de tipo


Bundle que contendrá el estado previo de la actividad o, dicho de otro modo, la información
que se haya grabado previamente de la actividad en forma de pares nombre-valor.

El estado de una actividad, a lo largo de su ciclo de vida, permanece en memoria, incluso


cuando dicha actividad no es visible (cuando la actividad se pausa o se para, es decir, cuando
se invoca a onPause() o a onStop()). Pero si la actividad es destruida, bien porque haya
finalizado, bien porque el sistema necesite memoria, en general será necesario grabar su
estado transitorio, para poder recuperarlo posteriormente en caso de que el usuario vuelva a
invocar a la actividad (de forma que el usuario vea la pantalla tal y como la dejó).

Para poder grabar estado de la actividad, existe otro método callback,


el
onSaveInstanceState() que el sistema siempre invocará antes de onStop() y,
posiblemente, antes de onPause() 5 . Este método recibe el objeto de tipo Bundle sobre el
cual se podrán grabar pares nombre-valor (a través de los métodos del Bundle putInt(),
putString(), etc…). El sistema almacenará este objeto hasta que se vuelva a invocar a la
actividad, y lo pasará como parámetro a los métodos onCreate() y
onRestoreInstanceState() 6.

Incluso si no se implementa el método antes mencionado, el sistema almacenará


automáticamente el estado de cada objeto View (concretamente de cada widget) de la
actividad, gracias a la implementación por defecto del método onSaveInstanceState()de la
superclase Activity. De esta forma, cualquier cambio visual en la interfaz de la aplicación
será automáticamente recuperado. El único requisito para que este proceso se lleve a cabo
correctamente es que cada uno de los widgets tenga definido el atributo android:id. Si no
tiene ID, el sistema no podrá guardar el estado del widget 7. Por otro lado, tampoco se grabará
el estado del widget si tiene la propiedad android:saveState establecida a false 8.

Si se decide implementar cualquiera de los métodos callback mencionados, será necesario


invocar siempre al correspondiente método callback de la superclase para garantizar el
grabado y recuperación del estado de los objetos View.

5
No existen garantías de que este método sea invocado siempre, ya que existen casos en los que no se necesita
grabar el estado de la actividad como, por ejemplo, cuando el usuario pulsa el botón “atrás”.
6
Si no se almacena ningún par nombre-valor, el objeto Bundle recibido será nulo.
7
Es útil probar a rotar el dispositivo ya que en este caso el sistema destruirá al actividad para renderizarla de nuevo
correctamente.
8
Es conveniente recordar que toda propiedad de una View definida en el archivo XML, tiene su correspondiente
método accesor (setPropiedad()) vía código.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 7


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

Es importante tener claro que a través del método onSaveInstanceState() no se debe


grabar ninguna información persistente del usuario. Estas tareas deberán ser siempre
realizadas en el método onPause().

Cambios de configuración

Cada vez que la pantalla es sometida a un cambio en la configuración (cambio de orientación,


de idioma, aparece o desaparece el teclado), el sistema necesitará renderizar (repintar) la UI.
Para ello invocará a onDestroy() y a continuación a onCreate() de forma que pueda adaptar
la actividad a la nueva configuración de pantalla (cargando otros layouts, por ejemplo).

Ya se han explicado los métodos callback que hay que sobreescribir (sin olvidar invocar al
correspondiente método de la superclase) para grabar correctamente el estado de la
actividad. Si este procedimiento se realiza correctamente, la actividad será mucho más robusta
ante eventuales comportamientos inesperados que sucedan durante su ciclo de vida.

Coordinación de actividades

Cuando una actividad invoca a otra en el mismo hilo, sus ciclos de vida se solapan durante
cierto tiempo. Mientras que la primera actividad se pausará y puede que se pare (si se hace
completamente invisible), la segunda actividad será creada.

La secuencia de transiciones de estados es la siguiente:

CURSO DE DESARROLLO DE APLICACIONES ANDROID 8


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

Fragmentos

Android 3.0 introduce en la API el concepto de fragmento. Un fragmento es, básicamente, una
sección modular de la interfaz de usuario de una actividad que tiene su propio ciclo de vida,
que acepta sus propios eventos y que puede ser reutilizado en distintas actividades. Se pueden
combinar diferentes fragmentos en una actividad para construir interfaces complejas así como
añadirlos o eliminarlos mientras la actividad está activa. Los fragmentos son útiles para crear
interfaces que se adapten correctamente a distintas resoluciones y orientaciones,
maximizando la experiencia del usuario y la reutilización de código.

Un fragmento sólo puede existir dentro de una actividad y el ciclo de vida de esta afectará al
ciclo de vida de aquel. Si la actividad es pausada, también serán pausados todos los
fragmentos que contenga. Sin embargo, mientras la actividad está activa, se pueden crear y
destruir fragmentos contenidos en la misma de forma independiente. Para poder realizar estas
operaciones, Android provee, para cada actividad, una nueva pila análoga a la back stack de las
actividades, que contendrá las operaciones (transacciones) realizadas con los fragmentos de
dicha actividad permitiendo que se deshagan los cambios realizados en los fragmentos al
pulsar el botón “atrás”.

Los fragmentos pueden tener su propio layout 9 que deberá ser insertado dentro de un
ViewGroup de la actividad, bien utilizando el elemento <fragment>, o bien vía código,
instanciando una subclase de Fragment y añadiéndola a un ViewGroup.

La gran ventaja que añade la utilización de fragmentos es que, como ya se ha mencionado, se


puede diseñar aplicaciones que se adapten mucho mejor a la pantalla del dispositivo y que
aprovechen al máximo el espacio disponible. Una aplicación podrá mostrar en una tablet, por
ejemplo, en una misma actividad, más secciones gracias a los fragmentos, mientras que en un
dispositivo con una pantalla menor, dichos fragmentos serán mostrados individualmente en
otras actividades. Por ello, los fragmentos serán diseñados como componentes reutilizables,
modulares, ya que poseen su propio layout y ciclo de vida, y se evitará la dependencia directa
entre fragmentos (los fragmentos se comunicarán con su actividad y viceversa, pero no
directamente entre ellos).

Ciclo de vida de un fragmento

El ciclo de vida de un fragmento es similar al de una actividad, aunque tiene más métodos
callback para gestionar las diferentes etapas de su ciclo de vida.

Si se está rescribiendo una actividad para convertirla en un fragmento, bastará con mover el
código de un método callback de la actividad, al mismo método del fragmento.

9
Pueden declararse fragmentos que no tengan asociado un layout, invisibles, dedicados a realizar tareas concretas
que no requieren interfaz de usuario.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 9


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

En el siguiente diagrama de bloques se muestra el ciclo de vida completo de un fragmento y su


relación con el ciclo de vida de la actividad que lo contiene.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 10


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

A continuación, se realiza una descripción exhaustiva de los métodos callback del ciclo de vida
de un fragmento.

onAttach() Método invocado cuando el fragmento ha sido asociado a la actividad.

onCreate() Método invocado al crearse el fragmento por primera vez.


Se deberán inicializar los componentes esenciales que se quieran
recuperar.
Puede recibir el estado previo si se grabó, vía Bundle.
Fragmento no visible aún.

onCreateView() Crea y devuelve la jerarquía de vistas (UI) del fragmento.


Devolverá null en caso de ser un fragmento invisible.
Puede recibir el estado previo si se grabó, vía Bundle.

onActivityCreated() Método invocado una vez ha finalizado onCreate() de la actividad.


Puede recibir el estado previo si se grabó, vía Bundle.

onStart() Método invocado una vez ha finalizado onStart() de la actividad.


Hace visible el fragmento, en función de su actividad.

onResume() Método invocado una vez ha finalizado onResume() de la actividad.


Hace que el fragmento pueda recibir eventos del usuario.

onPause() Método invocado una vez ha finalizado onPause() de la actividad.


Puede ser invocado si el fragmento es modificado en la actividad (por
ejemplo, al rotar la pantalla).
Se deberán grabar datos que se quieran conservar más allá de la
sesión.

onStop() Método invocado una vez ha finalizado onStop() de la actividad.


Puede ser invocado si el fragmento es modificado en la actividad (por
ejemplo, al rotar la pantalla).

onDestroyView() Método invocado para eliminar la jerarquía de vistas del fragmento.

onDestroy() Método invocado para liberar el resto de recursos del fragmento.

onDetach() Método invocado justo antes de desasociar el fragmento de la


actividad.

Al igual que las actividades, un fragmento adoptará, principalmente, uno de estos tres estados:

• Ejecutándose o reanudado. El fragmento es visible en la actividad que se está


ejecutando.
• En pausa. La actividad que contiene el fragmento está en segundo plano, visible pero
parcialmente oculta por otra actividad que ha obtenido el foco.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 11


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

• Parado. El fragmento no es visible. O bien se ha parado la actividad que lo contiene, o


bien se ha eliminado de la misma, añadiéndolo a su back stack. Si la actividad es
eliminada de memoria, el fragmento también será elimininado.

Se puede también conservar el estado del fragmento usando un objeto Bundle, de nuevo
durante la llamada al método onSaveInstanceState(), y podrá ser recuperado en cualquiera
de los tres métodos iniciales del ciclo de vida, una vez adjuntado el fragmento: onCreate(),
onCreateView() u onActivityCreated().

La mayor diferencia en la gestión del ciclo de vida de un fragmento respecto a la gestión del
ciclo de vida de una actividad es que, mientras que en el de la actividad es el sistema quien
añade la misma a la back stack cuando esta es parada, en el caso de los fragmentos sólo serán
añadidos a su propia pila en caso de que se invoque al método addToBackStack() durante
una transacción. Conceptualmente, esta pila contiene estados de fragmentos, o lo que es lo
mismo, fragmentos en un estado concreto.

Por último, hay que recordar que el ciclo de vida del fragmento está determinado por el ciclo
de vida de la actividad que lo contiene, aunque cuando la actividad esté ejecutándose
(resumed), el ciclo de vida del fragmento podrá evolucionar libremente (pudiéndose eliminar o
añadir distintos fragmentos a través de transacciones).

Creación de un fragmento. Transacciones

Para recibir las llamadas a los métodos callback anteriores, el fragmento deberá ser una
subclase de Fragment o bien de alguna de sus subclases ya implementadas en la SDK de
Android como, por ejemplo:

• DialogFragment, similar AlertDialog pero con la ventaja de que podrá conservar su


estado en la back stack una vez descartado.
• ListFragment, similar a ListActivity, mostrará una lista de ítems gestionados por
un adaptador.
• PreferenceFragment, similar a PreferenceActivity y útil para crear una lista de
settings de la aplicación en forma de árbol de objetos tipo Preference.

Para asociar un layout a un fragmento, se deberá implementar el método onCreateView()


que será invocado por el sistema cuando sea el momento de renderizar el layout del
fragmento. Este método devolverá el objeto View inflado, elemento raíz del layout del
fragmento. Como parámetros, además de un objeto LayoutInflater y un Bundle con el
estado del fragmento anteriormente grabado, recibe también el ViewGroup contenedor padre
del fragmento, que deberá ser utilizado al inflar el XML del layout del fragmento para asociar
las propiedades del elemento raíz del fragmento.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 12


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {

return inflater.inflate(R.layout.reproductor_musica_fragment,
container, false);
}

A continuación, será necesario añadir el fragmento al layout de la actividad, en el caso de que


este sea parte de la interfaz de usuario. Existen dos formas, como ya se ha mencionado. O bien
se declara el fragmento dentro del archivo XML de layout de la actividad, o bien se añade vía
código a un ViewGroup de la actividad que ya exista.

El siguiente código muestra la primera opción, que declara directamente un fragmento en el


layout main.xml.

Código de main.xml

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/linearLayout1"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal" >

<FrameLayout
android:id="@+id/listaMusicaFrameLayout"
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="0.4" >

<fragment
android:id="@+id/listaMusicaFragment"
android:name="com.cursoandroid.ui.ListaMusicaFragment"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
</fragment>
</FrameLayout>

<LinearLayout
android:id="@+id/detalleMusicaLayout"
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="0.6"
android:orientation="vertical" >
...
</LinearLayout>
</LinearLayout>

CURSO DE DESARROLLO DE APLICACIONES ANDROID 13


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

En el código anterior, se observa que el elemento <fragment> hace referencia, a través del
atributo android:name, a la clase que instanciará el layout del fragmento 10. Dicha clase, al
implementar el método onCreateView() proveerá el objeto View que el sistema insertará en
el lugar indicado por el elemento <fragment>. Será necesario al menos proveer el atributo
android:id (con identificador único) para que el sistema sea capaz de recuperar el fragmento
si la actividad es reiniciada y para poder capturar el fragmento en código mediante
findFragmentById() 11.

Para añadir un fragmento a una actividad en tiempo de ejecución, se deberá instanciar el


gestor de fragmentos y realizar una transacción que asocie la instancia del fragmento al
elemento raíz (ViewGroup) del layout de la actividad donde será insertado. El fragmento
anterior, que se ha añadido directamente en el layout, podría haberse añadido en tiempo de
ejecución con la siguiente transacción 12:

FragmentManager fragmentManager = getFragmentManager();


FragmentTransaction fragmentTransaction =
fragmentManager.beginTransaction();

ListaMusicaFragment lista = new ListaMusicaFragment();


fragmentTransaction.add(R.id.listaMusicaFrameLayout, lista);

fragmentTransaction.commit();

El gestor de fragmentos también está encargado de gestionar la pila back stack de los mismos,
y provee un método para registrar un listener que avise de cambios en la pila,
addBackStackChangedListener(). Para extraer estados de fragmentos de la pila, el gestor
utilizará el método popBackStack() que podrá recibir un identificador o nombre como
parámetro, entre otros, casos en los cuales extraerá de la pila todos estados de fragmentos
hasta el indicado.

Para almacenar fragmentos en un estado determinado en su pila, se ha de instanciar una


FragmentTransaction a través de FragmentManager. Conceptualmente, una transacción
equivale a realizar una serie de operaciones sobre un elemento, en este caso, una actividad.
Las operaciones consistirán en añadir, eliminar, sustituir o modificar fragmentos de dicha
actividad. Se podrán guardar estas transacciones en la pila de fragmentos, permitiéndose así
deshacer los cambios realizados sobre los mismos (lo cual simulará la navegación hacia atrás).

10
Al hacer referencia a la clase, se deberá incluir su espacio de nombres.
11
También se podrá indicar el atributo android:tag, más usado para localizar fragmentos sin representación visual
mediante findFragmentByTag().
12
Si se declara un fragmento en el archivo XML de layout de la actividad y, simultáneamente, se añade el mismo
fragmento en tiempo de ejecución en el mismo ViewGroup, se duplicará el fragmento, superponiéndose uno a otro.
Solo uno de los fragmentos capturará eventos y, por ejemplo, en caso de tratarse de un fragmento de tipo lista, al
hacer scroll, solo se moverán los ítems de una de las listas. Este tipo de comportamientos deberán evitarse al
realizar el correcto diseño de los diferentes layouts de una aplicación en función de la resolución de los diferentes
dispositivos.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 14


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

Para realizar las transacciones, el objeto FragmentTransaction provee los típicos métodos
add(), remove() y replace(). Para que las operaciones de la transacción tengan efecto, se
deberá invocar a commit(). Previamente se podrá haber invocado a addToBackStack() para
poder recuperar el estado anterior del fragmento posteriormente si se pulsa el botón “atrás”.

En el código de la página anterior, se realiza una transacción para añadir el fragmento de tipo
ListaMusicaFragment al contenedor (ViewGroup) cuyo identificador es
R.id.listaMusicaFragment. Esta operación no se podrá deshacer puesto que la transacción
no se ha añadido a la back stack. Si se quiere sustituir dicho fragmento por otro, y tener la
posibilidad de recuperar el nuevo fragmento posteriormente, se deberá implementar un
código similar al siguiente:

FragmentManager fragmentManager = getFragmentManager();


FragmentTransaction fragmentTransaction =
fragmentManager.beginTransaction();

ListaVideoFragment listaVideo =
new ListaVideoFragment();

fragmentTransaction.replace(R.id.listaMusicaFrameLayout, listaVideo);

fragmentTransaction.addToBackStack();

fragmentTransaction.setTransition(
FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
fragmentTransaction.commit();

Si no se hubiera invocado a addToBackStack(), el fragmento anterior, lista (de música),


habría sido eliminado definitivamente. Al haber invocado a dicho método, el fragmento lista
ha sido añadido a la cima de la pila, invocándose a su método onStop(). Además, al invocar al
método setTransition(), la sustitución de una lista por otra se realizará con una animación
(que podrá ser personalizada).

Las transacciones no se efectúan inmediatamente al invocar a commit() si no que se encolan


en el hilo de la interfaz de usuario de la actividad hasta que este esté disponible para realizar
los cambios solicitados sobre los fragmentos. Si se necesita realizar inmediatamente una
transacción porque otras tareas de otros hilos dependan de dicha transacción, se podrá
invocar al método executePendingTransactions().

Finalemente, es necesario tener en cuenta que las transacciones se deben realizar antes de
que la actividad cambie de estado, es decir, antes de que el usuario, por ejemplo, abandone la
misma, debido a que se puede perder el estado final de los fragmentos en caso de que la
actividad sea restaurada. Si esta situación no produce ningún problema en la aplicación, se
podrá utilizar el método commitAllowingStateLoss().

CURSO DE DESARROLLO DE APLICACIONES ANDROID 15


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

Comunicación entre fragmentos y la actividad que los instancia

Los fragmentos pueden acceder a recursos de la actividad que los instancia con tan solo
invocar al método getActivity(). Por ejemplo, dentro de un fragmento se puede acceder a
una View de la actividad:

TextView tw =
(TextView) getActivity().findViewById(R.id.tituloDetalleMusica);

Por otro lado, las actividades pueden acceder también a recursos de los fragmentos que
instancian, a través de FragmentManager. Por ejemplo:

ReproductorMusicaFragment reproductor = (ReproductorMusicaFragment)


getFragmentManager().findFragmentById(R.id.reproductorMusicaFragment);

En algunos casos será necesario que la actividad que implementa un fragmento reaccione ante
eventos que suceden dentro del mismo y, por ejemplo, reciba información de dichos eventos
para pasársela a otros fragmentos. Para poder implementar este comportamiento será
necesario definir una interfaz con un método callback dentro del fragmento, y obligar a que la
actividad lo implemente, comprobando que la actividad extiende dicha interfaz y lanzando una
excepción ClassCastException en caso de que no sea así 13.

El primer método invocado en el ciclo de vida del fragmento, onAttach(), deberá ser el
método donde se compruebe si la actividad que instancia el fragmento ha implementado la
interfaz que contiene el método callback.

A continuación, se puede ver un ejemplo que implementa este comportamiento. El fragmento


ListaMusicaFragment forzará a que la actividad que lo implementa,
BibliotecaMusicalActivity, implemente la interfaz OnCancionSelectedListener. Cuando
se seleccione una canción en la lista que muestra el fragmento, se invocará al método callback

13
Si una clase implementa un interfaz, deberá poderse realizar un cast de dicha clase al tipo de dicha interfaz.
Supóngase que una clase implementa una interfaz:

class A implements B.

A una implementación de dicha clase

A objetoA = new A(),

se le podrá hacer un cast al tipo de la interfaz:

B interfaz = (B) objetoA.

Si A no implementa B, el cast anterior lanzará ClassCastException.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 16


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

de dicha interfaz, de forma que llegue a la actividad el identificador de la canción seleccionada.


La actividad pasará entonces dicho identificador a un método de un segundo fragmento, el
reproductor, que reproduciría la canción seleccionada.

Extracto de código de ListaMusicaFragment.java

public class ListaMusicaFragment extends ListFragment {

OnCancionSelectedListener mCancionSelectedListener;

// La actividad que instancie este fragmento deberá implementar esta


// interfaz, lo cual se comprueba en onAttach()
public interface OnCancionSelectedListener {
public void onCancionSelected(long idCancion);
}

@Override
public void onAttach(Activity activity) {

// Este método callback es invocado cuando el fragmento es


// añadido a la actividad (la cual es recibida como parámetro del
// método)
super.onAttach(activity);

try {
mCancionSelectedListener = (OnCancionSelectedListener) activity;
}
catch (ClassCastException e) {
throw new ClassCastException("La " + activity.toString() +
" debe implementar OnCancionSelectedListener");
}
}

@Override
public void onListItemClick(ListView listaCanciones, View v, int
position, long id) {
// Se envía el evento y el identificado de la canción
// seleccionada a la actividad que contiene el fragmento
mCancionSelectedListener.onCancionSelected(
((TextView) v).getText().toString());
}

[El código que carga los elementos de la lista se mostrará al final


del tema]

CURSO DE DESARROLLO DE APLICACIONES ANDROID 17


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

Código de BibliotecaMusicalActivity.java

public class BibliotecaMusicalActivity extends Activity


implements OnCancionSelectedListener {

private static final String REPRODUCTOR_FRAGMENT_TAG =


"REPRODUCTOR_FRAGMENT";

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

// Alternativa para añadir un Fragmento, a través de una


// transacción. O bien se añade el fragmento en el layout XML, o
// bien se añade con una transacción.
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction =
fragmentManager.beginTransaction();

ListaMusicaFragment lista = new ListaMusicaFragment();


fragmentTransaction.add(R.id.listaMusicaFrameLayout, lista);

ReproductorMusicaFragment reproductor =
new ReproductorMusicaFragment();
fragmentTransaction.add(R.id.detalleMusicaFrameLayout, reproductor,
REPRODUCTOR_FRAGMENT_TAG);
fragmentTransaction.setTransition(
FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
fragmentTransaction.commit();
}

public void onCancionSelected(String tituloCancion) {


// Este método es el método callback del fragmento
// ListaMusicaFragment. Será invocado cuando se seleccione una
// canción en dicho fragmento y recibirá el título de la misma
ReproductorMusicaFragment reproductor = (ReproductorMusicaFragment)
getFragmentManager().findFragmentByTag(REPRODUCTOR_FRAGMENT_TAG);
reproductor.reproduce(tituloCancion);
}
}

CURSO DE DESARROLLO DE APLICACIONES ANDROID 18


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

Código de main.xml

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/linearLayout1"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:baselineAligned="false"
android:orientation="horizontal" >

<FrameLayout
android:id="@+id/listaMusicaFrameLayout"
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="0.4" >
</FrameLayout>

<LinearLayout
android:id="@+id/detalleMusicaLayout"
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="0.6"
android:orientation="vertical" >

<TextView
android:id="@+id/tituloDetalleMusica"
android:layout_width="fill_parent"
android:layout_height="100dp"
android:gravity="center_vertical"
android:text="@string/tituloDetalle"
android:textColor="#ffffff"
android:textSize="16sp"
android:textStyle="bold" />

<FrameLayout
android:id="@+id/detalleMusicaFrameLayout"
android:layout_width="match_parent"
android:layout_height="fill_parent" >
</FrameLayout>
</LinearLayout>

</LinearLayout>

CURSO DE DESARROLLO DE APLICACIONES ANDROID 19


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

Código de ReproductorMusicaFragment.java

public class ReproductorMusicaFragment extends Fragment {

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {

return inflater.inflate(R.layout.reproductor_musica_fragment,
container, false);
}

public void reproduce(String tituloCancion) {

TextView tw = (TextView)
getActivity().findViewById(R.id.tituloDetalleMusica);
tw.setText(getString(R.string.reproducir) + " " + tituloCancion);
}
}

Código de reproductor_musica_fragment.xml

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/reproductorMusicaLayout"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >

<TextView
android:id="@+id/tituloReproductorMusica"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/tituloReproductor"
android:textColor="#ffffff"
android:textSize="16dp"
android:textStyle="bold" />

</LinearLayout>

CURSO DE DESARROLLO DE APLICACIONES ANDROID 20


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

Fragmentos y ActionBar

Los fragmentos pueden contribuir con sus propios ítems al menú de opciones de la actividad y,
por lo tanto, a la ActionBar. Para que esto suceda, en su método onCreate() se deberá
invocar a setHasOptionsMenu() de forma que se indique al sistema que el fragmento
agregará opciones al menú. Entonces, el sistema invocará al método onCreateOptionsMenu()
implementado en el fragmento para añadir los ítems que ahí se definan, y el fragmento
recibirá callbacks a su método onOptionsItemSelected() cuando se seleccione alguna
opción del menú, siempre y cuando la actividad no haya gestionado el ítem seleccionado
(devolviendo true) en su propio método onOptionsItemSelected(), puesto que este será
invocado antes.

Igualmente, se podrá implementar un menú contextual en el fragmento, definido en un layout


XML, invocando a registerForContextMenu(). Al solicitar el menú contextual, el sistema
invocará al método onCreateContextMenu() del fragmento, y al seleccionar una de sus
opciones, el sistema invocará a onContextItemSelected().

CURSO DE DESARROLLO DE APLICACIONES ANDROID 21


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

Loaders

Tanto en las actividades como en los fragmentos, muchas veces será necesario cargar datos
externos de diversas fuentes. Como la carga de estos datos puede requerir cierto tiempo, no
será aconsejable, por ejemplo, utilizar el método onCreate() u otros métodos del ciclo de vida
para realizar la carga de datos, puesto que la interfaz del usuario de la aplicación podría llegar
a “congelarse” durante el tiempo de carga de dichos datos externos. Además, la actualización
de estos datos externos en la aplicación propia (por ejemplo, una lista de noticias de última
hora) ha de ser también gestionada para visualizar siempre la última versión de las fuentes de
los datos.

Android 3.0 provee un conjunto de clases que facilitan todo este proceso, admitiendo además
la carga asíncrona de datos, característica que permitirá un rendimiento óptimo de la
aplicación. Además de monitorizar la fuente de datos y comunicar la existencia de nuevos
datos cuando el contenido cambia, estas clases permiten la reconexión automática al último
cursor del loader cuando la actividad o el fragmento son recreados, de forma que no es
necesario volver a solicitar los datos a la fuente.

Existen cuatro clases básicas más una interfaz, con los cuales se podrá realizar todo el trabajo
de carga asíncrona y refresco de datos. También se podrán extender los loaders para crear
otros propios con características únicas.

• LoaderManager. Clase principal encargada de gestionar los loaders instanciados en la


actividad o fragmento y que ayudará a integrar operaciones de larga duración en el
ciclo de vida de la actividad o fragmento.
• Loader. Clase abstracta, padre de todos los loaders, que permitirá la carga asíncrona
de datos. Se podrá extender aunque en general se utilizará CursorLoader.
• AsyncTaskLoader. Loader que provee una tarea asíncrona, AsynkTask para realizar
el trabajo de carga.
• CursorLoader. Subclase de AsyncTaskLoader que consulta al ContentResolver 14
los datos obtenidos de un proveedor de contenido, ContentProvider, y devuelve un
Cursor. Utiliza un hilo secundario para la carga de datos, por lo que no bloquea la
interfaz de usuario.
• LoaderManager.LoaderCallbacks. Interfaz que permite la interacción con el
LoaderManager a través de los métodos callback onCreateLoader(),
onLoadFinished() y onLoaderReset(). En general será implementado por la
actividad o fragmento que instancia el LoaderManager. En los métodos
implementados se podrán crear nuevos loaders y gestionar los ya existentes.

Para utilizar loaders en una actividad o fragmento, se deberá crear una instancia única de
LoaderManager, que gestionará todos los loaders de la actividad o fragmento. En general, se

14
Esta clase da acceso a las aplicaciones al modelo de contenidos de Android, ofreciendo los datos encapsulados
por el ContentProvider. Se tratará más adelante en un próximo tema.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 22


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

instanciará un CursorLoader 15 el cual consultará al ContentResolver (interfaz única de la


fuente de datos ContentProvider) y devolverá un Cursor, siendo gestionado el proceso
gracias a los métodos callback del interfaz LoaderCallbacks. Para recorrer el cursor
obtenido y extraer los datos, se utilizará un adaptador como SimpleCursorAdapter.

La creación de los loaders se llevará a cabo en el método onCreate(), invocando al método


initLoader() del LoaderManager.

getLoaderManager().initLoader(
ID_LOADER, loaderArguments, instanciaLoaderCallbacks);

Este método, que inicializa y activa el loader, recibe tres argumentos que, respectivamente,
identifican de forma unívoca al loader creado, pasan argumentos adicionales al loader y, por
último, asocian la interfaz de tipo LoaderCallbacks para comunicar los eventos que ocurren
en el loader. Esta interfaz suele ser la propia clase (actividad o fragmento), que implementa
LoaderCallbacks (por lo que se puede pasar this como tercer argumento) 16.

Si el loader referenciado con ID_LOADER existe, la llamada a initLoader() reutilizará el


mismo. En cambio, si aún no existe, se invocará al método callback
LoaderCallbacks.onCreateLoader(), método que deberá ser implementado con el código
necesario para inicializar el loader.

No es necesario obtener una referencia del loader instanciado ya que LoaderManager se


encarga de gestionar el ciclo de vida del mismo, iniciando y parando la carga de datos del
loader. Sólo se interaccionará con el loader a través de los tres métodos callback de su interfaz
para interactuar con el proceso de carga de datos durante los eventos que suceden en el ciclo
de vida del loader.

Interfaz LoaderCallbacks

Como ya se ha mencionado, esta interfaz será en general implementada por la propia actividad
o fragmento y se utilizarán sus métodos para interactuar indirectamente con los loaders
instanciados, a través de LoaderManager.

Lo más común es que se utilice un CursorLoader para cargar datos desde el


ContentProvider y para mantenerlos una vez haya sido parado el loader de forma que la
actividad o fragmento pueda recuperar los datos consultados sin volverlos a cargar desde el
proveedor de contenido, después de pasar por onStop() y onStart().

15
Se podrían implementar subclases de Loader o AsyncTaskLoader para cargar datos de otras fuentes distintas a
los ContentProvider.
16
Si se necesita reiniciar el loader, se podrá invocar a getLoaderManager().restartLoader(), método que recibe
los mismos argumentos que initLoader().

CURSO DE DESARROLLO DE APLICACIONES ANDROID 23


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

En el método onCreateLoader() se construirá el loader que accederá al proveedor de


contenido. Si se instancia un CursorLoader, se utilizarán los siguientes argumentos en su
constructor, los cuales serán utilizados para acceder al ContentProvider:

• Actividad o fragmento propietario del loader, o contexto de la aplicación.


• URI (Uniform Resource Identifier), cadena de caracteres corta que identifica el recurso
que se quiere consultar (por ejemplo, "content://media/external/audio/media”).
• Proyección, o lista de columnas que devolverá el proveedor de contenido.
• Selección, o filtro equivalente a la cláusula WHERE en SQL.
• Argumentos de la selección, valores de tipo String que serán utilizados por el filtro.
• Ordenación, de los resultados obtenidos.

Por ejemplo:

public Loader<Cursor> onCreateLoader(int id, Bundle args) {

// Este método creará el cursor que utilizará el LoaderManager


// para cargar los datos del ContentProvider.
// Se creará un CursorLoader para extraer la lista de sonidos del
// dispositivo (de su almacén "interno". También existe el almacén
// externo - sdcard)
Uri uri = MediaStore.Audio.Media.INTERNAL_CONTENT_URI;
String[] proyeccion = { MediaStore.MediaColumns.TITLE,
MediaStore.MediaColumns.DISPLAY_NAME };
String select = "";
String ordenacion = MediaStore.MediaColumns.TITLE + " DESC";

return new CursorLoader(getActivity(), uri, proyeccion, select,


null, ordenacion);
}

Cuando el loader termine la carga de los datos, LoaderManager invocará al método


onLoadFinished(). En general, se pasarán los datos al adaptador (previamente definido en
onCreate() u onActivityCreated()), que se encargará de mostrarlos. Será LoaderManager
quien gestione internamente el borrado de los posibles antiguos datos que existieran en el
Cursor 17, por lo que bastará un código similar al siguiente:

public void onLoadFinished(Loader<Cursor> loader, Cursor datos) {

mAdaptador.swapCursor(datos);
}

17
Cursor es una interfaz que da acceso de lectura y escritura al conjunto de resultados obtenidos en una consulta
a base de datos.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 24


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

Por último, cuando el loader sea cerrado, los datos anteriores dejarán de estar disponibles, por
lo que se deberá eliminar toda referencia a los mismos, para liberar memoria. Para ello, se
implementará el método onLoaderReset() de la siguiente forma:

public void onLoaderReset(Loader<Cursor> loader) {

mAdaptador.swapCursor(null);
}

Explicación de clases mencionadas

LoaderManager

Gestor de los loaders de una actividad o fragmento.

LoaderCallbacks

Interfaz cuyos tres métodos deberán ser implementados, puesto que son invocados por el
LoaderManager en las diferentes etapas del ciclo de vida de cada loader.

CursorLoader

Loader que pregunta al ContentResolver y devuelve un Cursor. Esta clase implementa el


típico protocolo loader en la forma estándar para consultar cursores. Para no bloquear la
interfaz de usuario, es mejor utilizar AsyncTaskLoader que realizará la misma tarea pero en
un hilo en background.

El CursorLoader debe ser instanciado con toda la información necesaria para realizar la
consulta, bien a través del constructor o bien a través de los métodos accesores tipo “set”. Se
deberá informar el contexto de la aplicación, la Uri, la selección, los argumentos de la
selección, el orden y la proyección (columnas que se mostrarán).

Cursor

Interfaz que provee acceso aleatorio (o no ordenado a priori) de lectura y escritura a los datos
obtenidos en una consulta a base de datos.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 25


TEMA 9. ACTIVIDADES, FRAGMENTOS Y LOADERS

ContentResolver

Clase abstracta que permite el acceso, por parte de las aplicaciones, al modelo de contenido
de Android.

MediaStore

Clase que contiene todos los metadatos de todo el contenido multimedia almacenado tanto en
la memoria interna del dispositivo como en la externa.

ContentProvider

Los proveedores de contenido son uno de los pilares fundamentales de las aplicaciones
Android, ya que proveen de contenido a las mismas. Encapsulan los datos que les son
solicitados y los hacen disponibles a través del interfaz ContentResolver. Solo será necesario
implementar un proveedor de contenido propio si se necesita compartir información entre
varias aplicaciones. En cambio, para almacenar datos que no se quieran compartir con otras
aplicaciones, bastará con usar directamente una base de datos por medio de
SQLiteDatabase.

Cuando se realiza una consulta a través del ContentResolver el sistema examina la autoridad
contenida en la Uri (por ejemplo, content://media) y le pasa la consulta al proveedor de
contenido que esté registrado para dicha autoridad. El resto de la Uri se podrá interpretar de
múltiples formas.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 26

You might also like