Los Bottom Sheets en Android Studio | Jetpack Compose

Video thumbnail

Un Bottom Sheets no es más que una sección o panel deslizante, por lo tanto ahora contamos con una nuevo panel deslizante para agregar elementos o ampliar fácilmente funcionalidades de nuestra aplicación.

Recordemos que venimos en que ya sabemos como emplear los Logs en Android Studio que es ideal para hacer pruebas con este Composable que es más complejo en sus eventos e implementación.

Vamos a conocer otro Composable fundamental: el Modal Bottom Sheet. Esta es la famosa ventana que aparece desde la parte inferior de la pantalla. A continuación, te explico cómo funciona el código y la lógica detrás de este componente.

El componente ModalBottomSheet

Con la llegada de Jetpack Compose, la creación de interfaces de usuario en Android ha cambiado significativamente. A continuación se muestra cómo crear un Bottom Sheet utilizando Jetpack Compose, que es la forma moderna y recomendada.

Para implementar un "modal bottom sheet" en Jetpack Compose, se utiliza principalmente el composable ModalBottomSheet, que forma parte de Material Design 3. Este componente permite que el contenido se deslice hacia arriba desde la parte inferior de la pantalla, a menudo requiriendo la interacción del usuario antes de volver a la actividad principal.

Vamos a dividir por trozos este Composable es es de los más complejos que hemos visto hasta ahora.

Variables de Estado y State del Bottom Sheets

Para que el Bottom Sheet funcione, necesitamos gestionar tres elementos clave:

  • MutableStateOf (Variable reactiva): Al igual que con los diálogos, usamos un estado booleano para decidir si el Composable se debe renderizar en la interfaz.
  • SheetState/rememberModalBottomSheetState (El controlador): Es el objeto que maneja el estado interno del Bottom Sheet (si está expandido, oculto o a mitad de pantalla).
  • CoroutineScope: Dado que el Bottom Sheet incluye animaciones implícitas, necesitamos una corrutina para ejecutar funciones suspendidas (como ocultar el panel) sin bloquear el hilo principal.
// 1. Estado para controlar la visibilidad
var showSheet by remember { mutableStateOf(false) }

// 2. Estado interno del BottomSheet (para animaciones y gestos)
val sheetState = rememberModalBottomSheetState()
val scope = rememberCoroutineScope()

Estructura del Composable

En el ejemplo, utilizamos una Column para organizar el contenido. El botón principal simplemente cambia nuestra variable reactiva a true para disparar el renderizado.

val sheetState = rememberModalBottomSheetState()
val scope = rememberCoroutineScope()
var showSheet by remember { mutableStateOf(false) }
// Botón para abrir el modal
Button(onClick = { showSheet = true }) {
   Text("Abrir Modal")
}

Cuando showSheet es verdadero, se muestra el componente. Aquí definimos propiedades como el color del contenedor o la forma de los bordes (en este caso, redondeados a 16.dp).

Un parámetro crucial es onDismissRequest. Esta es una función de callback que se ejecuta automáticamente cuando el usuario toca fuera del modal. Por convención, aquí debemos establecer nuestro estado en false para que el componente deje de renderizarse.

Quedando la implementación como:

package com.example.myproyectandroid

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet

import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState

import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.myproyectandroid.ui.theme.MyProyectAndroidTheme
import kotlinx.coroutines.launch
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MyProyectAndroidTheme {
                // El Scaffold es el "marco" de la pantalla
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->

                    // USAMOS UNA COLUMN para que los elementos no se encimen
                    Column(
                        modifier = Modifier
                            .padding(innerPadding) // Aplicamos el padding del Scaffold
                            .fillMaxSize(),
                        verticalArrangement = Arrangement.spacedBy(16.dp), // Espacio entre elementos
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {
                        Greeting(name = "Android")
                        CardBonito()
                    }
                }
            }
        }

    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EjemploBottomSheet() {
    // 1. Estado para controlar la visibilidad
    var showSheet by remember { mutableStateOf(false) }

    // 2. Estado interno del BottomSheet (para animaciones y gestos)
    val sheetState = rememberModalBottomSheetState()
    val scope = rememberCoroutineScope()

    Column(
        modifier = Modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Button(onClick = { showSheet = true }) {
            Text("Abrir Modal")
        }
    }

    // 3. El componente Modal
    if (showSheet) {
        ModalBottomSheet(
            onDismissRequest = { showSheet = false },
            sheetState = sheetState,
            // Opcional: Personalizar el color o la forma
            containerColor = Color.White,
            shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
        ) {
            // Contenido del Bottom Sheet
            SheetContent(
                onClose = {
                    // Para cerrar con animación, usamos la corrutina
                    scope.launch { sheetState.hide() }.invokeOnCompletion {
                        if (!sheetState.isVisible) {
                            showSheet = false
                        }
                    }
                }
            )
        }
    }
}

@Composable
fun SheetContent(onClose: () -> Unit) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(24.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("¡Soy un Modal Bottom Sheet!", style = MaterialTheme.typography.headlineSmall)
        Spacer(modifier = Modifier.height(16.dp))
        Text("Puedo contener cualquier composable aquí dentro.")
        Spacer(modifier = Modifier.height(24.dp))
        Button(onClick = onClose) {
            Text("Cerrar")
        }
    }
}

@Preview(showBackground = true)
@Composable
fun Preview() {
    MyProyectAndroidTheme {
        PantallaPrincipal()
    }
}

Manejo de Animaciones y Corrutinas

Para cerrar el modal desde un botón interno, no basta con cambiar la variable a false. Si hiciéramos eso, el modal desaparecería de golpe sin animación. Para que se deslice suavemente hacia abajo, usamos la función hide() dentro de una corrutina:

  • Se lanza la corrutina con scope.launch.
  • Se llama a sheetState.hide(). Esta es una función suspendida que espera a que la animación de cierre termine.
  • Una vez completada la animación, ejecutamos el código para poner nuestra variable reactiva en false y liberar los recursos:
    • scope.launch { sheetState.hide() }.invokeOnCompletion {
           if (!sheetState.isVisible) {
               showSheet = false
            }
      }

⚠️ La "Capa Invisible" y el Estado

Es importante entender que el ModalBottomSheet tiene dos capas: 

  • El panel visual con tu contenido.
  • Otra capa oscura (scrim) que bloquea el resto de la pantalla.

Si olvidas poner la variable reactiva en false dentro del onDismissRequest, el panel podría ocultarse, pero la capa invisible seguiría activa, impidiéndote interactuar con los botones que están detrás. Por eso existe esa "redundancia" entre el estado del controlador y tu variable booleana; ambos deben estar sincronizados para que la experiencia de usuario sea la correcta.

Otros ejemplos de ModalBottomSheetState

A continuación se muestra otro ejemplo de cómo implementar un Bottom Sheet con Jetpack Compose:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyBottomSheetScreen() {
    val sheetState = rememberModalBottomSheetState(
        skipPartiallyExpanded = true // Optional: Makes the sheet only fully expanded or hidden
    )
    val scope = rememberCoroutineScope()
    var showBottomSheet by remember { mutableStateOf(false) }

    Scaffold(
        floatingActionButton = {
            ExtendedFloatingActionButton(
                text = { Text("Show bottom sheet") },
                icon = { Icon(Icons.Filled.Add, contentDescription = "") },
                onClick = {
                    showBottomSheet = true
                }
            )
        }
    ) { contentPadding ->
        Box(
            modifier = Modifier
                .fillMaxSize()
                .padding(contentPadding),
            contentAlignment = Alignment.Center
        ) {
            Text("Main Screen Content")
        }

        if (showBottomSheet) {
            ModalBottomSheet(
                onDismissRequest = {
                    // This is called when the user taps outside the sheet or presses back
                    showBottomSheet = false
                },
                sheetState = sheetState
            ) {
                // Sheet content
                Column(
                    modifier = Modifier.padding(16.dp),
                    horizontalAlignment = Alignment.CenterHorizontally
                ) {
                    Text("This is the content of the bottom sheet!")
                    Spacer(Modifier.height(16.dp))
                    Button(onClick = {
                        scope.launch {
                            sheetState.hide()
                        }.invokeOnCompletion {
                            if (!sheetState.isVisible) {
                                showBottomSheet = false
                            }
                        }
                    }) {
                        Text("Hide bottom sheet")
                    }
                }
            }
        }
    }
}

En este ejemplo, se utiliza ModalBottomSheet para mostrar el contenido del "bottom sheet". El estado del "bottom sheet" se controla con rememberModalBottomSheetState y se muestra u oculta cambiando el valor de la variable showBottomSheet.

Creando el Bottom Sheets en Android (XML - Legacy)

La siguiente es la forma de como se creaban los Bottom Sheets antes de la llegada de Jetpack Compose, si bien es funcional, hoy en día se recomienda usar Jetpack Compose para crear interfaces de usuario en Android. Si estás trabajando en un proyecto que no usa Jetpack Compose, puedes seguir usando esta forma.

Existen dos variantes de bottom sheets o paneles/modals deslizantes que podemos emplear en Android; por un lado tenemos los Persistent Bottom Sheet, que son empleados para mostrar información del mismo nivel o complementaria a la mostrada en la vista que contenga a dicho modal. Estos paneles tienen la misma elevación que la del resto de la vista y permanecen visibles incluso cuando no se están utilizando.

La segunda forma que tenemos son los modal bottom sheet, que son usados como alternativa a otros elementos que nos ofrece Android como menús, diálogos, alert, etc; estos tienen una elevación superior al contenido principal; lo que significa que lo oscurecen al mostrarse dicho panel o modal, y se deben ocultar para poder seguir empleando con el resto de la aplicación; funcionan como pantalla de bloqueo.

Este panel deslizante llamado Bottom Sheets cuenta con cinco estatus; en general cada uno de los estatus es bastante descriptivo pero aun así agregaremos una pequeña descripción a cada uno de ellos; el primero que veremos será el de persistent bottom sheet, que como definimos es un panel que cuenta con la misma elevación que la del contenido, por lo tanto no se oscurece el contenido que está "detrás":

Estados de los Bottom Sheets en Android

Los Bottom Sheets cuentan con estados como podemos ver a continuación que nos permite saber el estado de un Bottom Sheet en un momento dado:

  • BottomSheetBehavior.STATE_COLLAPSED: El Bottom Sheets está colapsado, lo que significa que el Bottom Sheets está totalmente abierto.
  • BottomSheetBehavior.STATE_DRAGGINGEl Bottom Sheets está siendo arrastrado por el usuario.
  • BottomSheetBehavior.STATE_EXPANDED: El Bottom Sheets no está expandido (STATE_COLLAPSED), pero tampoco está totalmente oculto (STATE_HIDDEN) se muestra una sección del mismo al "bottom" de la actividad cuyo tamaño es definido desde la propiedad behavior_peekHeight.
  • BottomSheetBehavior.STATE_HIDDENEl Bottom Sheets está totalmente oculto y no se mostrará a menos que indiquemos mediante una acción que se muestre (es decir desde código Java indiquemos que al menos se muestre una porción del Bottom Sheets para que el usuario pueda manipularlo).
  • BottomSheetBehavior.STATE_SETTLINGAl igual que ocurre con BottomSheetBehavior.STATE_DRAGGING, es un evento transitorio, lo que significa que se ejecuta cuando el Bottom Sheets está desplazándose entre los eventos anteriores.

De los cuales a mi parecer los más importantes o los que más podrían ser usados son estos:

  • BottomSheetBehavior.STATE_COLLAPSED:
  • BottomSheetBehavior.STATE_EXPANDED

Los cuales fueron explicados anteriormente y puedes ver ejemplificados en esta imagen:

Bottom sheets ejemplo en android

Cómo podrás darte cuenta, cuando el panel o Bottom Sheets está totalmente abierto, se establece el estado BottomSheetBehavior.STATE_EXPANDED, ahora si el mismo está plegado se establece el estado BottomSheetBehavior.STATE_COLLAPSED.

Para el caso de BottomSheetBehavior.STATE_COLLAPSED se muestra una pequeña sección del Bottom Sheets o panel, mientras que con BottomSheetBehavior.STATE_EXPANDED se muestra todo el layout que definamos aunque con el sistema de eventos podemos ocultar/mostrar layout fácilmente o realizar cualquier otra operación.

Cómo prácticamente todos los elementos en Android, son personalizables, por lo tanto podemos agregar cualquier elementos o cantidad de ellos así como un diseño y eventos personalizados contenidos dentro del panel; hasta hacer cosas como un reproductor personalizado como el mostrado en la imagen de presentación de esta entrada.

Ya con lo anterior claro, abrimos nuestro Android Studio y como primer paso, tenemos que agregar las dependencias correspondientes; debes consultar con la versión actual en la cual este Android; pero la idea es la siguiente; añadir el archivo build.gradle con las siguientes dependencias:

dependencies {
    ...
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
}

Una vez realizado lo anterior vamos al layout de nuestra actividad agregamos el siguiente código:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:fitsSystemWindows="true"
   tools:context="samples.despotoski.nikola.com.bottomsheetsample.MainActivity">
   <com.google.android.material.appbar.AppBarLayout
       android:id="@+id/app_bar"
       android:layout_width="match_parent"
       android:layout_height="@dimen/app_bar_height"
       android:fitsSystemWindows="true"
       android:theme="@style/AppTheme.AppBarOverlay">
       <com.google.android.material.appbar.CollapsingToolbarLayout
           android:id="@+id/toolbar_layout"
           android:layout_width="match_parent"
           android:layout_height="match_parent"
           android:fitsSystemWindows="true"
           app:contentScrim="?attr/colorPrimary"
           app:layout_scrollFlags="scroll|exitUntilCollapsed">
           <androidx.appcompat.widget.Toolbar
               android:id="@+id/toolbar"
               android:layout_width="match_parent"
               android:layout_height="?attr/actionBarSize"
               app:layout_collapseMode="pin"
               app:popupTheme="@style/AppTheme.PopupOverlay" />
       </com.google.android.material.appbar.CollapsingToolbarLayout>
   </com.google.android.material.appbar.AppBarLayout>
   <include layout="@layout/bottom_sheet_content_view" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

Cómo ves agregamos el código del Bottom Sheets a través de un include aunque puedes incluir el código directamente si así lo prefieres; en fin, nuestro layout de nuestro Bottom Sheets el cual lucirá de la siguiente manera:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   android:id="@+id/ll_bottom_sheet"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:background="@android:color/white"
   android:orientation="vertical"
   app:behavior_peekHeight="60dp"
   app:layout_behavior="@string/bottom_sheet_behavior">
   <TextView
       android:layout_width="match_parent"
       android:layout_height="60dp"
       android:background="#CCCCCC"
       android:gravity="center"
       android:text="@string/arrastrar"
       android:textAppearance="?android:attr/textAppearanceLarge" />
   <TextView
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:gravity="center"
       android:text="@string/contenido"
       android:textAppearance="?android:attr/textAppearanceLarge" />
</LinearLayout>

Como puedes ver hay dos atributos "extraños" los cuales son:

  • app:layout_behavior="@string/bottom_sheet_behavior" con el atributo app:layout_behavior con el valor @string/bottom_sheet_behavior convierte a nuestro LinearLayout u otro contenedor que escojamos en un bottom sheet. El valor de @string/bottom_sheet_behavior debe ser com.google.android.material.bottomsheet.BottomSheetBehavior.
  • app:behavior_peekHeight="60dp" Con el atributo app:behavior_peekHeight establecemos el tamaño de la sección que se mostrará en el estado BottomSheetBehavior.STATE_COLLAPSED.

Si no queremos que el Bottom Sheets ocupe todo el alto del contenedor, simplemente debemos variar el atributo android:layout_height del contenedor padre del panel que en nuestro ejemplo es el contenedor con el atributo android:id="@+id/ll_bottom_sheet".

Código java para el del Bottom Sheets en Android

Ahora vamos a ver algo de código Android el cual es necesario (lo mínimo) para indicar a Android que cree el Bottom Sheets a partir de los layouts anteriores; declaramos una variable de tipo BottomSheetBehavior:

private BottomSheetBehavior bottomSheetBehavior;

Ahora simplemente referenciamos nuestra vista y con el método from() creamos el bottom sheet; así de simple:

LinearLayout linearLayout = (LinearLayout) findViewById(R.id.ll_bottom_sheet); bottomSheetBehavior = BottomSheetBehavior.from(linearLayout);

Eventos del Bottom Sheets en Android

En cuanto a los eventos, podemos ejecutar secciones de código personalizadas en base al estado del panel con un código como el siguiente:

bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
   @Override
   public void onStateChanged(@NonNULL View bottomSheet, int newState) {
       switch (newState) {
           case BottomSheetBehavior.STATE_EXPANDED:
               Log.i("BottomSheetBehavior", "STATE_EXPANDED");
               break;
           case BottomSheetBehavior.STATE_DRAGGING:
               Log.i("BottomSheetBehavior", "STATE_DRAGGING");
               break;
           case BottomSheetBehavior.STATE_COLLAPSED:
               Log.i("BottomSheetBehavior", "STATE_COLLAPSED");
               break;
           case BottomSheetBehavior.STATE_HIDDEN:
               Log.i("BottomSheetBehavior", "STATE_HIDDEN");
               break;
           case BottomSheetBehavior.STATE_SETTLING:
               Log.i("BottomSheetBehavior", "STATE_SETTLING");
               break;
       }
   }
   @Override
   public void onSlide(@NonNULL View bottomSheet, float slideOffset) {
   }
});

Los estados de los mismos ya fueron explicados anteriormente; finalmente el código completo de nuestra actividad principal es:

public class MainActivity extends AppCompatActivity {
   private BottomSheetBehavior bottomSheetBehavior;
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
       setSupportActionBar(toolbar);
       LinearLayout linearLayout = (LinearLayout) findViewById(R.id.ll_bottom_sheet);
       bottomSheetBehavior = BottomSheetBehavior.from(linearLayout);
       bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
           @Override
           public void onStateChanged(@NonNULL View bottomSheet, int newState) {
               switch (newState) {
                   case BottomSheetBehavior.STATE_EXPANDED:
                       Log.i("BottomSheetBehavior", "STATE_EXPANDED");
                       break;
                   case BottomSheetBehavior.STATE_DRAGGING:
                       Log.i("BottomSheetBehavior", "STATE_DRAGGING");
                       break;
                   case BottomSheetBehavior.STATE_COLLAPSED:
                       Log.i("BottomSheetBehavior", "STATE_COLLAPSED");
                       break;
                   case BottomSheetBehavior.STATE_HIDDEN:
                       Log.i("BottomSheetBehavior", "STATE_HIDDEN");
                       break;
                   case BottomSheetBehavior.STATE_SETTLING:
                       Log.i("BottomSheetBehavior", "STATE_SETTLING");
                       break;
               }
           }
           @Override
           public void onSlide(@NonNULL View bottomSheet, float slideOffset) {
           }
       });
   }
   @Override
   public void onBackPressed() {
       if (bottomSheetBehavior.getState() != BottomSheetBehavior.STATE_HIDDEN) {
           bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
       } else {
           super.onBackPressed();
       }
   }
   @Override
   public boolean onCreateOptionsMenu(Menu menu) {
       getMenuInflater().inflate(R.menu.menu_bottom_sheet, menu);
       return true;
   }
   @Override
   public boolean onOptionsItemSelected(MenuItem item) {
       int id = item.getItemId();
       if (id == R.id.action_settings) {
           return true;
       }
       return super.onOptionsItemSelected(item);
   }
}

Y con esto conseguimos un persistent Bottom Sheets como el siguiente:

persistent bottom sheet

Creando un modal bottom sheet en Android Studio

Si por el contrario quisiera emplear la modal bottom sheet que es un modal que solapa o está posicionado por encima del resto del contenido y por ende aparece una sombra gris sobre el resto del contenido:

modal bottom sheet

Debemos crear una clase que extienda de BottomSheetDialogFragment que defina dicho modal; que a la final es un fragment modal y desde aquí puedes realizar las conexiones a la base de datos, a alguna API, validar internamente en la app, etc; un esquema sencillo sería el siguiente:

public class MiBottomSheetDialogFragment extends BottomSheetDialogFragment {
   static MiBottomSheetDialogFragment newInstance() {
       return new MiBottomSheetDialogFragment();
   }
   @Override
   public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
       View v = inflater.inflate(R.layout.bottom_sheet_modal_fragment, container, false);
       return v;
   }
}

En el layout, puedes emplear el presentado anteriormente, o el que tu prefieras; finalmente, en la actividad que queremos mostrar el Modal Bottom Sheet hacemos lo siguiente:

        Button b_open_bs = (Button)findViewById(R.id.b_open_bs);
        b_open_bs.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                BottomSheetDialogFragment bsdFragment =
                        MiBottomSheetDialogFragment.newInstance();
                bsdFragment.show(
                        MainActivity.this.getSupportFragmentManager(), "BSDialog");
            }
        });

El siguiente paso, aprende a usar otro panel deslizante como lo es un Drawer en Android Studio.

Aprende a implementar un Modal Bottom Sheet en Jetpack Compose. Guía paso a paso sobre estados reactivos, manejo de corrutinas para animaciones y personalización de diseños inferiores en Android.

Acepto recibir anuncios de interes sobre este Blog.

Andrés Cruz

EN In english