Crear un botón de descarga
Cómo implementar un botón de descarga.
Las apps están llenas de botones que ejecutan comportamientos de larga duración. Por ejemplo, un botón podría iniciar una descarga, lo que comienza un proceso de descarga, recibe datos a lo largo del tiempo, y luego proporciona acceso al asset descargado. Es útil mostrar al usuario el progreso de un proceso de larga duración, y el botón en sí mismo es un buen lugar para proporcionar esta información. En esta receta, construirás un botón de descarga que realiza transiciones a través de múltiples estados visuales, basados en el estado de una descarga de la app.
La siguiente animación muestra el comportamiento de la app:
Definir un nuevo stateless widget
#El widget de tu botón necesita cambiar su apariencia con el tiempo. Por lo tanto, necesitas implementar tu botón con un stateless widget personalizado.
Define un nuevo stateless widget llamado DownloadButton.
@immutable
class DownloadButton extends StatelessWidget {
const DownloadButton({super.key});
@override
Widget build(BuildContext context) {
// TODO:
return const SizedBox();
}
}
Definir los posibles estados visuales del botón
#La presentación visual del botón de descarga se basa en un
estado de descarga determinado. Define los estados posibles de
la descarga, y luego actualiza DownloadButton para que acepte
un DownloadStatus y una Duration del tiempo que el botón
debería tomar para animarse de un estado a otro.
enum DownloadStatus { notDownloaded, fetchingDownload, downloading, downloaded }
@immutable
class DownloadButton extends StatelessWidget {
const DownloadButton({
super.key,
required this.status,
this.transitionDuration = const Duration(milliseconds: 500),
});
final DownloadStatus status;
final Duration transitionDuration;
@override
Widget build(BuildContext context) {
// TODO: We'll add more to this later.
return const SizedBox();
}
}
Mostrar la forma del botón
#El botón de descarga cambia su forma basándose en el estado de
descarga. El botón muestra un rectángulo gris redondeado durante
los estados notDownloaded y downloaded.
El botón muestra un círculo transparente durante los
estados fetchingDownload y downloading.
Basándote en el DownloadStatus actual,
construye un AnimatedContainer con una
ShapeDecoration que muestre un rectángulo
redondeado o un círculo.
Considera definir el árbol de widgets de la forma en un
widget Stateless separado para que el método build()
principal se mantenga simple, permitiendo las adiciones
que siguen. En lugar de crear una función para retornar un widget,
como Widget _buildSomething() {}, prefiere siempre crear un
StatelessWidget o un StatefulWidget, lo cual tiene un mejor rendimiento. Puedes encontrar
más consideraciones sobre esto en la documentación
o en un video dedicado en el canal de YouTube de Flutter.
Por ahora, el hijo del AnimatedContainer es solo un SizedBox porque volveremos a él en otro paso.
@immutable
class DownloadButton extends StatelessWidget {
const DownloadButton({
super.key,
required this.status,
this.transitionDuration = const Duration(milliseconds: 500),
});
final DownloadStatus status;
final Duration transitionDuration;
bool get _isDownloading => status == DownloadStatus.downloading;
bool get _isFetching => status == DownloadStatus.fetchingDownload;
bool get _isDownloaded => status == DownloadStatus.downloaded;
@override
Widget build(BuildContext context) {
return ButtonShapeWidget(
transitionDuration: transitionDuration,
isDownloaded: _isDownloaded,
isDownloading: _isDownloading,
isFetching: _isFetching,
);
}
}
@immutable
class ButtonShapeWidget extends StatelessWidget {
const ButtonShapeWidget({
super.key,
required this.isDownloading,
required this.isDownloaded,
required this.isFetching,
required this.transitionDuration,
});
final bool isDownloading;
final bool isDownloaded;
final bool isFetching;
final Duration transitionDuration;
@override
Widget build(BuildContext context) {
final ShapeDecoration shape;
if (isDownloading || isFetching) {
shape = const ShapeDecoration(
shape: CircleBorder(),
color: Colors.transparent,
);
} else {
shape = const ShapeDecoration(
shape: StadiumBorder(),
color: CupertinoColors.lightBackgroundGray,
);
}
return AnimatedContainer(
duration: transitionDuration,
curve: Curves.ease,
width: double.infinity,
decoration: shape,
child: const SizedBox(),
);
}
}
Quizás te preguntes por qué necesitas un widget ShapeDecoration
para un círculo transparente, dado que es invisible.
El propósito del círculo invisible es orquestar
la animación deseada. El AnimatedContainer comienza con un rectángulo
redondeado. Cuando el DownloadStatus cambia a fetchingDownload,
el AnimatedContainer necesita animarse desde un rectángulo redondeado
a un círculo, y luego desvanecerse a medida que ocurre la animación.
La única forma de implementar esta animación es definir tanto
la forma inicial de un rectángulo redondeado como la
forma final de un círculo. Pero no quieres que el círculo
final sea visible, así que lo haces transparente,
lo que provoca un desvanecimiento animado.
Mostrar el texto del botón
#El DownloadButton muestra GET durante la
fase notDownloaded, OPEN durante la fase downloaded,
y ningún texto en el medio.
Añade widgets para mostrar texto durante cada fase de descarga,
y anima la opacidad del texto en el medio. Añade el árbol
de widgets de texto como hijo del AnimatedContainer en el
widget contenedor del botón.
@immutable
class ButtonShapeWidget extends StatelessWidget {
const ButtonShapeWidget({
super.key,
required this.isDownloading,
required this.isDownloaded,
required this.isFetching,
required this.transitionDuration,
});
final bool isDownloading;
final bool isDownloaded;
final bool isFetching;
final Duration transitionDuration;
@override
Widget build(BuildContext context) {
final ShapeDecoration shape;
if (isDownloading || isFetching) {
shape = const ShapeDecoration(
shape: CircleBorder(),
color: Colors.transparent,
);
} else {
shape = const ShapeDecoration(
shape: StadiumBorder(),
color: CupertinoColors.lightBackgroundGray,
);
}
return AnimatedContainer(
duration: transitionDuration,
curve: Curves.ease,
width: double.infinity,
decoration: shape,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: AnimatedOpacity(
duration: transitionDuration,
opacity: isDownloading || isFetching ? 0.0 : 1.0,
curve: Curves.ease,
child: Text(
isDownloaded ? 'OPEN' : 'GET',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.bold,
color: CupertinoColors.activeBlue,
),
),
),
),
);
}
}
Mostrar un indicador de carga (spinner) mientras se obtiene la descarga
#Durante la fase fetchingDownload, el DownloadButton
muestra un indicador de carga radial (radial spinner). Este indicador aparece gradualmente desde
la fase notDownloaded y se desvanece hacia
la fase fetchingDownload.
Implementa un indicador de carga radial que se ubique encima de la forma del botón y que aparezca y desaparezca gradualmente en los momentos apropiados.
Hemos eliminado el constructor de ButtonShapeWidget para mantener el
enfoque en su método build y en el widget Stack que hemos añadido.
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _onPressed,
child: Stack(
children: [
ButtonShapeWidget(
transitionDuration: transitionDuration,
isDownloaded: _isDownloaded,
isDownloading: _isDownloading,
isFetching: _isFetching,
),
Positioned.fill(
child: AnimatedOpacity(
duration: transitionDuration,
opacity: _isDownloading || _isFetching ? 1.0 : 0.0,
curve: Curves.ease,
child: ProgressIndicatorWidget(
downloadProgress: downloadProgress,
isDownloading: _isDownloading,
isFetching: _isFetching,
),
),
),
],
),
);
}
Mostrar el progreso y un botón de detener mientras se descarga
#Después de la fase fetchingDownload está la fase downloading.
Durante la fase downloading, el DownloadButton
reemplaza el indicador de progreso radial por una barra de progreso
radial en crecimiento. El DownloadButton también muestra un icono de botón
de parada para que el usuario pueda cancelar una descarga en curso.
Añade una propiedad progress al widget DownloadButton,
y luego actualiza la visualización del progreso para cambiar a una barra de progreso
radial durante la fase downloading.
A continuación, añade un icono de botón de parada en el centro de la barra de progreso radial.
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _onPressed,
child: Stack(
children: [
ButtonShapeWidget(
transitionDuration: transitionDuration,
isDownloaded: _isDownloaded,
isDownloading: _isDownloading,
isFetching: _isFetching,
),
Positioned.fill(
child: AnimatedOpacity(
duration: transitionDuration,
opacity: _isDownloading || _isFetching ? 1.0 : 0.0,
curve: Curves.ease,
child: Stack(
alignment: Alignment.center,
children: [
ProgressIndicatorWidget(
downloadProgress: downloadProgress,
isDownloading: _isDownloading,
isFetching: _isFetching,
),
if (_isDownloading)
const Icon(
Icons.stop,
size: 14.0,
color: CupertinoColors.activeBlue,
),
],
),
),
),
],
),
);
}
Añadir callbacks para el toque del botón
#El último detalle que necesita tu DownloadButton es el
comportamiento del botón. El botón debe hacer cosas cuando el usuario lo toca.
Añade propiedades de widget para callbacks que inicien una descarga, cancelen una descarga y abran una descarga.
Finalmente, envuelve el árbol de widgets existente de DownloadButton
con un widget GestureDetector, y reenvía el
evento de toque a la propiedad de callback correspondiente.
@immutable
class DownloadButton extends StatelessWidget {
const DownloadButton({
super.key,
required this.status,
this.downloadProgress = 0,
required this.onDownload,
required this.onCancel,
required this.onOpen,
this.transitionDuration = const Duration(milliseconds: 500),
});
final DownloadStatus status;
final double downloadProgress;
final VoidCallback onDownload;
final VoidCallback onCancel;
final VoidCallback onOpen;
final Duration transitionDuration;
bool get _isDownloading => status == DownloadStatus.downloading;
bool get _isFetching => status == DownloadStatus.fetchingDownload;
bool get _isDownloaded => status == DownloadStatus.downloaded;
void _onPressed() {
switch (status) {
case DownloadStatus.notDownloaded:
onDownload();
case DownloadStatus.fetchingDownload:
// do nothing.
break;
case DownloadStatus.downloading:
onCancel();
case DownloadStatus.downloaded:
onOpen();
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _onPressed,
child: const Stack(
children: [
/* ButtonShapeWidget and progress indicator */
],
),
);
}
}
¡Felicitaciones! Tienes un botón que cambia su visualización dependiendo de en qué fase se encuentre el botón: no descargado, obteniendo descarga, descargando y descargado. Ahora, el usuario puede tocar para iniciar una descarga, tocar para cancelar una descarga en curso, y tocar para abrir una descarga completada.
Ejemplo interactivo
#Ejecutar la app:
- Haz clic en el botón GET para iniciar una descarga simulada.
- El botón cambia a un indicador de progreso para simular una descarga en curso.
- Cuando se completa la descarga simulada, el botón realiza la transición a OPEN, para indicar que la app está lista para que el usuario abra el asset descargado.
A menos que se indique lo contrario, la documentación de este sitio refleja Flutter 3.44.0. Página actualizada por última vez el 2026-05-18. Ver código fuente oreportar un problema.