شارك المقالة

شرح مكتبة app_links في Flutter لاستقبال Deep Link وApp Links من الروابط الخارجية

عند تطوير تطبيق Flutter قد تحتاج في بعض الحالات إلى فتح شاشة معينة داخل التطبيق مباشرة عند ضغط المستخدم على رابط موجود في WhatsApp أو Telegram أو البريد الإلكتروني أو المتصفح.

على سبيل المثال، لديك رابط دعوة مثل:

https://myapp.com/invitation/ABC123

عند ضغط المستخدم على هذا الرابط، يمكن أن يقوم النظام بفتح تطبيق Flutter مباشرة ثم يستقبل التطبيق الرابط ويقرأ منه قيمة ABC123 حتى يعرف أي دعوة يجب عرضها.

هذا النوع من الروابط يعرف باسم Deep Links، ومع استخدام روابط HTTPS المرتبطة بموقعك يمكن استخدام Android App Links وiOS Universal Links.

في هذا المقال سنتعرف بشكل كامل على مكتبة app_links، وكيفية تثبيتها، وكيفية إعداد Android وiOS، وكيفية إنشاء رابط من تطبيق Flutter، وكيفية استقبال الرابط داخل التطبيق، وكيفية استخراج البيانات من الرابط، بالإضافة إلى شرح العلاقة بين التطبيق والـ Backend وطريقة اختبار الروابط وحل أكثر المشاكل شيوعًا.

ما هي مكتبة app_links؟

مكتبة app_links هي Flutter Plugin للتعامل مع الروابط التي يمكن استخدامها لفتح التطبيق، وتدعم Android App Links وDeep Links على Android، وUniversal Links وCustom URL Schemes على iOS، بالإضافة إلى دعم عدد من منصات سطح المكتب والويب. :contentReference[oaicite:1]{index=1}

المكتبة لا تقوم بإنشاء نظام الروابط على الخادم، وإنما وظيفتها الأساسية هي استقبال الرابط الذي أرسله نظام التشغيل إلى التطبيق ثم توفيره لك على شكل Uri أو String.

وهذه نقطة مهمة جدًا:

app_links تستقبل الرابط ولا تقوم بإنشائه أو تقصيره أو تخزينه في الـ Backend.

أما إنشاء الرابط نفسه فيمكن أن يتم ببساطة من خلال Dart، أو من الـ Backend، حسب طبيعة المشروع وطريقة تخزين بيانات الرابط.

ما الفرق بين Deep Link وApp Link وUniversal Link؟

هناك عدة أسماء ستقابلها أثناء العمل مع الروابط داخل Flutter، ولذلك من المهم فهم الفرق بينها.

Deep Link

هو رابط يمكنه فتح شاشة أو محتوى معين داخل التطبيق بدلًا من فتح الصفحة الرئيسية فقط.

مثال:

myapp://invitation/ABC123

هنا myapp هو Custom Scheme خاص بالتطبيق.

Android App Links

هي روابط HTTPS مرتبطة بدومين حقيقي يملكه التطبيق أو الشركة، ويقوم Android بالتحقق من علاقة الموقع بالتطبيق باستخدام ملف assetlinks.json.

مثال:

https://myapp.com/invitation/ABC123

عند اكتمال الإعداد، يمكن للنظام توجيه الرابط إلى التطبيق مباشرة بدلًا من فتحه في المتصفح. Android يعتمد في هذه العملية على ملف Digital Asset Links الموجود في:

https://myapp.com/.well-known/assetlinks.json

ويتحقق من Package Name وبصمة شهادة SHA-256 الخاصة بالتطبيق. :contentReference[oaicite:2]{index=2}

iOS Universal Links

هي الطريقة المقابلة في نظام iOS، حيث يتم ربط التطبيق بدومين معين من خلال Associated Domains وملف:

apple-app-site-association

والذي يتم وضعه في:

https://myapp.com/.well-known/apple-app-site-association

ويحدد الملف التطبيقات المسموح لها بالتعامل مع مسارات الموقع. :contentReference[oaicite:3]{index=3}

مميزات مكتبة app_links

  • استقبال HTTPS App Links.
  • استقبال Deep Links باستخدام Custom Schemes.
  • دعم Android.
  • دعم iOS.
  • دعم Windows.
  • دعم macOS.
  • دعم Linux.
  • دعم Web مع توفير الرابط الأولي فقط.
  • استقبال الروابط من خلال Stream.
  • الحصول على الرابط الأول الذي فتح التطبيق.
  • الحصول على آخر رابط تم استقباله.
  • التعامل مع URI مباشرة بدلًا من تحليل String يدويًا.

توفر المكتبة حاليًا uriLinkStream وstringLinkStream بالإضافة إلى getInitialLink() وgetInitialLinkString() وgetLatestLink() وgetLatestLinkString(). :contentReference[oaicite:4]{index=4}

تثبيت مكتبة app_links

يمكن إضافة المكتبة إلى مشروع Flutter باستخدام:

flutter pub add app_links

أو إضافتها يدويًا إلى pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter

  app_links: ^7.2.1

ثم:

flutter pub get

الإصدار المشار إليه في صفحة الحزمة هو 7.2.1، ويتطلب Dart SDK 3.12 أو أحدث. :contentReference[oaicite:5]{index=5}

هل app_links تقوم بتوليد الرابط؟

لا.

هذه من أهم النقاط التي يجب فهمها قبل استخدام المكتبة.

لو كان لديك رابط:

https://myapp.com/invitation/ABC123

فالمكتبة لا تقوم بإنشاء ABC123 ولا تقوم بإنشاء الدومين ولا تقوم بحفظ الدعوة في قاعدة البيانات.

وظيفتها تبدأ عندما يقوم المستخدم بفتح هذا الرابط، وبعد أن يقوم Android أو iOS بتمرير الرابط إلى التطبيق، تستقبله المكتبة.

كيف أقوم بتوليد رابط في Flutter؟

يمكنك إنشاء رابط HTTPS من خلال Dart باستخدام Uri.https().

final invitationCode = 'ABC123';

final invitationUrl = Uri.https(
  'myapp.com',
  '/invitation/$invitationCode',
).toString();

print(invitationUrl);

الناتج سيكون:

https://myapp.com/invitation/ABC123

ويمكنك مشاركة هذا الرابط باستخدام أي مكتبة مشاركة أو باستخدام Share في Flutter.

هل الـ Backend ضروري لإنشاء الرابط؟

ليس بالضرورة.

من الناحية البرمجية تستطيع إنشاء String يمثل الرابط بالكامل داخل التطبيق.

لكن في التطبيقات الحقيقية غالبًا ما يكون جزء معين من الرابط مرتبطًا ببيانات محفوظة في Backend، مثل:

  • Invitation ID.
  • User ID.
  • Order ID.
  • Product ID.
  • Campaign ID.
  • Referral Code.
  • Reset Token.

في هذه الحالة يقوم الـ Backend عادةً بإنشاء أو حفظ الرمز، ثم يقوم التطبيق باستخدامه لبناء الرابط أو يحصل على الرابط كاملًا من الـ API.

على سبيل المثال، قد يعيد الـ API:

{
  "code": "ABC123",
  "link": "https://myapp.com/invitation/ABC123"
}

ثم يقوم التطبيق بعرض الرابط للمستخدم حتى يستطيع مشاركته.

هل host يجب أن يكون Base URL الخاص بالـ API؟

لا، ليس شرطًا.

الـ host الموجود في الرابط هو الدومين الذي تريد ربطه بالتطبيق، وليس بالضرورة عنوان الـ API.

مثلًا يمكن أن يكون لديك:

API:
https://api.myapp.com

App Links:
https://myapp.com

وفي هذه الحالة يمكن أن يكون رابط الدعوة:

https://myapp.com/invitation/ABC123

بينما يبقى الـ API:

https://api.myapp.com/invitations/ABC123

والربط بين الاثنين يتم من خلال التطبيق أو الـ Backend حسب تصميم النظام.

أفضل اختيار لدومين الروابط

يمكنك استخدام نفس الدومين الخاص بالموقع، أو دومين مخصص للروابط، أو Subdomain.

مثلًا:

https://myapp.com/invitation/ABC123

أو:

https://links.myapp.com/invitation/ABC123

المهم أن يكون الدومين تحت سيطرتك وأن تستطيع نشر ملفات التحقق المطلوبة على الخادم.

إعداد Android مع app_links

على Android يجب تسجيل نوع الروابط التي يستطيع التطبيق استقبالها.

في الإصدارات الحديثة من Flutter، توثيق app_links يطلب تعطيل نظام Flutter الافتراضي للـ Deep Linking عند استخدام هذه المكتبة، وذلك من خلال إضافة Metadata داخل MainActivity. :contentReference[oaicite:6]{index=6}

افتح:

android/app/src/main/AndroidManifest.xml

ثم داخل Activity الخاصة بـ MainActivity أضف:

<meta-data
    android:name="flutter_deeplinking_enabled"
    android:value="false" />

إضافة HTTPS App Link في AndroidManifest.xml

يمكنك بعد ذلك إضافة 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="myapp.com"
        android:pathPrefix="/invitation" />

</intent-filter>

هنا قمنا بتحديد:

  • scheme = https
  • host = myapp.com
  • pathPrefix = /invitation

وبالتالي رابط مثل:

https://myapp.com/invitation/ABC123

يمكن أن يكون من الروابط التي يتعامل معها التطبيق.

توثيق app_links يوضح أيضًا أن android:autoVerify="true" يستخدم مع App Links للتحقق من علاقة التطبيق بالدومين. :contentReference[oaicite:7]{index=7}

إعداد assetlinks.json في Android

إضافة Intent Filter وحدها لا تكفي عندما تريد Android App Links موثقة؛ يجب أن يعرف Android أن هذا التطبيق مصرح له بالتعامل مع روابط الدومين.

لذلك يجب إنشاء:

/.well-known/assetlinks.json

ليصبح الرابط:

https://myapp.com/.well-known/assetlinks.json

مثال:

[
  {
    "relation": [
      "delegate_permission/common.handle_all_urls"
    ],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.myapp",
      "sha256_cert_fingerprints": [
        "AA:BB:CC:DD:EE:FF:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:11:22:33:44:55:66:77:88:99:00:11"
      ]
    }
  }
]

استبدل:

com.example.myapp

بـ Application ID الحقيقي لتطبيقك.

كما يجب استبدال SHA-256 بالبصمة الصحيحة لشهادة توقيع التطبيق. وإذا كنت تستخدم Google Play App Signing فمن المهم استخدام بصمة شهادة التوقيع الخاصة بالإصدار الموجود على Google Play، وليس بالضرورة المفتاح المحلي الموجود على جهازك. :contentReference[oaicite:8]{index=8}

الحصول على SHA-256

يمكن استخدام:

keytool -list -v -keystore my-release-key.keystore

لكن في حالة استخدام Google Play App Signing يمكنك الحصول على SHA-256 الصحيح من Play Console، لأن Google تقوم بتوقيع النسخة التي تصل للمستخدمين بمفتاحها الخاص عند استخدام هذه الخدمة. :contentReference[oaicite:9]{index=9}

استخدام Custom Scheme في Android

إذا كنت لا تريد استخدام HTTPS، يمكنك أيضًا استخدام Custom Scheme.

مثل:

myapp://invitation/ABC123

وفي Android:

<intent-filter>

    <action android:name="android.intent.action.VIEW" />

    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />

    <data
        android:scheme="myapp"
        android:host="invitation" />

</intent-filter>

Custom Scheme لا يحتاج إلى assetlinks.json، لكن HTTPS App Links تعطيك رابطًا طبيعيًا يمكن استخدامه داخل المتصفح والمشاركة بشكل أسهل، مع إمكانية فتح الموقع عندما لا يكون التطبيق مثبتًا. :contentReference[oaicite:10]{index=10}

إعداد iOS مع app_links

على iOS يوجد نظامان شائعان:

  • Custom URL Schemes.
  • Universal Links.

وتوثيق الإصدار 7 من app_links يطلب تعطيل Flutter Deep Linking الافتراضي في Info.plist. :contentReference[oaicite:11]{index=11}

افتح:

ios/Runner/Info.plist

وأضف:

<key>FlutterDeepLinkingEnabled</key>
<false/>

إضافة Custom Scheme في iOS

يمكنك تسجيل Scheme مثل:

myapp://invitation/ABC123

من خلال Info.plist:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLName</key>
        <string>com.example.myapp</string>

        <key>CFBundleURLSchemes</key>
        <array>
            <string>myapp</string>
        </array>
    </dict>
</array>

وبذلك يمكن للتطبيق استقبال رابط يبدأ بـ:

myapp://

إعداد Universal Links في iOS

إذا كنت تريد استخدام رابط HTTPS:

https://myapp.com/invitation/ABC123

فستحتاج إلى تفعيل Associated Domains في Xcode وإضافة:

applinks:myapp.com

صيغة Associated Domains تعتمد على الخدمة applinks مع الدومين الذي تريد ربطه بالتطبيق. :contentReference[oaicite:12]{index=12}

إنشاء ملف apple-app-site-association

يجب أن يستضيف موقعك ملفًا باسم:

apple-app-site-association

بدون امتداد.

ويكون في:

https://myapp.com/.well-known/apple-app-site-association

مثال مبسط:

{
  "applinks": {
    "details": [
      {
        "appID": "TEAMID.com.example.myapp",
        "paths": [
          "/invitation/*"
        ]
      }
    ]
  }
}

قم باستبدال:

TEAMID.com.example.myapp

بالـ Team ID وBundle ID الصحيحين لتطبيقك.

Apple توضح أن ملف Association يجب أن يتطابق مع Associated Domains Entitlement الموجود داخل التطبيق. :contentReference[oaicite:13]{index=13}

إنشاء رابط Invitation كامل

الآن لنفترض أننا نريد إنشاء رابط دعوة:

https://myapp.com/invitation/ABC123

يمكن أن يكون الكود:

String generateInvitationLink(String code) {
  return Uri.https(
    'myapp.com',
    '/invitation/$code',
  ).toString();
}

ثم:

final link = generateInvitationLink('ABC123');

print(link);

الناتج:

https://myapp.com/invitation/ABC123

إضافة Query Parameters إلى الرابط

يمكن أيضًا إضافة معلومات إضافية إلى الرابط باستخدام Query Parameters.

مثل:

https://myapp.com/invitation/ABC123?ref=whatsapp

في Flutter:

final uri = Uri.https(
  'myapp.com',
  '/invitation/ABC123',
  {
    'ref': 'whatsapp',
    'campaign': 'summer',
  },
);

print(uri.toString());

الناتج:

https://myapp.com/invitation/ABC123?ref=whatsapp&campaign=summer

استقبال الرابط داخل Flutter

بعد الانتهاء من إعداد Android أو iOS يمكن استقبال الروابط باستخدام:

import 'package:app_links/app_links.dart';

final appLinks = AppLinks();

final subscription = appLinks.uriLinkStream.listen((uri) {
  print(uri);
});

مكتبة app_links توصي بإنشاء AppLinks في وقت مبكر حتى لا تفقد الرابط الأول عند تشغيل التطبيق من حالة Terminated أو Cold Start. كما أن uriLinkStream يتعامل مع الرابط الأول والروابط اللاحقة. :contentReference[oaicite:14]{index=14}

قراءة أجزاء الرابط

إذا وصل:

https://myapp.com/invitation/ABC123?ref=whatsapp

فإن:

void handleLink(Uri uri) {
  print(uri.scheme);
  print(uri.host);
  print(uri.path);
  print(uri.pathSegments);
  print(uri.queryParameters);
}

ستكون القيم تقريبًا:

scheme:
https

host:
myapp.com

path:
/invitation/ABC123

pathSegments:
[invitation, ABC123]

queryParameters:
{ref: whatsapp}

استخراج Invitation Code من الرابط

يمكننا استخراج الكود من pathSegments:

void handleInvitation(Uri uri) {
  final segments = uri.pathSegments;

  if (segments.length < 2) {
    return;
  }

  if (segments[0] != 'invitation') {
    return;
  }

  final code = segments[1];

  print('Invitation Code: $code');
}

وعند استقبال:

https://myapp.com/invitation/ABC123

سيكون:

ABC123

قراءة Query Parameters

إذا كان الرابط:

https://myapp.com/invitation/ABC123?ref=whatsapp

يمكن قراءة:

final source = uri.queryParameters['ref'];

print(source);

الناتج:

whatsapp

فتح شاشة محددة عند استقبال الرابط

يمكن بعد قراءة الرابط الانتقال إلى صفحة الدعوة.

void handleLink(
  BuildContext context,
  Uri uri,
) {
  final segments = uri.pathSegments;

  if (segments.length < 2) {
    return;
  }

  if (segments[0] == 'invitation') {
    final code = segments[1];

    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (_) => InvitationPage(
          code: code,
        ),
      ),
    );
  }
}

مشكلة Cold Start وWarm Start

هناك حالتان مهمتان عند استقبال الروابط.

Cold Start

التطبيق غير مفتوح، ويضغط المستخدم على الرابط.

في هذه الحالة يجب أن تكون عملية تهيئة AppLinks مبكرة حتى يتم التقاط الرابط الأول. توثيق المكتبة يؤكد على أهمية إنشاء AppLinks مبكرًا لهذا السيناريو. :contentReference[oaicite:15]{index=15}

Warm Start

التطبيق مفتوح أو موجود في الخلفية، ثم يضغط المستخدم على رابط جديد.

في هذه الحالة يصل الرابط من خلال:

uriLinkStream

استخدام getInitialLink()

توفر المكتبة أيضًا:

final appLinks = AppLinks();

final initialUri = await appLinks.getInitialLink();

if (initialUri != null) {
  print(initialUri);
}

وهذا يسمح لك بالحصول على الرابط الأول بشكل مباشر كعملية واحدة بدل الاعتماد على Stream.

كما توفر المكتبة:

await appLinks.getInitialLinkString();

await appLinks.getLatestLink();

await appLinks.getLatestLinkString();

وهذه الدوال موثقة ضمن API الرسمي للحزمة. :contentReference[oaicite:16]{index=16}

هل أستخدم getInitialLink و uriLinkStream معًا؟

يمكن ذلك في تصميمات معينة، لكن يجب الانتباه إلى أن uriLinkStream في الإصدارات الحالية يتعامل مع الرابط الأول والروابط اللاحقة، لذلك من السهل إنشاء معالجة مزدوجة لنفس الرابط إذا قمت بكتابة منطق استقبال في المكانين بدون تصميم واضح.

في أغلب التطبيقات يكفي إنشاء Listener مبكر على:

appLinks.uriLinkStream

ثم معالجة جميع الروابط في مكان واحد.

إنشاء AppLinksService

في المشاريع الكبيرة من الأفضل فصل منطق استقبال الروابط عن واجهة المستخدم.

import 'dart:async';

import 'package:app_links/app_links.dart';

class AppLinksService {
  AppLinksService._();

  static final AppLinksService instance = AppLinksService._();

  final AppLinks _appLinks = AppLinks();

  StreamSubscription<Uri>? _subscription;

  Future<void> init({
    required void Function(Uri uri) onLink,
  }) async {
    await _subscription?.cancel();

    _subscription = _appLinks.uriLinkStream.listen(
      onLink,
      onError: (error) {
        print('Deep link error: $error');
      },
    );
  }

  Future<void> dispose() async {
    await _subscription?.cancel();
    _subscription = null;
  }
}

استخدام AppLinksService داخل main.dart

يمكن تهيئة الخدمة عند تشغيل التطبيق:

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  runApp(const MyApp());
}

وبعد بناء التطبيق يمكن تشغيل الخدمة في Widget رئيسية:

@override
void initState() {
  super.initState();

  AppLinksService.instance.init(
    onLink: handleDeepLink,
  );
}

معالجة Invitation Link

void handleDeepLink(Uri uri) {
  final segments = uri.pathSegments;

  if (segments.length < 2) {
    return;
  }

  final type = segments[0];
  final value = segments[1];

  if (type == 'invitation') {
    print('Open invitation: $value');

    // Navigate to invitation page
  }
}

مثال كامل لرابط الدعوة

لنفترض أن لدينا رابطًا:

https://myapp.com/invitation/ABC123

عند ضغط المستخدم على الرابط:

Browser / WhatsApp
        ↓
Android / iOS
        ↓
App Links / Universal Links
        ↓
app_links
        ↓
uriLinkStream
        ↓
handleDeepLink()
        ↓
ABC123
        ↓
InvitationPage

وبذلك يمكنك فتح الدعوة المطلوبة داخل التطبيق مباشرة.

هل الـ Backend يستقبل الرابط؟

ليس بالضرورة أن يستقبل الـ Backend الرابط نفسه عند الضغط عليه.

عند وجود التطبيق مثبتًا وإعداد App Links أو Universal Links بشكل صحيح، يمكن للنظام توجيه الرابط مباشرة إلى التطبيق، ثم التطبيق يقرأ الرابط باستخدام app_links.

بعد ذلك يستطيع التطبيق إرسال الكود إلى الـ API:

GET /invitations/ABC123

والـ Backend يعيد بيانات الدعوة.

لذلك يكون السيناريو:

رابط المشاركة
     ↓
التطبيق
     ↓
app_links
     ↓
قراءة ABC123
     ↓
API
     ↓
بيانات الدعوة

هل يمكن إنشاء الرابط بالكامل داخل التطبيق؟

نعم.

مثل:

final code = 'ABC123';

final link = Uri.https(
  'myapp.com',
  '/invitation/$code',
).toString();

لكن إذا كان ABC123 يمثل سجلًا موجودًا في قاعدة البيانات، فغالبًا ستحتاج إلى إنشاء السجل أو الـ token في Backend أولًا.

أما مكتبة app_links نفسها فلا تتطلب وجود Backend فقط من أجل إنشاء String يمثل الرابط.

مشاركة الرابط من Flutter

بعد إنشاء الرابط يمكنك تمريره إلى مكتبة مشاركة مثل share_plus.

final link = Uri.https(
  'myapp.com',
  '/invitation/ABC123',
).toString();

// استخدم الرابط مع مكتبة المشاركة التي تعتمد عليها
print(link);

اختبار الرابط على Android

يمكن اختبار Custom Scheme باستخدام ADB:

adb shell am start -a android.intent.action.VIEW \
  -d "myapp://invitation/ABC123"

كما توثق مكتبة app_links استخدام ADB لاختبار Deep Links، ويمكن اختبار App Links أيضًا من أدوات Android Studio. :contentReference[oaicite:17]{index=17}

اختبار الرابط على iOS

يمكن اختبار Custom Scheme على Simulator باستخدام:

xcrun simctl openurl booted \
  "myapp://invitation/ABC123"

ويقدم المثال الرسمي للمكتبة طريقة مماثلة لاختبار الروابط من خلال simctl. :contentReference[oaicite:18]{index=18}

اختبار HTTPS App Link

يمكنك تجربة:

https://myapp.com/invitation/ABC123

لكن نجاح الرابط يعتمد على اكتمال إعدادات Android وiOS والخادم.

في Android مثلًا يجب أن يكون assetlinks.json صحيحًا، وأن تكون شهادة التوقيع الموجودة فيه مطابقة لشهادة التطبيق، كما أن إعادة التوجيه بين الدومينات أو من HTTP إلى HTTPS قد تمنع التحقق في بعض السيناريوهات. :contentReference[oaicite:19]{index=19}

لماذا يفتح الرابط المتصفح بدل التطبيق؟

إذا كان الرابط:

https://myapp.com/invitation/ABC123

ويفتح المتصفح بدل التطبيق، فهناك عدة أشياء يجب فحصها.

  • التأكد من وجود Intent Filter الصحيح في AndroidManifest.xml.
  • التأكد من وجود android:autoVerify="true".
  • التأكد من أن assetlinks.json موجود في المسار الصحيح.
  • التأكد من Package Name.
  • التأكد من SHA-256 الخاص بالتوقيع المستخدم في النسخة المثبتة.
  • إذا كان التطبيق منشورًا على Google Play، استخدم SHA-256 الخاصة بـ Play App Signing عند الحاجة.
  • التأكد من عدم وجود Redirect غير مناسب في الدومين.
  • التأكد من أن الدومين المستخدم في Intent Filter يطابق الدومين الموجود في ملفات التحقق.

Android يقوم تلقائيًا بعملية التحقق من الدومينات عند استخدام App Links مع android:autoVerify="true"، ويمكن أيضًا استخدام أدوات التحقق الخاصة بـ Android لاختبار الإعداد. :contentReference[oaicite:20]{index=20}

مشكلة www وبدون www

انتبه إلى الفرق بين:

myapp.com
www.myapp.com

فهما Host مختلفان من منظور إعدادات الروابط.

إذا كنت تستخدم:

https://www.myapp.com/invitation/ABC123

يجب أن تكون إعدادات App Links وملف التحقق مناسبة لـ www.myapp.com أيضًا.

توثيق Android يذكر أيضًا أن Redirect بين النطاقات يمكن أن يؤثر على عملية التحقق. :contentReference[oaicite:21]{index=21}

مشكلة Android 13 وأحدث أثناء الاختبار

توثيق app_links يشير إلى أنه أثناء التطوير على Android 13 وما بعده قد تحتاج إلى تفعيل الروابط يدويًا من إعدادات التطبيق ضمن:

App Info
→ Open by default
→ Add link

خصوصًا أثناء الاختبار والتطوير. :contentReference[oaicite:22]{index=22}

مشكلة Universal Links على iOS

إذا كان Universal Link لا يفتح التطبيق على iOS، افحص:

  • Associated Domains Capability.
  • وجود applinks:myapp.com.
  • وجود ملف apple-app-site-association.
  • أن الملف موجود في /.well-known/.
  • أن Team ID وBundle ID صحيحان.
  • أن المسار /invitation/* مسجل بشكل صحيح.

Apple توضح أن النظام يتحقق من ملف Associated Domain، كما أن تحديثات ملف Association قد تمر عبر آليات التخزين المؤقت الخاصة بالنظام وApple CDN، لذلك قد لا تظهر بعض التغييرات فورًا. :contentReference[oaicite:23]{index=23}

استخدام app_links مع GoRouter

يمكنك بعد استقبال الرابط من uriLinkStream تمرير المسار إلى نظام Navigation المستخدم في مشروعك.

مثلًا إذا كان لديك:

/invitation/:code

يمكن استخراج الكود من:

final code = uri.pathSegments[1];

ثم توجيه المستخدم إلى شاشة الدعوة بالطريقة التي يعتمد عليها مشروعك.

الفرق المهم هنا أن app_links مسؤول عن استقبال الرابط من نظام التشغيل، بينما إدارة Routing داخل التطبيق مسؤولية نظام الـ Navigation الموجود لديك.

الحماية والتحقق من Invitation Code

إذا كان الرابط يحتوي على:

https://myapp.com/invitation/ABC123

لا ينبغي اعتبار وجود ABC123 وحده دليلًا على صلاحية الدعوة.

التطبيق يمكنه قراءة الكود، لكن يجب أن يقوم الـ Backend بالتحقق من:

  • وجود الدعوة.
  • صلاحية الكود.
  • انتهاء مدة الرابط إن كان له Expiration.
  • حالة الدعوة.
  • صلاحيات المستخدم.

مثال:

Future<void> openInvitation(String code) async {
  final response = await api.get(
    '/invitations/$code',
  );

  // اعرض بيانات الدعوة بعد التحقق من الخادم
}

ويفضل أن يكون الكود عبارة عن Token غير قابل للتخمين بدل وضع معلومات حساسة مباشرة داخل الرابط.

إنشاء Deep Link للمنتج

يمكنك تطبيق نفس الفكرة على المنتجات:

final uri = Uri.https(
  'myapp.com',
  '/product/125',
);

الناتج:

https://myapp.com/product/125

وعند الاستقبال:

if (uri.pathSegments.length >= 2 &&
    uri.pathSegments[0] == 'product') {
  final productId = uri.pathSegments[1];

  print(productId);
}

إنشاء رابط للطلب

final uri = Uri.https(
  'myapp.com',
  '/order/98765',
);

وبالتالي:

https://myapp.com/order/98765

إنشاء رابط لإعادة تعيين كلمة المرور

يمكن أيضًا استخدام Deep Links في سيناريوهات مثل Reset Password:

https://myapp.com/reset-password/TOKEN123

ثم:

if (uri.pathSegments.length >= 2 &&
    uri.pathSegments[0] == 'reset-password') {
  final token = uri.pathSegments[1];

  // افتح شاشة إعادة تعيين كلمة المرور
}

Web مع app_links

المكتبة تدعم Web، لكن التوثيق يوضح أن Web يوفر الرابط الأول فقط، ولا يعمل بنفس نموذج استقبال الأحداث المستمر الموجود في المنصات الأخرى. كما أنه لا توجد إعدادات Native إضافية للويب داخل المكتبة. :contentReference[oaicite:24]{index=24}

Windows وmacOS وLinux

توفر المكتبة أيضًا دعمًا لمنصات سطح المكتب، لكن إعداد التسجيل يختلف حسب نظام التشغيل.

على Windows مثلًا توجد إعدادات مرتبطة بتسجيل البروتوكول أو Web-to-App، ويقدم المشروع الرسمي توثيقًا خاصًا لهذا الأمر. :contentReference[oaicite:25]{index=25}

الفرق بين إنشاء الرابط واستقبال الرابط

من المفيد فصل العمليتين ذهنيًا:

إنشاء الرابط
↓
https://myapp.com/invitation/ABC123

مشاركة الرابط
↓
WhatsApp / Telegram / Browser / Email

ضغط المستخدم على الرابط
↓
Android / iOS

تسليم الرابط إلى التطبيق
↓
app_links

قراءة URI
↓
ABC123

الاتصال بالـ API
↓
بيانات الدعوة

عرض الشاشة
↓
InvitationPage

بهذا الشكل تكون مسؤولية كل جزء واضحة، ويصبح من الأسهل تصميم النظام سواء كان الـ Backend مكتوبًا بـ .NET أو Node.js أو أي تقنية أخرى.

الخلاصة

مكتبة app_links تقدم طريقة عملية للتعامل مع Deep Links وAndroid App Links وiOS Universal Links وCustom URL Schemes داخل تطبيقات Flutter.

المكتبة نفسها مسؤولة عن استقبال الرابط من نظام التشغيل، بينما إنشاء الرابط يمكن أن يتم من Flutter أو من الـ Backend، حسب طريقة تصميم التطبيق.

إذا كنت تريد رابطًا مثل:

https://myapp.com/invitation/ABC123

فأنت تحتاج إلى ثلاثة أجزاء رئيسية:

  • إنشاء الرابط باستخدام دومين تملكه.
  • إعداد Android وiOS لربط الدومين بالتطبيق.
  • استخدام app_links لاستقبال الرابط وقراءة الـ URI.

وعندما تكون الدعوة أو الطلب أو المنتج مرتبطًا ببيانات في قاعدة البيانات، يستطيع التطبيق استخراج الـ ID أو Token من الرابط ثم إرسال القيمة إلى الـ Backend للتحقق وإرجاع البيانات المطلوبة.

بهذه الطريقة يمكن بناء روابط للطلبات والمنتجات والدعوات والحملات وإعادة تعيين كلمة المرور وغيرها من السيناريوهات التي تحتاج إلى فتح شاشة محددة داخل تطبيق Flutter مباشرة.



مكتبة app_links على pub.dev

مستودع app_links على GitHub

المزيد من مقالات Flutter على Geecoders

الكود كامل

import 'dart:async';

import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final AppLinks _appLinks = AppLinks();

  StreamSubscription<Uri>? _linkSubscription;

  @override
  void initState() {
    super.initState();

    _initDeepLinks();
  }

  Future<void> _initDeepLinks() async {
    await _linkSubscription?.cancel();

    _linkSubscription = _appLinks.uriLinkStream.listen(
      (uri) {
        _handleDeepLink(uri);
      },
      onError: (error) {
        debugPrint(
          'Deep Link Error: $error',
        );
      },
    );
  }

  void _handleDeepLink(Uri uri) {
    debugPrint(
      'Received URI: $uri',
    );

    debugPrint(
      'Scheme: ${uri.scheme}',
    );

    debugPrint(
      'Host: ${uri.host}',
    );

    debugPrint(
      'Path: ${uri.path}',
    );

    debugPrint(
      'Segments: ${uri.pathSegments}',
    );

    debugPrint(
      'Query Params: ${uri.queryParameters}',
    );

    final segments = uri.pathSegments;

    if (segments.length >= 2 &&
        segments[0] == 'invitation') {
      final invitationCode = segments[1];

      _openInvitation(
        invitationCode,
      );
    }
  }

  void _openInvitation(
    String code,
  ) {
    debugPrint(
      'Invitation code: $code',
    );

    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (_) => InvitationPage(
          code: code,
        ),
      ),
    );
  }

  String generateInvitationLink(
    String code,
  ) {
    return Uri.https(
      'myapp.com',
      '/invitation/$code',
    ).toString();
  }

  @override
  void dispose() {
    _linkSubscription?.cancel();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'App Links Demo',
      home: const HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(
          'App Links',
        ),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment:
              MainAxisAlignment.center,
          children: [
            const Text(
              'Deep Link Demo',
              style: TextStyle(
                fontSize: 24,
                fontWeight: FontWeight.bold,
              ),
            ),

            const SizedBox(
              height: 20,
            ),

            TextButton(
              onPressed: () {
                final link = Uri.https(
                  'myapp.com',
                  '/invitation/ABC123',
                ).toString();

                debugPrint(link);
              },
              child: const Text(
                'Generate Invitation Link',
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class InvitationPage
    extends StatelessWidget {
  final String code;

  const InvitationPage({
    super.key,
    required this.code,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(
          'Invitation',
        ),
      ),
      body: Center(
        child: Text(
          'Invitation Code: $code',
          style: const TextStyle(
            fontSize: 22,
          ),
        ),
      ),
    );
  }
}

AndroidManifest.xml

<manifest
    xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        android:label="myapp"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">

        <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">

            <meta-data
                android:name="flutter_deeplinking_enabled"
                android:value="false" />

            <intent-filter>

                <action
                    android:name="android.intent.action.MAIN" />

                <category
                    android:name="android.intent.category.LAUNCHER" />

            </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="myapp.com"
                    android:pathPrefix="/invitation" />

            </intent-filter>

        </activity>

    </application>

</manifest>

assetlinks.json

[
  {
    "relation": [
      "delegate_permission/common.handle_all_urls"
    ],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.myapp",
      "sha256_cert_fingerprints": [
        "YOUR_SHA256_FINGERPRINT"
      ]
    }
  }
]

ضع الملف في:

https://myapp.com/.well-known/assetlinks.json

iOS Info.plist

<key>FlutterDeepLinkingEnabled</key>
<false/>

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLName</key>
        <string>com.example.myapp</string>

        <key>CFBundleURLSchemes</key>
        <array>
            <string>myapp</string>
        </array>
    </dict>
</array>

Apple Universal Links

من Xcode:

Runner
→ Signing & Capabilities
→ Associated Domains
→ applinks:myapp.com

ثم ضع ملف:

apple-app-site-association

داخل:

https://myapp.com/.well-known/apple-app-site-association

ومحتواه مثل:

{
  "applinks": {
    "details": [
      {
        "appID": "TEAM_ID.com.example.myapp",
        "paths": [
          "/invitation/*"
        ]
      }
    ]
  }
}

إنشاء رابط دعوة

String generateInvitationLink(
  String code,
) {
  final uri = Uri.https(
    'myapp.com',
    '/invitation/$code',
  );

  return uri.toString();
}

final link = generateInvitationLink(
  'ABC123',
);

print(link);

الناتج:

https://myapp.com/invitation/ABC123

استقبال الرابط

final appLinks = AppLinks();

final subscription =
    appLinks.uriLinkStream.listen(
  (Uri uri) {
    final segments = uri.pathSegments;

    if (segments.length >= 2 &&
        segments[0] == 'invitation') {
      final code = segments[1];

      print(
        'Invitation Code: $code',
      );
    }
  },
);

اختبار Android

adb shell am start \
  -a android.intent.action.VIEW \
  -d "myapp://invitation/ABC123"

اختبار iOS

xcrun simctl openurl booted \
  "myapp://invitation/ABC123"


مزيد من المقالات : شرح Android Developer Verification وكيفية حجز Package Name قبل إنشاء التطبيق في Google Play Console
شاهد أيضًا
مقالات ذات صلة

🚫 مانع الإعلانات مفعل

يجب إيقاف مانع الإعلانات لاستكمال تصفح الموقع