التنقل والتوجيه

الروابط العميقة وإعداد المنصة

16 دقيقة الدرس 13 من 14

الروابط العميقة وإعداد المنصة

تتيح الروابط العميقة للروابط الخارجية فتح شاشات محددة داخل تطبيق Flutter. عندما ينقر المستخدم على رابط مثل myapp://profile/42 أو https://myapp.com/products/99، ينتقل مباشرةً إلى الشاشة ذات الصلة بدلاً من الصفحة الرئيسية. يغطي هذا الدرس كيفية إعداد نظامَي Android وiOS للروابط العميقة، وربطها بـ GoRouter، واختبار التدفق الكامل على الأجهزة الحقيقية والمحاكيات.

ما هو الرابط العميق؟

الرابط العميق هو URI يوجه المستخدم إلى موقع محدد داخل تطبيق الجوال. ثمة ثلاثة أنواع شائعة من الروابط العميقة في Flutter:

  • مخططات URI المخصصة — مثل myapp://products/42. سهلة الإعداد لكن لا يمكن النقر عليها من الويب.
  • روابط تطبيق Android — روابط HTTPS تُتحقق من صحتها مقابل نطاقك عبر Digital Asset Links. يفتح Android التطبيق بدلاً من المتصفح عند نجاح التحقق.
  • الروابط العالمية لـ iOS — روابط HTTPS مرتبطة بنطاقك عبر ملف apple-app-site-association (AASA). يوجه iOS الروابط المتحقق منها مباشرةً إلى التطبيق.
ملاحظة: في تطبيقات الإنتاج، استخدم دائماً App Links / Universal Links بدلاً من المخططات المخصصة. يمكن لتطبيقات أخرى على الجهاز اعتراض المخططات المخصصة؛ أما الروابط المبنية على HTTPS فهي مرتبطة بنطاقك ولا يمكن اختطافها.

إعداد Android — فلاتر النوايا (Intent Filters)

يستخدم Android فلاتر النوايا المُعلَنة في AndroidManifest.xml لوصف معرفات URI التي يمكن للتطبيق معالجتها. افتح android/app/src/main/AndroidManifest.xml وأضف الفلتر داخل عنصر <activity> الرئيسي.

android/app/src/main/AndroidManifest.xml — مخطط مخصص + App Links

<!-- داخل <activity android:name=".MainActivity" ...> -->

<!-- المخطط المخصص: myapp://... -->
<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" />
</intent-filter>

<!-- Android App Links: https://myapp.com/... -->
<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" />
</intent-filter>

تُخبر الخاصية android:autoVerify="true" نظام Android بالتحقق من ملكية myapp.com عبر جلب https://myapp.com/.well-known/assetlinks.json. يجب أن يتضمن هذا الملف بصمة SHA-256 لشهادة تطبيقك.

إعداد iOS — مخططات URL والنطاقات المرتبطة

في iOS، يجب إعداد شيئين في Xcode (أو مباشرةً في ملفات المصدر):

  • مخطط URL مخصص — أضف إدخال CFBundleURLTypes إلى ios/Runner/Info.plist.
  • الروابط العالمية — فعّل إمكانية Associated Domains وأضف applinks:myapp.com.

ios/Runner/Info.plist — مخطط URL مخصص

<!-- أضف داخل <dict> الجذر -->
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLName</key>
        <string>com.example.myapp</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>myapp</string>
        </array>
    </dict>
</array>

للروابط العالمية، افتح Xcode → Runner → Signing & Capabilities → + Capability → Associated Domains وأضف applinks:myapp.com. يُولِّد هذا إدخالاً في ملف entitlements. يجب استضافة ملف AASA على https://myapp.com/.well-known/apple-app-site-association.

نصيحة: توثيق حزمتَي flutter_deep_link وgo_router يحيل إلى أدوات التحقق من Google وApple. استخدم App Links Assistant في Android Studio ومحقق AASA من Apple على branch.io/resources/aasa-validator للتحقق من ملفات الخادم قبل الإطلاق.

معالجة الروابط العميقة مع GoRouter

يتكامل GoRouter مع واجهة برمجة Router في Flutter التي تستقبل تلقائياً معرفات URI للروابط العميقة من المنصة. كل ما تحتاجه هو التأكد من أن أنماط المسارات تتطابق مع مقاطع URI الواردة. يحدد initialLocation في GoRouter الشاشة الافتراضية عند بدء التطبيق بارداً بدون رابط عميق.

main.dart — GoRouter مع مسارات جاهزة للروابط العميقة

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

final _router = GoRouter(
  initialLocation: '/home',           // الافتراضي عند البدء البارد
  debugLogDiagnostics: true,          // تسجيل جميع أحداث التنقل
  routes: [
    GoRoute(
      path: '/home',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/products/:id',          // :id يطابق مقطع المسار
      builder: (context, state) {
        final id = state.pathParameters['id']!;
        return ProductDetailScreen(productId: id);
      },
    ),
    GoRoute(
      path: '/profile/:userId',
      builder: (context, state) {
        final userId = state.pathParameters['userId']!;
        final tab = state.uri.queryParameters['tab'] ?? 'posts';
        return ProfileScreen(userId: userId, initialTab: tab);
      },
    ),
  ],
);

void main() {
  runApp(
    MaterialApp.router(
      routerConfig: _router,
      title: 'عرض الروابط العميقة',
    ),
  );
}

عندما يُطلق Android النية https://myapp.com/products/99، تجرد محرك Flutter المضيف وتمرر /products/99 إلى GoRouter، الذي يطابق مسار /products/:id ويبني ProductDetailScreen(productId: '99'). معاملات الاستعلام متاحة عبر state.uri.queryParameters.

عمليات إعادة التوجيه وحراسة المصادقة

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

إعادة توجيه GoRouter — حماية المسارات التي تصلها روابط عميقة

final _router = GoRouter(
  initialLocation: '/home',
  redirect: (BuildContext context, GoRouterState state) {
    final isLoggedIn = AuthService.instance.isLoggedIn;
    final isOnLogin  = state.matchedLocation == '/login';

    // غير مسجل الدخول وليس متجهاً للتسجيل → أرسله للتسجيل
    if (!isLoggedIn && !isOnLogin) return '/login';

    // مسجل الدخول ومتجه للتسجيل → أرسله للرئيسية
    if (isLoggedIn && isOnLogin) return '/home';

    // لا حاجة لإعادة توجيه
    return null;
  },
  routes: [
    GoRoute(path: '/login',   builder: (c, s) => const LoginScreen()),
    GoRoute(path: '/home',    builder: (c, s) => const HomeScreen()),
    GoRoute(
      path: '/orders/:id',
      builder: (c, s) => OrderScreen(orderId: s.pathParameters['id']!),
    ),
  ],
);
تحذير: بدون حارس إعادة التوجيه، سيعرض رابط عميق نحو /orders/123 شاشة الطلب حتى عندما يكون المستخدم غير مسجل الدخول. تحقق دائماً من حالة المصادقة (وسائر الشروط المسبقة) في دالة رد النداء redirect لكل مسار محمي.

اختبار الروابط العميقة

يمكنك تشغيل الروابط العميقة على المحاكيات والأجهزة الفعلية من سطر الأوامر دون الحاجة إلى متصفح:

  • Android (adb): adb shell am start -a android.intent.action.VIEW -d "myapp://products/42" com.example.myapp
  • iOS (xcrun): xcrun simctl openurl booted "myapp://products/42"
  • Flutter DevTools: تبويب Deep Links (Flutter 3.19+) يتحقق من إدخالات manifest ويتيح إطلاق URIs تجريبية مباشرةً.
نصيحة: فعّل debugLogDiagnostics: true على نسخة GoRouter أثناء التطوير. كل URI وارد ومسار مطابق وعملية إعادة توجيه تُطبع في وحدة التحكم، مما يجعل تتبع سبب هبوط رابط على شاشة خاطئة أمراً يسيراً.

خلاصة

تربط الروابط العميقة عالم الويب بعالم التطبيقات الأصلية. في Android، تُعلن فلاتر النوايا في AndroidManifest.xml معرفات URI التي يفتح التطبيق لها؛ وتُضيف App Links التحقق من النطاق لروابط HTTPS. في iOS، يُسجّل Info.plist المخططات المخصصة بينما تُتيح Associated Domains الروابط العالمية. يعالج GoRouter معرفات URI الواردة تلقائياً — كل ما عليك هو ضمان تطابق أنماط المسارات ووجود حراس إعادة التوجيه لحماية المسارات الحساسة. اختبر دائماً باستخدام adb أو xcrun simctl قبل الإطلاق.