How To Write a Simple Downloading App With Flutter
22 April 2020
Intro
In this article you will learn how to write a simple app which:
- downloads a file,
- shows download progress,
- stores downloaded file available outside the app,
- shows local notification with a download result,
- opens downloaded file in a native app by tapping the notification.
If you are new to Flutter and you have never built nor created an application using Flutter, please get familiar with Get started section on the official flutter.dev website before continuing.
Click here to find out how to set up your development environment.
End result
Step 0. Setting up the project
First of all, create a new Flutter project if you haven’t done it already 🙂
Step 1. Adding dependencies
Let’s start with a few packages that are going to help us achieving our final result. Add following dependencies to the pubspec.yaml file:
dio: ^3.0.9
path_provider: ^1.6.5
downloads_path_provider_28: ^0.1.0
permission_handler: ^4.4.0+hotfix.2
open_file: ^3.0.1
flutter_local_notifications: ^1.3.0
dio — a powerful http client for Dart, which supports file downloading among other things,
path_provider — plugin for finding commonly used locations in the filesystem, in our case used for a particular application document directory on iOS,
downloads_path_provider_28 — plugin to get the downloads directory on Android,
permission_handler — plugin to check and request permissions,
open_file —plugin which can call a native app to open file with certain extension,
flutter_local_notifications — plugin for displaying local notifications.
Step 2. Info.plist and AndroidManifest.xml
- iOS
To allow arbitrary loads and to be able to see downloaded files in app directory (Locations/On My iPhone/<app_name>) you have to extend your info.plist with keys listed below.
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>UIFileSharingEnabled</key>
<true/>
Detailed info about used keys:
Setup flutter_local_notifications plugin as described on the iOS integration section of the plugin readme. For our needs, we just need to copy one if statement, depending on what iOS language in our project we use.
Add the following lines to the didFinishLaunchingWithOptions method in the AppDelegate.m/AppDelegate.swift file of your iOS project
Swift:
Objective-C:
- Android
All we have to do in our Android project, is to add Internet and read/write external storage permissions to AndroidManifest.xml file.
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
Detailed info about used permissions:
Step 3. Starting point
Copy and replace _MyHomePageState class as our starting point and let’s start coding! As you can see, all I have changed in the code below are just the small things like icons and labels.
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { String _progress = "-"; Future<void> _download() async { // download } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'Download progress:', ), Text( '$_progress', style: Theme.of(context).textTheme.display1, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _download, tooltip: 'Download', child: Icon(Icons.file_download), ), ); } }
Step 4. Resources
To download anything, we need to have url to the file we are going to get, so let’s specify it. Add two final Strings with url and filename. Just to make it simple we are going to hardcode the resources. In a real life scenario you would probably get the resource URL from your database or Web API.
final String _fileUrl = "http://lot.services/blog/files/DSCF0277.jpg"; final String _fileName = "DSCF0277.jpg";
Step 5. Downloads directory path
We already know the filename and url to the file we want to download, so now it’s time to get the directory where we will store it. Depending on the platform we are going to get different paths since both operating systems have different policies of storing files.
If you are interested in this topic, read more about it here for iOS and here for Android.
The simplest way to get our download path is to use path_provider and downloads_path_provider_28 just like this:
Future<Directory> _getDownloadDirectory() async { if (Platform.isAndroid) { return await DownloadsPathProvider.downloadsDirectory; } // in this example we are using only Android and iOS so I can assume // that you are not trying it for other platforms and the if statement // for iOS is unnecessary // iOS directory visible to user return await getApplicationDocumentsDirectory(); }
Step 6. Storage permissions
Check and request for the storage permissions. Even though we have added appropriate keys to our native configuration files we still need to make sure if we are allowed to access the storage.
Future<bool> _requestPermissions() async { var permission = await PermissionHandler().checkPermissionStatus(PermissionGroup.storage); if (permission != PermissionStatus.granted) { await PermissionHandler().requestPermissions([PermissionGroup.storage]); permission = await PermissionHandler().checkPermissionStatus(PermissionGroup.storage); } return permission == PermissionStatus.granted; }
Step 7. Download method
We already know what we want to download, where to store it and if we have sufficient permissions to do this. Actually, the next step is to download… But right before this, let’s fill body of our _download() method with the things we know.
Future<void> _download() async { final dir = await _getDownloadDirectory(); final isPermissionStatusGranted = await _requestPermissions(); if (isPermissionStatusGranted) { final savePath = path.join(dir.path, _fileName); await _startDownload(savePath); } else { // handle the scenario when user declines the permissions } }
To combine our directory path and filename we need to import dart path package.
import 'package:path/path.dart' as path;
Step 8. Download process
Finally, it’s time to download our file. Create an instance of Dio and add _startDownload() method which will download a file from the provided url to “save path”.
final Dio _dio = Dio(); Future<void> _startDownload(String savePath) async { final response = await _dio.download( _fileUrl, savePath ); }
To update download progress, we need to add one more method in which we will calculate the percentage download status.
Simple as that.
void _onReceiveProgress(int received, int total) { if (total != -1) { setState(() { _progress = (received / total * 100).toStringAsFixed(0) + "%"; }); } }
Now, to enable the process of calculating the progress, we have to pass our method as a parameter to Dio’s download function.
final response = await _dio.download( _fileUrl, savePath, onReceiveProgress: _onReceiveProgress );
Build and run the example and you should already be able to download the file and see the progress updated as shown below.
Step 9. Local notifications initialization
Create a new instance of the flutter_local_notifications plugin and initialize it.
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
Initialisation should only be done once and the place where this can be done is in the main function, or alternatively within the first shown page of your app. Override initState and initialize it.
@override void initState() { super.initState(); flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); final android = AndroidInitializationSettings('@mipmap/ic_launcher'); final iOS = IOSInitializationSettings(); final initSettings = InitializationSettings(android, iOS); flutterLocalNotificationsPlugin.initialize(initSettings, onSelectNotification: _onSelectNotification); }
For now, add also an empty onSelectNotification method.
Future<void> _onSelectNotification(String json) async { // todo: handling clicked notification }
Step 10. Calling _showNotification method on downloads finish
After the download is completed, we need to show the notification whether it has succeeded or not. Modify our _startDownload method.
Future<void> _startDownload(String savePath) async { Map<String, dynamic> result = { 'isSuccess': false, 'filePath': null, 'error': null, }; try { final response = await _dio.download( _fileUrl, savePath, onReceiveProgress: _onReceiveProgress ); result['isSuccess'] = response.statusCode == 200; result['filePath'] = savePath; } catch (ex) { result['error'] = ex.toString(); } finally { await _showNotification(result); } }
We have added simple Map with three values to determine if a download is completed successfully and to store a file path or error message. The logic is also closed within try catch finally clause which finally calls _showNotification.
Step 11. Showing local notification
Add _showNotification method. In this method we will call show function to show simple notification.
Future<void> _showNotification(Map<String, dynamic> downloadStatus) async { final android = AndroidNotificationDetails( 'channel id', 'channel name', 'channel description', priority: Priority.High, importance: Importance.Max ); final iOS = IOSNotificationDetails(); final platform = NotificationDetails(android, iOS); final json = jsonEncode(downloadStatus); final isSuccess = downloadStatus['isSuccess']; await flutterLocalNotificationsPlugin.show( 0, // notification id isSuccess ? 'Success' : 'Failure', isSuccess ? 'File has been downloaded successfully!' : 'There was an error while downloading the file.', platform, payload: json ); }
Except the basic configuration needed to show the notification I have also encoded our Map to JSON and passed it to a payload parameter. The payload String will be used in the method fired when we tap the notification.
Step 12. Handling notification tap
The last thing we have to do is to fulfill our empty _onSelectNotification method. Here we will just decode our encoded Map to JSON String and open downloaded file if the download succeeded or show dialog with an error message.
Future<void> _onSelectNotification(String json) async { final obj = jsonDecode(json); if (obj['isSuccess']) { OpenFile.open(obj['filePath']); } else { showDialog( context: context, builder: (_) => AlertDialog( title: Text('Error'), content: Text('${obj['error']}'), ), ); } }
That’s it!
You can find the full example in my GitHub repository.