androidflutterdartdeep-linkingflutter-deep-link

Flutter Deep linking is not working in cold state


i was doing deep linking for my flutter app. The dependency i am using is AppLinks. Everything is working perfectly fine for iOS but for android, it is only working when the app is running in the background. If we remove the app from the background, it's not getting opened or say not working. I am seeking a solution for that.

This is my flutter code below :-

`Future<void> initDeepLinks() async {
_appLinks = AppLinks();
final appLink = await _appLinks.getInitialAppLink();
if (appLink != null) {
refresh();

openAppLink(appLink);

print('getInitialAppLink: $appLink');
}
_linkSubscription = _appLinks.uriLinkStream.listen((uri) {
print('onAppLink: $uri');
update();
openAppLink(uri);
});
}

void openAppLink(Uri uri) {
if (uri.path.contains('activeOrder')) {
onBottomNavItemPressed(3);
Get.find<OrdersController>().tabController?.index = 0;
}
if (uri.path.contains('request')) {
onBottomNavItemPressed(3);
Get.find<OrdersController>().tabController?.index = 3;
}
if (uri.path.contains('orderReview')) {
Get.to(() => OrderReviewView());
}
if (uri.path.contains('payoutHistory')) {     
Get.to(() => TransactionHistory());
}
if (uri.path.contains('payoutDetails')) {
var transactionDetailsController =
Get.put(TransactionHistoryController());
//  print(uri.queryParameters['transaction_id']);
transactionDetailsController.getPaymentDataDetails(
trxId: uri.queryParameters['transaction_id'] ?? "");

Get.to(() => PayoutHistoryDetailPage());
}
}

`

This is the code for the same and working fine for iOS, but it's not working for Android.

Solution

  • have you added the deeplink configuration to the AndroidManifest.xml ? this is the way i'm using deeplink in my app and it works fine. first of all I use uni_links package:

    and for android in manifest i add the code below in activity tag:

    <activity
                android:name=".MainActivity"
                android:exported="true"
                android:launchMode="singleTop"
                android:theme="@style/LaunchTheme"
                android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
                android:hardwareAccelerated="true"
                android:windowSoftInputMode="adjustResize">
              "....the codes ...."
                <!-- Deep Links -->
                <intent-filter>
                    <action android:name="android.intent.action.VIEW" />
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.BROWSABLE" />
                    <!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
                    <data
                        android:scheme="exampleapp"
                        android:host="example"/>
                </intent-filter>
            </activity>
    
    

    as you see the link that opens your app should be like this, for example you are increasing your wallet and after payment the link is :

    exampleapp://example/payment?success
    

    see the code below how i use to back to the app:

    bool _initialURILinkHandledIncreaseBalance = false;
    
    
    class IncreaseBalanceModal extends StatefulWidget {
      const IncreaseBalanceModal({Key? key}) : super(key: key);
    
      @override
      State<IncreaseBalanceModal> createState() => _IncreaseBalanceModalState();
    }
    
    class _IncreaseBalanceModalState extends State<IncreaseBalanceModal> {
      StreamSubscription? _streamSubscription;
      TextEditingController amountTextController =
          MaskedTextController(mask: '0', length: 50);
    
    
      Future<void> _initURIHandler() async {
        if (!_initialURILinkHandledIncreaseBalance) {
          _initialURILinkHandledIncreaseBalance = true;
    
          try {
            final initialURI = await getInitialUri();
            // Use the initialURI and warn the user if it is not correct,
            // but keep in mind it could be `null`.
            if (initialURI != null) {
              debugPrint("Initial URI received $initialURI");
              if (!mounted) {
                return;
              }
            } else {
              debugPrint("Null Initial URI received");
            }
          } on PlatformException {
            // Platform messages may fail, so we use a try/catch PlatformException.
            // Handle exception by warning the user their action did not succeed
            debugPrint("Failed to receive initial uri");
          } on FormatException catch (err) {
            if (!mounted) {
              return;
            }
            debugPrint('Malformed Initial URI received $err');
          }
        }
      }
    
      /// Handle incoming links - the ones that the app will receive from the OS
      /// while already started.
      void _incomingLinkHandler() {
        if (!kIsWeb) {
          // It will handle app links while the app is already started - be it in
          // the foreground or in the background.
          _streamSubscription = uriLinkStream.listen((Uri? uri) {
            if (!mounted) {
              return;
            }
    
            debugPrint('Received URI hello we are home: $uri');
            var query = uri!.query;
            if(query == 'failed'){
              locator<NavigationService>().goBack();
              snackBar(S.of(context).transactionWasUnsuccessfulText, context,color: brandMainColor);
            }
            else if(query == 'success'){
              onTapBackToApp();
            }
          }, onError: (Object err) {
            if (!mounted) {
              return;
            }
            debugPrint('Error occurred: $err');
          });
        }
      }
    
    
      @override
      void initState() {
        super.initState();
        _initURIHandler();
        _incomingLinkHandler();
      }
    
    
      @override
      void dispose() {
        amountTextController.dispose();
        if (!kIsWeb) _streamSubscription!.cancel();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        var width = MediaQuery.of(context).size.width;
        return Consumer<UserProvider>(
            builder: (context,userProvider, child) {
              return SafeArea(
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      CustomAppBar(
                        title: S.of(context).increaseBalanceText,
                        hasBackButton: false,
                        color: primaryDark,
                      ),
                      Padding(
                        padding: EdgeInsets.symmetric(horizontal: width * 0.04),
                        child: Column(
                          children: [
                            SizedBox(
                              height: width * 0.04,
                            ),
                            TextFormFieldItem(
                                labelText: amountLabelText,
                                textController: amountTextController,
                                minHeight: width * 0.1333,
                                onChanged: (s) {
                                  setState(() {});
                                }
                            ),
                          ],
                        ),
                      ),
                      Padding(
                        padding: EdgeInsets.all(width * 0.04),
                        child: RectAngleButton(
                          state: ViewState.ready,
                          nameOfButton: S.of(context).submitText,
                          color: amountTextController.text.trim().isNotEmpty
                              ? brandMainColor
                              : secondaryDark,
                          height: width * 0.1493,
                          width: width,
                          onTap: amountTextController.text.trim().isNotEmpty
                              ? () async {
                            var url = await UserService.createPaymentByUser(
                                Provider.of(context, listen: false),
                                Provider.of(context, listen: false),
                                URLs.connectToGateMutation(), {
    
                                  "amount" : int.parse(amountTextController.text)
                            }
                            );
                            if (url.isNotEmpty) {
                              _launchURLBrowser(url);
                            } else {
                              snackBar(
                                  S
                                      .of(context)
                                      .somethingWentWrongPleaseTryAgainText,
                                  context);
                            }
                          }
                              : null,
                        ),
                      ),
                    ],
                  )
              );
            });
      }
    
      void onTapBackToApp() async {
        await UserService.getUser(Provider.of(context,listen: false), Provider.of(context,listen: false),Provider.of(context, listen: false),
            Provider.of(context,listen: false), URLs.meQuery(),false);
        locator<NavigationService>().goBack();
        snackBar(S.of(context).transactionWasSuccessfulText, context,color: greenColor);
      }
    
      _launchURLBrowser(String url) async {
        if (await canLaunch(url)) {
          await launch(url, forceSafariVC: !kIsWeb
              ? defaultTargetPlatform == TargetPlatform.iOS
              ? false
              : null
              : null,
              webOnlyWindowName: kIsWeb ? '_self' : '_blank');
        } else {
          throw 'Could not launch $url';
        }
      }
    
    }
    
    
    

    even if you close the app, the deeplink will open it again, and you can manage the navigation by the query of your link like what: from this link exampleapp://example/payment?success you can understand that it is coming to the app because of payment:

    var type = uri.pathSegments.first;//payment
    

    and the type tells you to navigate to payment page from the beginning.

    Edit: these explanation that I've said before is completely true, and when I checked your Manifest code I saw that you have multiple configs for your deeplink.

         <!-- Deep linking -->
                <meta-data android:name="flutter_deeplinking_enabled" android:value="true" />
    
                <intent-filter android:autoVerify="true">
                    <action android:name="android.intent.action.VIEW" />
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.BROWSABLE" />
                    <data android:scheme="http"  android:host="host.com" android:pathPrefix="/orderReview"/>
                    <data android:scheme="https" />
                </intent-filter>
    
                <intent-filter android:autoVerify="true">
                    <action android:name="android.intent.action.VIEW" />
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.BROWSABLE" />
                    <data android:scheme="http"  android:host="host.com" android:pathPrefix="/activeOrder"/>
                    <data android:scheme="https" />
                </intent-filter>
    
                 <intent-filter android:autoVerify="true">
                    <action android:name="android.intent.action.VIEW" />
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.BROWSABLE" />
                    <data android:scheme="http"  android:host="host.com" android:pathPrefix="/request"/>
                    <data android:scheme="https" />
                </intent-filter>
    
                 <intent-filter android:autoVerify="true">
                    <action android:name="android.intent.action.VIEW" />
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.BROWSABLE" />
                    <data android:scheme="http"  android:host="host.com" android:pathPrefix="/payoutHistory"/>
                    <data android:scheme="https" />
                </intent-filter>
    
                 <intent-filter android:autoVerify="true">
                    <action android:name="android.intent.action.VIEW" />
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.BROWSABLE" />
                    <data android:scheme="http"  android:host="host.com" android:pathPrefix="/payoutDetails"/>
                    <data android:scheme="https" />
                </intent-filter>
    
                <!-- <intent-filter android:autoVerify="true">
                    <action android:name="android.intent.action.VIEW" />
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.BROWSABLE" />
                    <data android:scheme="http" android:host="host.com" android:pathPrefix="/request"/>
                </intent-filter>
    
                <intent-filter android:autoVerify="true">
                    <action android:name="android.intent.action.VIEW" />
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.BROWSABLE" />
                    <data android:scheme="http" android:host="host.com" android:pathPrefix="/activeOrder"/>
                </intent-filter>
                <intent-filter android:autoVerify="true">
                    <action android:name="android.intent.action.VIEW" />
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.BROWSABLE" />
                    <data android:scheme="http" android:host="host.com" android:pathPrefix="/payoutHistory"/>
                </intent-filter>
                <intent-filter android:autoVerify="true">
                    <action android:name="android.intent.action.VIEW" />
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.BROWSABLE" />
                    <data android:scheme="http" android:host="host.com" android:pathPrefix="/payoutDetails"/>
                </intent-filter> 
    
                 <intent-filter android:autoVerify="true">
                    <action android:name="android.intent.action.VIEW" />
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.BROWSABLE" />
                    <data android:scheme="https" android:host="host.com" android:pathPrefix="/orderReview"/>
                </intent-filter> 
    
                 <intent-filter android:autoVerify="true">
                    <action android:name="android.intent.action.VIEW" />
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.BROWSABLE" />
                    <data android:scheme="https" android:host="host.com" android:pathPrefix="/request"/>
                </intent-filter>
    
                <intent-filter android:autoVerify="true">
                    <action android:name="android.intent.action.VIEW" />
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.BROWSABLE" />
                    <data android:scheme="https" android:host="host.com" android:pathPrefix="/activeOrder"/>
                </intent-filter>
                <intent-filter android:autoVerify="true">
                    <action android:name="android.intent.action.VIEW" />
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.BROWSABLE" />
                    <data android:scheme="https" android:host="host.com" android:pathPrefix="/payoutHistory"/>
                </intent-filter>
                <intent-filter android:autoVerify="true">
                    <action android:name="android.intent.action.VIEW" />
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.BROWSABLE" />
                    <data android:scheme="https" android:host="host.com" android:pathPrefix="/payoutDetails"/>
                </intent-filter> -->
    
    

    it is not recommended to do add all the configs with all the pathPrefix in your app, if you have just one pathPrefix it will be ok to do that but please delete those lines that you've added before and please add these lines below instead:

    <intent-filter>
                    <action android:name="android.intent.action.VIEW" />
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.BROWSABLE" />
                    <!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
                    <data
                        android:scheme="https"
                        android:host="host.app"/>
                </intent-filter>
    

    you should use main config in your manifest and check your pathPrefix in your Flutter code.

    happy coding...