FlutterDownloader works downloading pdf in android but fails on iOS

Viewed 2712

I'm new to flutter/iOS.

I'm using:

Flutter 1.22.6 • channel stable • https://github.com/flutter/flutter.git Framework • revision 9b2d32b605 • 2021-01-22 14:36:39 -0800 Engine • revision 2f0af37152 Tools • Dart 2.10.5 flutter_downloader: ^1.4.4

I have to correct an application that I did not code I'm trying to understand it. It downloads a pdf file and open it, but is not working in iOS.

All the configuration that I read in https://github.com/fluttercommunity/flutter_downloader is correct.

Flutter doctor is OK.

Below I show you parts of the code

main.dart

final _prefs = SharedPreferences();

void main() async {
  WidgetsFlutterBinding.ensureInitialized(); 
  final prefs = SharedPreferences();
  await prefs.initPrefs();
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
    DeviceOrientation.portraitDown
  ]);
  WidgetsFlutterBinding.ensureInitialized();
  await FlutterDownloader.initialize(debug: true);
  _prefs.uid = await getId();
  runApp(MyApp());
}

pages/registry/facture.dart

List<Widget> _actionsCreateBar(BuildContext context) {
  return <Widget>[
    (document.id != null)
        ? IconButton(
            icon: Icon(EvaIcons.downloadOutline),
            onPressed: () async {
              _downloadAction(); // This method is executed when user press download icon
            },
            color: primaryColor,
            iconSize: 25,
          )
        : Container(),
  ];
}

void _downloadAction() async {
  if (await utils.isInternetAvailable()) {
    if (await _validateUrlRideBeforeDownload()) {
      await _pdfBloc.downloadPdf(document.url_ride, Theme.of(context).platform);
      setState(() {});
      return;
    }
    _showDialogOk(
        context, 'Download', 'Wait please');
  } else {
    _showDialogOk(context, 'Info',
        'No conection');
  }
}

bloc/pdf/pdfbloc.dart

class PdfBloc {

  final _downloadingController    = BehaviorSubject<bool>();
  final _loadingController        = BehaviorSubject<bool>();
  final _progressStringController = BehaviorSubject<String>();
  final _pdfProvider              = DownloadProvider();
  
  
  Stream<String> get progressStringStream => _progressStringController.stream;
  Stream<bool> get loadingStream => _loadingController.stream;
  Stream<bool> get downloadingStream => _downloadingController.stream;

  Future<ResponseData> downloadPdf(String url, var platform) async {
    _downloadingController.sink.add(true);
    ResponseData resData = await _pdfProvider.downloadPdf(url, _progressStringController, platform);
    _downloadingController.sink.add(false);
    return resData;
  }

  dispose() {
    _downloadingController.close();
    _progressStringController.close();
    _loadingController.close();
  }
}

provider/download/downloadprovider.dart

class DownloadProvider {

  Future<ResponseData> downloadPdf(String url, dynamic progressString, var platform) async {
  ResponseData resData = ResponseData();
  final _prefs = SharedPreferences();
    try {
      var path = await findLocalPath(platform) + '/';
      FlutterDownloader.cancelAll();
      final String taskId = await FlutterDownloader.enqueue(
        url: url,
        savedDir: path,
        showNotification: true, // show download progress in status bar (for Android)
        openFileFromNotification: true, // click on notification to open downloaded file (for Android)
        headers: {HttpHeaders.authorizationHeader: _prefs.token, 'Content-type': 'application/json'},
      );

      // Last developer used this "while" to wait while a dialog is shown
      // Android behaviour: flutter says "only success task can be opened" but then it works
      // iOS behaviour: flutter says "only success task can be opened" infinitely and never 
      // shows the pdf

      // In iOS this loop iterates forever
      while(!await FlutterDownloader.open(taskId: taskId,)) {
        // Last developer did this validation, but I don't know why
        if (platform == TargetPlatform.iOS) {
          await FlutterDownloader.open(taskId: taskId);
        }
      }
      _setResponseData(resData, 'Completed', false);
      return resData;
    } catch(e) {
      _setResponseData(resData, 'Error', true);
      return resData;
    }
  }

  _setResponseData(ResponseData resData, String message, bool state) {
    resData.setData(message);
    resData.setError(state);
  }

}
Future<String> findLocalPath(var platform) async {
  final directory = platform == TargetPlatform.android
      ? await getExternalStorageDirectory()
      : await getApplicationDocumentsDirectory();
  return directory.path;
}

I have tried several versions of ios and iphone without success.

Any ideas? Please help me, I'm stuck.

Thanks.

2 Answers

I could to solve this problem. The previous developers committed a bad programming practice, which caused a race condition in ios when trying to force open a task without checking its status.

I had to change the "while" loop and within it, check the status and progress of the download task. Once it reached 100% progress and its status was complete, then we break the loop and finally open the task.

In provider/download/downloadprovider.dart

  bool waitTask = true;
  while(waitTask) {
    String query = "SELECT * FROM task WHERE task_id='" + taskId + "'";
    var _tasks = await FlutterDownloader.loadTasksWithRawQuery(query: query);
    String taskStatus = _tasks[0].status.toString();
    int taskProgress = _tasks[0].progress;
    if(taskStatus == "DownloadTaskStatus(3)" && taskProgress == 100){ 
      waitTask = false;
    }
  }

  await FlutterDownloader.open(taskId: taskId);

open your ios project in Xcode

enter image description here

Add sqlite library.

enter image description here

enter image description here

Configure AppDelegate:

/// AppDelegate.h
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>

@interface AppDelegate : FlutterAppDelegate

@end

// AppDelegate.m
#include "AppDelegate.h"
#include "GeneratedPluginRegistrant.h"
#include "FlutterDownloaderPlugin.h"

@implementation AppDelegate

void registerPlugins(NSObject<FlutterPluginRegistry>* registry) {   
  if (![registry hasPlugin:@"FlutterDownloaderPlugin"]) {
     [FlutterDownloaderPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterDownloaderPlugin"]];
  }
}

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [GeneratedPluginRegistrant registerWithRegistry:self];
  [FlutterDownloaderPlugin setPluginRegistrantCallback:registerPlugins];
  // Override point for customization after application launch.
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

@end

Completely disable ATS: (add following codes to your Info.plist file)

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key><true/>
</dict>

Configure maximum number of concurrent tasks: the plugin allows 3 download tasks running at a moment by default (if you enqueue more than 3 tasks, there're only 3 tasks running, other tasks are put in pending state). You can change this number by adding following codes to your Info.plist file.

<!-- changes this number to configure the maximum number of concurrent tasks -->
<key>FDMaximumConcurrentTasks</key>
<integer>5</integer>

Localize notification messages: the plugin will send a notification message to notify user in case all files are downloaded while your application is not running in foreground. This message is English by default. You can localize this message by adding and localizing following message in Info.plist file. (you can find the detail of Info.plist localization in this link)

 <key>FDAllFilesDownloadedMessage</key>
<string>All files have been downloaded</string>
Related