How to Create a Download Button in Flutter

How to Create a Download Button in Flutter?

Apps are brimming with buttons that carry out long-running actions. A button, for example, could initiate a download, which initiates the download process, receives data over time, and ultimately grants access to the downloaded content.

It’s useful to provide the user with the details of how a long-running procedure is progressing, and the button is a suitable spot to do so. This recipe will show you how to make a download button that changes visual states depending on the status of an app download. If you are having trouble implementing the solution, gets talented Flutter developers for hire from our company. Our developers are also experts in creating high-end mobile applications.

The steps involved in the process of creating a download button in the flutter development app are mentioned below:

  1. Defining a new Stateful widget
  2. Defining the possible visual states of the button
  3. Display the shape of the button
  4. Creating the display of the button’s text
  5. Display a spinner while fetching the download
  6. Display the progress percentage and a stop button while downloading
  7. Add callbacks to button taps

1. Defining a new stateful widget

The appearance of your button widget must alter over time. As a result, you’ll need to use a bespoke stateless widget to implement your button. Then, define a new stateless widget called DownloadButton.

class DownloadButton extends StatelessWidget {
const DownloadButton({
super.key,
  });
 @override
  Widget build(BuildContext context) {
    // TODO:
return const SizedBox();
  }
}

2. Defining the possible visual states of the button

The current download status determines the visual display of the download button. After Defining the download’s possible states, update DownloadButton to accept a DownloadStatus and specify the duration for how long the button should fly from one visual state to the next.

When designing a custom widget, you must select whether it receives all necessary information from its parent or if the Widget orchestrates the application’s activity internally. DownloadButton, for example, might get the existing DownloadStatus from its parent, or it may coordinate the download process within its State object.

Rather than managing behavior within the Widget, the ideal solution for most widgets is to transfer relevant information into it from its parent. You assure increased utility for the Widget, simpler testing, and future modifications to application behavior by handing in all essential information.

enum DownloadStatus {
  notDownloaded,
  fetchingDownload,
  downloading,
  downloaded,
}

3. Display the shape of the button

The form of the download button varies depending on the download state. During the notDownloaded and downloaded phases, the button shows a gray, rounded rectangle. During the fetchingDownload and downloading stages, the button shows a translucent circle.

In the next step, Create an AnimatedContainer with a ShapeDecoration that shows a rounded rectangle or a circle based on the current DownloadStatus.

Consider establishing the shape’s widget tree in a separate Stateless widget to keep the main construct() function simple while allowing for the modifications that will come later. Then, rather than constructing a function to return a widget, such as Widget _buildSomething(), always create a StatelessWidget or a StatefulWidget, which is faster.

Now, the AnimatedContainer appears to be just a SizedBox child, but you don’t need to worry; we will resolve this state in another step.

@immutable
classButtonShapeWidget extends StatelessWidget {
constButtonShapeWidget({
super.key,
requiredthis.isDownloading,
requiredthis.isDownloaded,
requiredthis.isFetching,
requiredthis.transitionDuration,
  });
final bool isDownloading;
final bool isDownloaded;
final bool isFetching;
final Duration transitionDuration;
  @override
  Widget build(BuildContext context) {
var shape = constShapeDecoration(
shape: StadiumBorder(),
color: CupertinoColors.lightBackgroundGray,
    );

if (isDownloading || isFetching) {
shape = ShapeDecoration(
shape: constCircleBorder(),
color: Colors.white.withOpacity(0.0),
      );
    }
return AnimatedContainer(
duration: transitionDuration,
curve: Curves.ease,
width: double.infinity,
decoration: shape,
child: constSizedBox(),
    );
  }
}

4. Creating the display of the button’s text

Different messages are displayed through the Download button in its different phases. The notDownloaded phase displays the GET option. The downloaded phases display the OPEN option for the users, while no text is displayed in the intermediate process.

Add widgets to show text throughout each download step, and animate the opacity of the text between them. For example, in the button wrapper widget, make the text widget tree a child of the AnimatedContainer.

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.button?.copyWith(
fontWeight: FontWeight.bold,
color: CupertinoColors.activBlue,
                ),
          ),
        ),
      ),
    );

5. Display a spinner while fetching the download

The DownloadButton shows a radial spinner while fetchingDownload is in progress. This spinner transitions from notDownloaded to fetchingDownload.

Install a radial spinner on top of the button form that fades in and out at appropriate intervals.

The function Object() { [native code] } of the ButtonShapeWidget has been eliminated to focus on its build function and the Stack widget that we’ve created.

@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,
            ),
          ),
        ),
      ],
    ),
  );
}

6. Display the progress percentage and a stop button while downloading

Following the retrieval, the downloading step is exactly that. The DownloadButton transforms the radial progress spinner with a rising radial progress bar during the downloading period. A stop button symbol is also displayed on the DownloadButton, allowing the user to halt an ongoing download.

After adding an advancement property to the DownloadButton plugin, change the progress display to a radial toolbar during the downloading phase. Then, place a stop button icon in the center of the radial progress bar.

@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,
                  ),
              ],
            ),
          ),
        ),
      ],
    ),
  );
}

7. Add callbacks to button taps

The button behavior is the final thing that your DownloadButton requires. When the user clicks the button, it must do some action. Add callbacks for starting a download, canceling a download, and opening a download to the widget properties.

Finally, use a GestureDetector widget to cover DownloadButton’s current widget tree and route the tap event to the relevant callback property.

void _onPressed() {
switch (status) {
case DownloadStatus.notDownloaded:
 onDownload();
 break;
case DownloadStatus.fetchingDownload:
        // do nothing.
 break;
case DownloadStatus.downloading:
 onCancel();
 break;
case DownloadStatus.downloaded:
 onOpen();
 break;
    }
  }

Now at the end of the process, you have a button that displays different information based on whatever phase it is in: not downloaded, seeking download, downloading phase, and downloaded completely. The user may tap to initiate a download, stop an ongoing download, and access a finished download by tapping.

Let’s see a full example of DownloadButton:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';


void main() {
  runApp(
    const MaterialApp(
      home: ExampleDownloadButton(),
      debugShowCheckedModeBanner: false,
    ),
  );
}


@immutable
class ExampleDownloadButton extends StatefulWidget {
  const ExampleDownloadButton({super.key});
  @override
  _ExampleDownloadButtonState createState() => _ExampleDownloadButtonState();
}


class _ExampleDownloadButtonState extends State {
  late final List _downloadControllers;
  @override
  void initState() {
    super.initState();
    _downloadControllers = List.generate(
      10,
      (index) => SimulatedDownloadController(onOpenDownload: () {
        _openDownload(index);
      }),
    );
  }


  void _openDownload(int index) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text('Open PDF ${index + 1}'),
      ),
    );
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Download Button')),
      body: ListView.separated(
        itemCount: _downloadControllers.length,
        separatorBuilder: (_, __) => const Divider(),
        itemBuilder: _buildListItem,
      ),
    );
  }


  Widget _buildListItem(BuildContext context, int index) {
    final theme = Theme.of(context);
    final downloadController = _downloadControllers[index];
    return ListTile(
      leading: const Icon(Icons.list_rounded),
      title: Text(
        'Pdf ${index + 1}',
        overflow: TextOverflow.ellipsis,
        style: theme.textTheme.titleLarge,
      ),
      trailing: SizedBox(
        width: 96,
        child: AnimatedBuilder(
          animation: downloadController,
          builder: (context, child) {
            return DownloadButton(
              status: downloadController.downloadStatus,
              downloadProgress: downloadController.progress,
              onDownload: downloadController.startDownload,
              onCancel: downloadController.stopDownload,
              onOpen: downloadController.openDownload,
            );
          },
        ),
      ),
    );
  }
}


enum DownloadStatus {
  notDownloaded,
  fetchingDownload,
  downloading,
  downloaded,
}


abstract class DownloadController implements ChangeNotifier {
  DownloadStatus get downloadStatus;
  double get progress;


  void startDownload();
  void stopDownload();
  void openDownload();
}


class SimulatedDownloadController extends DownloadController
    with ChangeNotifier {
  SimulatedDownloadController({
    DownloadStatus downloadStatus = DownloadStatus.notDownloaded,
    double progress = 0.0,
    required VoidCallback onOpenDownload,
  })  : _downloadStatus = downloadStatus,
        _progress = progress,
        _onOpenDownload = onOpenDownload;


  DownloadStatus _downloadStatus;
  @override
  DownloadStatus get downloadStatus => _downloadStatus;


  double _progress;
  @override
  double get progress => _progress;


  final VoidCallback _onOpenDownload;


  bool _isDownloading = false;


  @override
  void startDownload() {
    if (downloadStatus == DownloadStatus.notDownloaded) {
      _doSimulatedDownload();
    }
  }


  @override
  void stopDownload() {
    if (_isDownloading) {
      _isDownloading = false;
      _downloadStatus = DownloadStatus.notDownloaded;
      _progress = 0.0;
      notifyListeners();
    }
  }


  @override
  void openDownload() {
    if (downloadStatus == DownloadStatus.downloaded) {
      _onOpenDownload();
    }
  }


  Future _doSimulatedDownload() async {
    _isDownloading = true;
    _downloadStatus = DownloadStatus.fetchingDownload;
    notifyListeners();


    // Wait a second to simulate fetch time.
    await Future.delayed(const Duration(seconds: 1));


    // If the user chose to cancel the download, stop the simulation.
    if (!_isDownloading) {
      return;
    }


    // Shift to the downloading phase.
    _downloadStatus = DownloadStatus.downloading;
    notifyListeners();


    const downloadProgressStops = [0.0, 0.15, 0.45, 0.8, 1.0];
    for (final stop in downloadProgressStops) {
      // Wait a second to simulate varying download speeds.
      // await Future.delayed(const Duration(seconds: 1));


      if (!_isDownloading) {
        return;
      }


      _progress = stop;
      notifyListeners();
    }
    await Future.delayed(const Duration(seconds: 1));


    if (!_isDownloading) {
      return;
    }


    _downloadStatus = DownloadStatus.downloaded;
    _isDownloading = false;
    notifyListeners();
  }
}


@immutable
class DownloadButton extends StatelessWidget {
  const DownloadButton({
    super.key,
    required this.status,
    this.downloadProgress = 0.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();
        break;
      case DownloadStatus.fetchingDownload:
        // do nothing.
        break;
      case DownloadStatus.downloading:
        onCancel();
        break;
      case DownloadStatus.downloaded:
        onOpen();
        break;
    }
  }


  @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,
                      color: CupertinoColors.activeBlue,
                    ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}


@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) {
    var shape = const ShapeDecoration(
      shape: StadiumBorder(),
      color: CupertinoColors.lightBackgroundGray,
    );
    if (isDownloading || isFetching) {
      shape = ShapeDecoration(
        shape: const CircleBorder(),
        color: Colors.white.withOpacity(0),
      );
    }
    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,
                ),
          ),
        ),
      ),
    );
  }
}


@immutable
class ProgressIndicatorWidget extends StatelessWidget {
  const ProgressIndicatorWidget({
    super.key,
    required this.downloadProgress,
    required this.isDownloading,
    required this.isFetching,
  });
  final double downloadProgress;
  final bool isDownloading;
  final bool isFetching;
  @override
  Widget build(BuildContext context) {
    return AspectRatio(
      aspectRatio: 1,
      child: TweenAnimationBuilder(
        tween: Tween(begin: 0, end: downloadProgress),
        duration: const Duration(milliseconds: 200),
        builder: (context, progress, child) {
          return CircularProgressIndicator(
            backgroundColor: isDownloading
                ? CupertinoColors.lightBackgroundGray
                : Colors.white.withOpacity(0),
            valueColor: AlwaysStoppedAnimation(isFetching
                ? CupertinoColors.lightBackgroundGray
                : CupertinoColors.activeBlue),
            strokeWidth: 2,
            value: isFetching ? null : progress as double?,
          );
        },
      ),
    );
  }
}

Output

Download Button Final Output
Download Button Final Output

Conclusion

So far, we learned How to create download button in Flutter. We have seen the 7 step process to create a download button. Thank you for Reading the article. Hope you enjoying our content. Keep visiting Flutter Agency for Flutter business app development solutions.

Flutter Developers from Flutter Agency

Abhishek Dhanani

Written by Abhishek Dhanani

Abhishek Dhanani, a skilled software developer with 3+ years of experience, masters Dart, JavaScript, TypeScript, and frameworks like Flutter and NodeJS. Proficient in MySQL, Firebase, and cloud platforms AWS and GCP, he delivers innovative digital solutions.

Leave a comment

Your email address will not be published. Required fields are marked *


Discuss Your Project

Connect with Flutter Agency's proficient skilled team for your app development projects across different technologies. We'd love to hear from you! Fill out the form below to discuss your project.

Have Project For Us

Get in Touch

"*" indicates required fields

ready to get started?

Fill out the form below and we will be in touch soon!

"*" indicates required fields