شرح مكتبة 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= httpshost= myapp.compathPrefix= /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.jsoniOS 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




