How can we handle deep links in flutter with Get X for go to Custom pages of the application?
Asked Answered
P

3

5

How can we handle deep links in flutter with Get X for go to Custom pages of the application ?

By default, by adding the desired address to the Android Manifest file:

        <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="flutterbooksample.com" />
            <data android:scheme="https" android:host="flutterbooksample.com"/>
        </intent-filter>

when the application opens, the main page of application will be displayed to us.

I am looking to implement this I can direct the user to any page of the application that is needed. A practical example is to pay on the web page and return to the application. When we return, we should show the user a message about the status of the payment, not direct the user to the first page of the application.

Postfree answered 30/8, 2021 at 12:19 Comment(5)
Did you find a solution for this?Tisza
Unfortunately no @TiszaPostfree
Ok, Thanks for the responseTisza
Hi, @Tisza here is an answer.Postfree
Thanks, @Huseyn, I will try this out.Tisza
F
1

Complete solution example using https://pub.dev/packages/app_links.

Let's assume we have three screens, Home, ProductList, and Product which we use to display many different products.

Main method:

void main() async{
  var firstScreen =  await DeepLinkParser().getFirstScreen();
  runApp( MainApp(firstScreen: firstScreen));
} 

We got firstScreen widget using DeepLinkParser.

class DeepLinkParser {
  DeepLinkParser._();
  static final _instance = DeepLinkParser._();
  factory DeepLinkParser() => _instance;

  final _appLinks = AppLinks();

  Future<Uri?> getInitialLink() async {
    return _appLinks.getInitialAppLink();
  }

  Future <Widget> getFirstScreen() async {
    Uri? uri = await getInitialLink();
    if (uri == null){
      return const Home();
    }

    String fragment = uri.fragment;
    if (fragment.contains('/product-list')){
      return const ProductList();
    }

    if (fragment.contains('/product/')){
      var lastIndexOfSlash = fragment.lastIndexOf('/');
      if (lastIndexOfSlash == fragment.length - 1){
        return const ProductList();
      }
      String id = fragment.substring(lastIndexOfSlash + 1);
      return ProductScreen.withId(id: id);
    }

    return const Home();
  }
}

MainApp:

class MainApp extends StatelessWidget {
  final Widget firstScreen;
   const MainApp({super.key, required this.firstScreen});

  @override
  Widget build(BuildContext context)  {
    return GetMaterialApp(
      home:  firstScreen,
      getPages: Routes.routes,
      navigatorObservers: [NavigationHistoryObserver()],
      debugShowCheckedModeBanner: false,
    );
  }
}

ProductScreen has two constructors:

class ProductScreen extends StatelessWidget {
  String? id;
  ProductScreen.withId({super.key, required this.id});

  ProductScreen({super.key});
@override
  Widget build(BuildContext context) {
    id ??= Get.parameters['id'];
    Product product = MockProductService().getById(id!)!;
    return Scaffold(
      appBar: AppBar(
        title:  Text('Product ${product.id}'),
        centerTitle: true,
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
             //skipped Image and Text widgets with product info
          ],
        ),
      ),
    );
  }
}

When we call ProductScreen from ProductList we use the default constructor (without id parameter) and we pass id like below:

onTap: () {                     
   Get.toNamed('${Routes.PRODUCT}/${products[index].id}');
}

For that to work, we need to create named routes:

class Routes {
  static const HOME = '/';
  static const PRODUCT_LIST = '/product-list';
  static const PRODUCT = '/product';

  static final routes = [
    GetPage(
      name: HOME,
      page: () => const Home(),
      transition: Transition.circularReveal,
    ),
    GetPage(
      name: PRODUCT_LIST,
      page: () => const ProductList(),
      transition: Transition.circularReveal,
    ),
    GetPage(
      name: '$PRODUCT/:id',
      page: () =>  ProductScreen(),
      transition: Transition.circularReveal,
      preventDuplicates: false,
    ),
  ];
}

It works with one problem: when the app is launched from the deep link https://example.com/#/product/123 Get pushes three routes into the stack:

/
/product
/product/123

despite I expect only the last one. It creates some problems with back button behavior, which I currently resolve with a lot of if statements.

UPDATE.

I recently upgraded my projects to GetX 5 (release candidate at the time). GetX 5 always uses Navigator 2.0 internally and supports deep links out of the box. Breaking changes are relatively few. So, I advice anybody interested in the web platform upgrade as well.

Fairfax answered 8/6, 2024 at 9:56 Comment(0)
H
4

Do the page routing in your app, when you process the deeplink.

In my apps, I usually use uni_links (https://pub.dev/packages/uni_links) and in my main.dart, I have something like this:

StreamSubscription<String> _subUniLinks;

@override
initState() {
    super.initState();
    // universal link setup
    initUniLinks();
}

Future<void> initUniLinks() async {
    try {
        final String initialLink = await getInitialLink();
        await processLink(initialLink);
    } on PlatformException {
    }
    _subUniLinks = linkStream.listen(processLink, onError: (err) {});
}

processLink(String link) async {
    // parse link and decide what to do:
    // - use setState
    // - or use Navigator.pushReplacement
    // - or whatever mechanism if you have in your app for routing to a page
}

@override
dispose() {
    if (_subUniLinks != null) _subUniLinks.cancel();
    super.dispose();
}


Hobard answered 15/12, 2021 at 4:5 Comment(2)
where this linkStream come from ?Stenotype
old question but linkSream comes from the same package: uni_linksSociable
P
3

my method is alike as Didier Prophete answer, but I use this in singleTon class, call in main.dart (above return MyApp line)

await DeepLinkService().initialDeepLinks();

but I don't know how to cancel and dispose the StreamSubscription

StreamSubscription? _sub;

Future<void> initialDeepLinks() async {
    try {
      //init deeplink when start from inactive
      final initialLink = await getInitialLink();
      if (initialLink != null) {
        await _handleDeepLink(initialLink);
      }

      //Check deeplink in foreground/background
      _sub = linkStream.listen((String? link) async {
        if (link != null) {
          await _handleDeepLink(link);
        }
      }, onError: (e) {
        DebugLog().show(e.toString());
      });
    } catch (e) {
      DebugLog().show(e.toString());
    }
  }

  // Handle the deep link
  Future<void> _handleDeepLink(String link) async {
    Uri deepLink = Uri.parse(link);
    String path = deepLink.path;
    DebugLog().show('open app from deeplink: $deepLink with path: $path');

    //Switch the path then navigate to destinate page
  }
Presurmise answered 29/11, 2023 at 3:55 Comment(0)
F
1

Complete solution example using https://pub.dev/packages/app_links.

Let's assume we have three screens, Home, ProductList, and Product which we use to display many different products.

Main method:

void main() async{
  var firstScreen =  await DeepLinkParser().getFirstScreen();
  runApp( MainApp(firstScreen: firstScreen));
} 

We got firstScreen widget using DeepLinkParser.

class DeepLinkParser {
  DeepLinkParser._();
  static final _instance = DeepLinkParser._();
  factory DeepLinkParser() => _instance;

  final _appLinks = AppLinks();

  Future<Uri?> getInitialLink() async {
    return _appLinks.getInitialAppLink();
  }

  Future <Widget> getFirstScreen() async {
    Uri? uri = await getInitialLink();
    if (uri == null){
      return const Home();
    }

    String fragment = uri.fragment;
    if (fragment.contains('/product-list')){
      return const ProductList();
    }

    if (fragment.contains('/product/')){
      var lastIndexOfSlash = fragment.lastIndexOf('/');
      if (lastIndexOfSlash == fragment.length - 1){
        return const ProductList();
      }
      String id = fragment.substring(lastIndexOfSlash + 1);
      return ProductScreen.withId(id: id);
    }

    return const Home();
  }
}

MainApp:

class MainApp extends StatelessWidget {
  final Widget firstScreen;
   const MainApp({super.key, required this.firstScreen});

  @override
  Widget build(BuildContext context)  {
    return GetMaterialApp(
      home:  firstScreen,
      getPages: Routes.routes,
      navigatorObservers: [NavigationHistoryObserver()],
      debugShowCheckedModeBanner: false,
    );
  }
}

ProductScreen has two constructors:

class ProductScreen extends StatelessWidget {
  String? id;
  ProductScreen.withId({super.key, required this.id});

  ProductScreen({super.key});
@override
  Widget build(BuildContext context) {
    id ??= Get.parameters['id'];
    Product product = MockProductService().getById(id!)!;
    return Scaffold(
      appBar: AppBar(
        title:  Text('Product ${product.id}'),
        centerTitle: true,
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
             //skipped Image and Text widgets with product info
          ],
        ),
      ),
    );
  }
}

When we call ProductScreen from ProductList we use the default constructor (without id parameter) and we pass id like below:

onTap: () {                     
   Get.toNamed('${Routes.PRODUCT}/${products[index].id}');
}

For that to work, we need to create named routes:

class Routes {
  static const HOME = '/';
  static const PRODUCT_LIST = '/product-list';
  static const PRODUCT = '/product';

  static final routes = [
    GetPage(
      name: HOME,
      page: () => const Home(),
      transition: Transition.circularReveal,
    ),
    GetPage(
      name: PRODUCT_LIST,
      page: () => const ProductList(),
      transition: Transition.circularReveal,
    ),
    GetPage(
      name: '$PRODUCT/:id',
      page: () =>  ProductScreen(),
      transition: Transition.circularReveal,
      preventDuplicates: false,
    ),
  ];
}

It works with one problem: when the app is launched from the deep link https://example.com/#/product/123 Get pushes three routes into the stack:

/
/product
/product/123

despite I expect only the last one. It creates some problems with back button behavior, which I currently resolve with a lot of if statements.

UPDATE.

I recently upgraded my projects to GetX 5 (release candidate at the time). GetX 5 always uses Navigator 2.0 internally and supports deep links out of the box. Breaking changes are relatively few. So, I advice anybody interested in the web platform upgrade as well.

Fairfax answered 8/6, 2024 at 9:56 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.