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.
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
Add sqlite library.
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
I use this package: https://pub.dev/packages/connectivity_plus
I have a finished application that is working on Android but when I am testing it on iOS it shows that there is no internet. I can use and open pages in Safari so there is definitely one. But the following code returns false in iOS:
class InternetConnectivity with ChangeNotifier {
StreamSubscription<ConnectivityResult>? _subscription;
bool haveInternet = false;
void checkConnectivity() {
if (_subscription == null) {
_subscription = Connectivity().onConnectivityChanged.listen((ConnectivityResult result) {
bool res = result == ConnectivityResult.mobile || result == ConnectivityResult.wifi;
setHaveInternet = res;
});
}
}
set setHaveInternet(bool value) {
if (haveInternet != value) {
haveInternet = value;
notifyListeners();
}
}
}
I don't get any errors so I don't really know where to look for the problem.
On the screen where it checks that internet connection starts with this:
bool _haveInternet = true;
then in initState() I set the value of it:
#override
void initState() {
super.initState();
InternetConnectivity ? _internetConnectivity = InternetConnectivity();
setState(() {
_haveInternet = _internetConnectivity!.haveInternet;
});
After the initState() ran, the _haveInternet becomes false, so the connectivity_plus package returns false while normally it should be true.
Thanks in advance.
The package has a bug. According to documentation it should only affect iOS simulator. https://github.com/fluttercommunity/plus_plugins/issues/479
From package comments:
/// On iOS, the connectivity status might not update when WiFi
/// status changes, this is a known issue that only affects simulators.
/// For details see https://github.com/fluttercommunity/plus_plugins/issues/479.
I have the following code in my application:
For creating the dynamic link I have:
/// Function that creates a DynamicLink for a [SingleFashionItem] using [FirebaseDynamicLinks]
static Future<String> createFashionItemLink(
BuildContext context,
SingleFashionItem fashionItem,
) async {
// Get the package information, this function won't probably fail,
// therefore don't any errors
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
final DynamicLinkParameters parameters = DynamicLinkParameters(
uriPrefix: CUSTOM_DOMAIN,
link: Uri.parse(
"$CUSTOM_DOMAIN/item?parentID=${fashionItem.parentID}&objectID=${fashionItem.objectID}"),
androidParameters: AndroidParameters(
packageName: packageInfo.packageName,
),
iosParameters: IosParameters(
bundleId: packageInfo.packageName,
appStoreId: "123456789" // TODO change this AppStoreID
),
socialMetaTagParameters: SocialMetaTagParameters(
title:
"${AppLocalizations.of(context).translate("check_out")} ${fashionItem.name}",
description: fashionItem.description,
imageUrl: Uri.parse(fashionItem.images[0].url),
),
);
// Build a short link
return parameters
.buildShortLink()
.then((shortDynamicLink) => shortDynamicLink.shortUrl.toString())
.catchError((error) {
print("The error is: ${error.toString()}");
return null;
});
}
// Function that handles the Dynamic Link using [FirebaseDynamicLinks]
static void handleDynamicLink(BuildContext context) async {
// Get the initial dynamic link if the app is started using the link
final PendingDynamicLinkData data =
await FirebaseDynamicLinks.instance.getInitialLink();
print("The dynamic data is ${data.toString()}");
// Handle the dynamic link
_handleDynamicLink(context, data);
// Handle when the application is brought to the foreground when being in background
FirebaseDynamicLinks.instance.onLink(
onSuccess: (PendingDynamicLinkData data) async {
_handleDynamicLink(context, data);
}, onError: (OnLinkErrorException error) async {
// Send the error to Crashlytics
FirebaseCrashlytics.instance.reportNonFatalError(
exception: error,
customMessage: "Error while handling the dynamic link: ${data.link}",
userID: "not-available",
);
});
And for handling the dynamic link:
/// Function that handles the incoming [PendingDynamicLinkData]'s deep link
static void _handleDynamicLink(
BuildContext context, PendingDynamicLinkData data) {
final Uri deepLink = data?.link;
print("The deep link is: ${deepLink.toString()}");
// Check that the deep link is null or not
if (deepLink != null) {
// Handle deepLink
var isItem = deepLink.pathSegments.contains("item");
// Check if the items path segment is contained inside the path segment
if (isItem) {
// The deep link contains the items path segment
final objectID = deepLink.queryParameters["objectID"];
final parentID = deepLink.queryParameters["parentID"];
// Check that the itemID string is not null
if (objectID != null && parentID != null)
_goToDetailedFashionItemScreen(
context,
selectedFashionItemID: objectID,
parentID: parentID,
);
}
}
}
-> I've added in my
Associated Domains applinks:example.com
URL schemes com.example.application
In the Info.plist
<key>FirebaseDynamicLinksCustomDomains</key>
<array>
<string>https://example.com/item</string>
</array>
What I'm getting in a null value from the:
final Uri deepLink = data?.link
From what I've seen in this Github issue it might be because of a race condition, however this doesn't work for me.
This is the only solution that worked for me.
You should use the package "app_links" besides "firebase_dynamic_links".
Here is my code that worked for IOS:
static Future<void> initDynamicLinkIOS(BuildContext context) async {
final appLinks = AppLinks();
final Uri? uri = await appLinks.getInitialAppLink();
if (uri != null) {
final PendingDynamicLinkData? data = await FirebaseDynamicLinks.instance.getDynamicLink(uri);
Uri? deepLink = data?.link;
if (deepLink != null) {
// do your logic here, like navigation to a specific screen
}
}
}
I'm building my first Flutter application and I've run into a bit of an async issue.
When my application executes I'd like it to ask for permissions and wait until they are granted. My main() function looks like this:
import 'permission_manager.dart' as Perm_Manager;
void main() async
{
//Ensure valid permissions
Perm_Manager.Permission_Manager pm = Perm_Manager.Permission_Manager();
var res = await pm.get_permissions();
print(res);
return runApp(MyApp());
}
The Permission Manager class' get_permissions() function uses the Flutter Simple Permissions package to check and ask for permissions.
import 'package:simple_permissions/simple_permissions.dart';
import 'dart:io' as IO;
import 'dart:async';
class Permission_Manager {
/* Get user permissions */
Future<bool> get_permissions() async
{
//Android handler
if (IO.Platform.isAndroid)
{
//Check for read permissions
SimplePermissions.checkPermission(Permission.ReadExternalStorage).then((result)
{
//If granted
if (result)
return true;
//Otherwise request them
else
{
SimplePermissions.requestPermission(Permission.ReadExternalStorage)
.then((result)
{
// Determine if they were granted
if (result == PermissionStatus.authorized)
return true;
else
IO.exit(0); //TODO - display a message
});
}
});
}
else
return true;
}
}
When I run the application it does not wait for the function to complete as intended and prints the value of "res" before the Future is updated.
Launching lib\main.dart on Android SDK built for x86 in debug mode...
Built build\app\outputs\apk\debug\app-debug.apk.
I/SimplePermission(15066): Checking permission : android.permission.READ_EXTERNAL_STORAGE
I/flutter (15066): null
I/SimplePermission(15066): Requesting permission : android.permission.READ_EXTERNAL_STORAGE
The Future returns a value midway through the function! Does anyone know what I'm doing wrong?
To await something you have to call the await keyword on a future instead of .then
final result = await future;
// do something
instead of
future.then((result) {
// do something
});
If you really want to use .then then you can await the generated future:
await future.then((result) {
// do something
});
Just ensure that when using nested asynchronous calls that the async keyword is used on each:
await future.then((result) async{
// do something
await future.then((result_2) {
// do something else
});
});
Got it working. The issue seems to be resolved using a Completer:
import 'package:simple_permissions/simple_permissions.dart';
import 'dart:io' as IO;
import 'dart:async';
class Permission_Manager {
/* Get user permissions */
final Completer c = new Completer();
Future get_permissions() async
{
//Android handler
if (IO.Platform.isAndroid)
{
//Check for read permissions
SimplePermissions.checkPermission(Permission.ReadExternalStorage).then((result)
{
//If granted
if (result)
{
c.complete(true);
}
//Otherwise request them
else
{
SimplePermissions.requestPermission(Permission.ReadExternalStorage).then((result)
{
// Determine if they were granted
if (result == PermissionStatus.authorized)
{
c.complete(true);
}
else
{
IO.exit(0); //TODO - display a message
}
});
}
});
}
else
{
c.complete(true);
}
return c.future;
}
}
I want my splash screen to always appear in my application and it does which is great, but I have a walk through after the splash screen and I want it to be a one time walk through, So i want to add an integer to the shared preferences with a value of 0 and everytime I open the splash screen the value is incremented by one, so when "number" equals 1 or greater at the second run the splash screen skips the walkthrough and goes to home , here is the code that I want to edit now :
void initState() {
// TODO: implement initState
super.initState();
Timer(Duration(seconds: 5), () => MyNavigator.goToIntro(context));
}
And I want it to be like :
void initState() {
// TODO: implement initState
super.initState();int number=0;//this is in the shared prefs
Timer(Duration(seconds: 5), () => if(number==0){MyNavigator.goToIntro(context));
}else{MyNavigator.goToHome(context));
number++;}
}
The below code prints perfectly as we expect(during first launch only, "First launch"). You can use your navigation logic instead of print.
#override
void initState() {
super.initState();
setValue();
}
void setValue() async {
final prefs = await SharedPreferences.getInstance();
int launchCount = prefs.getInt('counter') ?? 0;
prefs.setInt('counter', launchCount + 1);
if (launchCount == 0) {
print("first launch"); //setState to refresh or move to some other page
} else {
print("Not first launch");
}
}
We need to have the number value to be saved across multiple app launches. We can use shared_preference plugin to achieve this.
secondly, getData that saved in our device.
Future<bool> getSaveData() async {
SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
bool isIntroScreenOpenedBefore =
sharedPreferences.getBool("isIntroScreenOpened") ?? false;
print(sharedPreferences.containsKey("isIntroScreenOpened")); // check your key either it is save or not?
if (isIntroScreenOpenedBefore == true) {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return LoginBoard();
}));
} else {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return WalKThroughScreen();
}));
}
return isIntroScreenOpenedBefore;
}
at first, let's save the data as boolean
Future<void> saveData() async {
SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
bool isIntroScreenOpened = true;
sharedPreferences.setBool("isIntroScreenOpened", isIntroScreenOpened); // saved data to your device.
}
Answer by #Dinesh Balasubramanian is works really fine.
But I have 4 initial screen that need to show once. I have done that using same logic in each screen. and then my app was showing 5th screen second time like fast forwarding all the previous screen and stopping on 5th screen.
To resolve this I am getting all the set Preferences at main.dart to open directly 5th screen. but when I do that I am having this problem,
"E/flutter (32606): [ERROR:flutter/lib/ui/ui_dart_state.cc(186)]
Unhandled Exception: Navigator operation requested with a context that
does not include a Navigator.
E/flutter (32606): The context used to
push or pop routes from the Navigator must be that of a widget that is
a descendant of a Navigator widget."
Here is code to switch from main.dart:
int firstLogin, firstMobile, firstOtp, firstInfo;
void setValue() async {
final prefs = await SharedPreferences.getInstance();
firstLogin = prefs.getInt('counterLogin') ?? 0;
firstMobile = prefs.getInt('counterMobile') ?? 0;
firstOtp = prefs.getInt('counterOtp') ?? 0;
firstInfo = prefs.getInt('counterInfo') ?? 0;
prefs.setInt('counterLogin', firstLogin + 1);
prefs.setInt('counterMobile', firstMobile + 1);
prefs.setInt('counterOtp', firstOtp + 1);
prefs.setInt('counterInfo', firstInfo + 1);
if ((firstLogin == 0) && (firstMobile == 0) && (firstOtp == 0) && (firstInfo == 0)) {
setState(() {
print("first launch");
Navigator.of(context).pushNamed(LoginScreen.routeName);
});
} else {
setState(() {
print("not first launch");
Navigator.of(context).pushNamed(LandingSection.routeName);
});
}
}
And calling the setValue() in initState()
I am looking forward for solution.
I want to show All pdf files present in internal as well as external storage, So on tapping that particular file, i want to open that file in full screen dialog.
So in order to do that you need to:
Grant access to external storage in a directory where there are your PDF file. Let's call that folder <external storage>/pdf.
List all file of that directory a display them to the user.
Open the selected file with an application that can visualize PDF.
In order to do all that thinks I suggest you to use those flutter packages:
path_provider
simple_permission
With path_provider you can get the external storage directory of an Android device.
Directory extDir = await getExternalStorageDirectory();
String pdfPath = extDir + "/pdf/";
In order to access external storage you need to set this permission request in the ApplicationManifest.xml:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
You could also only use READ_EXTERNAL_STORAGE but then the simple_permission plugin won't work.
With the simple_permission plugin then you go and ask user to be granted external storage access:
bool externalStoragePermissionOkay = false;
_checkPermissions() async {
if (Platform.isAndroid) {
SimplePermissions
.checkPermission(Permission.WriteExternalStorage)
.then((checkOkay) {
if (!checkOkay) {
SimplePermissions
.requestPermission(Permission.WriteExternalStorage)
.then((okDone) {
if (okDone) {
debugPrint("${okDone}");
setState(() {
externalStoragePermissionOkay = okDone;
debugPrint('Refresh UI');
});
}
});
} else {
setState(() {
externalStoragePermissionOkay = checkOkay;
});
}
});
}
}
Once we have been granted external storage access we an list our PDF directory:
List<FileSystemEntity> _files;
_files = dir.listSync(recursive: true, followLinks: false);
And show them in a ListView:
return new ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: _files.length,
itemBuilder: (context, i) {
return _buildRow(_files.elementAt(i).path);
});
Than you have to open them with a viewer when the user tap on them.
To do that there isn't an easy way, because with Android we need to build a ContentUri and give access to this URI to the exteranl application viewer.
So we do that in Android and we use flutter platform channels to call the Android native code.
Dart:
static const platform =
const MethodChannel('it.versionestabile.flutterapp000001/pdfViewer');
var args = {'url': fileName};
platform.invokeMethod('viewPdf', args);
Native Java Code:
public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "it.versionestabile.flutterapp000001/pdfViewer";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
new MethodChannel.MethodCallHandler() {
#Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
if (call.method.equals("viewPdf")) {
if (call.hasArgument("url")) {
String url = call.argument("url");
File file = new File(url);
//*
Uri photoURI = FileProvider.getUriForFile(MainActivity.this,
BuildConfig.APPLICATION_ID + ".provider",
file);
//*/
Intent target = new Intent(Intent.ACTION_VIEW);
target.setDataAndType(photoURI,"application/pdf");
target.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
target.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(target);
result.success(null);
}
} else {
result.notImplemented();
}
}
});
}
}
And after all we can have our PDF list and viewable on Android.
You have a lot to study. I hope this could be an useful playground for you.
This is for External Storage, but you can get Also the Internal and Temporary directory and act similarly as here.
If you wanna do the same thing on iOS you need to create the same Native Code pdfViewer also on iOS project. Refer alway to flutter platform channels in order to do it. And remember that the external storage doesn't exists on iOS devices. So you could use only the application sandbox document folder or the temporary one.
GitHub repo.
Happy coding.
i use this code for list files and directories
Future<List<FileSystemEntity>> dirContents(Directory dir) {
var files = <FileSystemEntity>[];
var completer = Completer<List<FileSystemEntity>>();
var lister = dir.list(recursive: false);
lister.listen((file) async {
FileStat f = file.statSync();
if (f.type == FileSystemEntityType.directory) {
await dirContents(Directory(file.uri.toFilePath()));
} else if (f.type == FileSystemEntityType.file && file.path.endsWith('.pdf')) {
_files.add(file);
}
}, onDone: () {
completer.complete(files);
setState(() {
//
});
});
return completer.future;
}
Directory dir = Directory('/storage/emulated/0');
var files = await dirContents(dir);
print(files);
Here is my code to list files from the download folder
List<dynamic> filesList = [];
Future listDir() async {
Directory dir = Directory(
'/storage/emulated/0/Download');
await for (FileSystemEntity entity
in dir.list(recursive: true, followLinks: false)) {
FileSystemEntityType type = await FileSystemEntity.type(entity.path);
if (type == FileSystemEntityType.file &&
entity.path.endsWith('.pdf')) {
filesList.add(entity.path);
}
}
return filesList;}