7

The getInitialLink() continues to give a valid dynamic link after the first retrieval. This leads to a loop where a user is continuously forced into a page associated with the deep link.

Running `flutter doctor --verbose` produced the below results.

[✓] Flutter (Channel stable, 1.20.4, on Mac OS X 10.15.6 19G2021, locale en-US)
    • Flutter version 1.20.4 at /Users/sjsam/Documents/Developement/flutter
    • Framework revision fba99f6cf9 (5 days ago), 2020-09-14 15:32:52 -0700
    • Engine revision d1bc06f032
    • Dart version 2.9.2

 
[✓] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
    • Android SDK at /Users/sjsam/Library/Android/sdk
    • Platform android-29, build-tools 28.0.3
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_212-release-1586-b4-5784211)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 11.7)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 11.7, Build version 11E801a
    • CocoaPods version 1.9.0

[✓] Android Studio (version 3.6)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin version 44.0.2
    • Dart plugin version 192.7761
    • Java version OpenJDK Runtime Environment (build 1.8.0_212-release-1586-b4-5784211)

[✓] Connected device (2 available)
    • AOSP on IA Emulator (mobile)       • emulator-5554 • android-x86 • Android 9 (API 28) (emulator) # No issues here
    • Android SDK built for x86 (mobile) • emulator-5556 • android-x86 • Android 8.0.0 (API 26) (emulator) # Issue happens here

• No issues found!

My code sample (which is mostly of a copy of the code given in the documentation) is given below.

 @override
  void initState() {
    super.initState();
    this.initDynamicLinks();
  }
  void initDynamicLinks() async {
    try {
      FirebaseDynamicLinks.instance.onLink(onSuccess: (PendingDynamicLinkData dynamicLink) async {
        var deepLink = dynamicLink?.link?.toString() ?? null;
        if (null != deepLink ) {
          var lastIndex = deepLink.lastIndexOf(RegExp(r'/'));
          assert(lastIndex != -1, "Problematic Link");
          var feedId = deepLink.substring(lastIndex + 1);
          // get the required data using feedID
          try {
            await Navigator.of(this.context).pushNamedAndRemoveUntil(
              DeliveryTakeOrder.page,
              ModalRoute.withName(PAGENAME),
              arguments: argumentsConstructor(/*Arguments Here*/),
            );
          } catch (e) {
            if (!kReleaseMode) {
              debugPrint(e.toString());
            }
          }
        }
      }, onError: (OnLinkErrorException e) async {
        print('onLinkError');
        print(e.message);
      });
    } catch (e, s) {
      print(s);
    }

   // await FirebaseDynamicLinks.instance.
      final PendingDynamicLinkData dynamicLink = await FirebaseDynamicLinks.instance.getInitialLink();

    var deepLink = dynamicLink?.link?.toString() ?? null;
    if (null != deepLink ) {
      // A bit of reverse engineering here
      var lastIndex = deepLink.lastIndexOf(RegExp(r'/'));
      assert(lastIndex != -1, "Problematic Link");
      var feedId = deepLink.substring(lastIndex + 1);
     // get the required data using feedID
      try {
        await Navigator.of(this.context).pushNamedAndRemoveUntil(
          DeliveryTakeOrder.page,
          ModalRoute.withName(PAGENAME),
          arguments: argumentsConstructor(/*Arguments Here*/),
        );
      } catch (e) {
        if (!kReleaseMode) {
          debugPrint(e.toString());
        }
      }
    }
  }

In the inline documentation, I see the below comments.

  /// Attempts to retrieve the dynamic link which launched the app.
  ///
  /// This method always returns a Future. That Future completes to null if
  /// there is no pending dynamic link or any call to this method after the
  /// the first attempt.
  Future<PendingDynamicLinkData> getInitialLink() async {
  //..
  }

So, on reaching getInitialLink() a second time, it should return a null which is not the case here. Any help is appreciated.

Note: This issue is observed until Android API Level 27.

sjsam
  • 21,411
  • 5
  • 55
  • 102
  • 1
    I've this issue on iOS 15.3.1, Flutter 2.10.0, firebase_dynamic_links: 4.0.8 as well – Pinolpier Mar 10 '22 at 20:48
  • A fix has been implemented for Android in version 4.0.6, released at February 10, 2022: This is a link to the relevant PullRequest: https://github.com/FirebaseExtended/flutterfire/commit/67cc66471046822463f326c05e732313dbaa9560 – Pinolpier Mar 10 '22 at 20:55
  • @Pinolpier Thank you for this info. But I am curious why you're getting the same error in 4.0.8 if a fix has been implemented in 4.0.6. This was really annoying at the time I was dealing with it. – sjsam Mar 11 '22 at 00:12
  • I only get the error on iOS and the linked PullRequest shows how the fix was implemented for Android only. I guess they are not aware of the error happening on iOS as well in certain circumstances. – Pinolpier Mar 12 '22 at 13:03

2 Answers2

4

As of now, this is an ongoing issue as I could confirm from here I managed to work around this (temporarily) using the shared preferences module.

Inside main :

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  SharedPreferences prefs = await SharedPreferences.getInstance();
  await prefs.setBool('just_opening', true);
  runApp(YourAppName());
}

Inside initState(), check for the persistent value just_opening, call initDynamicLinks and then set the just_opening attribute to false.

SharedPreferences.getInstance().then((prefs) async{
  if(prefs.getBool('just_opening')){
    this.initDynamicLinks();
  }
  await prefs.setBool('just_opening', false);
});

This certainly looks like an ugly hack, but it works on iOS and Android.

Edit

Looks like this issue is still in the wild as of March 13, 2022. A better alternative is to use a static variable to work around this as mentioned by @Pinolpier in this comment

sjsam
  • 21,411
  • 5
  • 55
  • 102
  • 1
    An alternative to using SharedPreferences could be to use a static variable of type bool. SharedPreferences are asynchronous and thus could fail if the value is being red very soon after it has been written. The relevant asynchronous gap here is not the actual process of saving the value but getting a sharedPreferences instance. The getInstance() method returns a Singleton future that (if awaited more than once at the same time before completion) could crash. If you are sure that the future has completed the process of storing values is synchronous as SharedPrefs buffer in an internal map. – Pinolpier Mar 12 '22 at 13:04
  • 1
    @Pinolpier : Oh! Noted. I will mention this in my answer. Thank you. – sjsam Mar 13 '22 at 12:12
1

Using a local variable

Here is an implemented solution inspired by @Pinolpier, with a listener on the dynamic link.

  bool isOpening = false; // the local bool

  @override
  Widget build(BuildContext context) {
    WidgetsBinding.instance?.addPostFrameCallback((_) {
      // * Open post on dynamic link event
      FirebaseDynamicLinks.instance.onLink.listen((event) async {

        print("listener event called"); // this is getting called multiple times
        
        if (isOpening == false){
          isOpening = true;
          //do your operation here
          await Future.delayed(Duration(seconds: 1));
          isOpening = false;
        }

      },
     );
    });
    return Center());
  }
Paul
  • 1,349
  • 1
  • 14
  • 26