Что такое Deep Link в Flutter и как его реализовать?

Ответ

Deep Link — это URL, который открывает конкретный экран или контент внутри приложения. В моих Flutter-проектах это ключевая функциональность для интеграции с вебом, push-уведомлениями и кросс-платформенной навигацией.

Полная реализация с go_router (рекомендуемый подход):

// pubspec.yaml зависимости:
// go_router: ^13.0.0
// uni_links: ^0.5.1
// url_launcher: ^6.0.0

import 'package:go_router/go_router.dart';
import 'package:uni_links/uni_links.dart';

// 1. Определение маршрутов
final router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/product/:id',
      builder: (context, state) {
        final id = state.pathParameters['id']!;
        return ProductScreen(productId: id);
      },
    ),
    GoRoute(
      path: '/category/:name',
      builder: (context, state) {
        final name = state.pathParameters['name']!;
        return CategoryScreen(categoryName: name);
      },
    ),
  ],
  errorBuilder: (context, state) => ErrorScreen(error: state.error),
);

// 2. Обработка deep links в main
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Обработка cold start (приложение закрыто)
  final initialUri = await getInitialUri();
  if (initialUri != null) {
    handleDeepLink(initialUri);
  }

  // Обработка warm start (приложение открыто)
  uriLinkStream.listen((Uri? uri) {
    if (uri != null) handleDeepLink(uri);
  });

  runApp(MyApp(router: router));
}

// 3. Функция обработки ссылок
void handleDeepLink(Uri uri) {
  if (uri.pathSegments.length >= 2) {
    final type = uri.pathSegments[0];
    final id = uri.pathSegments[1];

    switch (type) {
      case 'product':
        router.go('/product/$id');
        break;
      case 'category':
        router.go('/category/$id');
        break;
    }
  }

  // Обработка query параметров
  final params = uri.queryParameters;
  if (params.containsKey('ref')) {
    analytics.trackReferral(params['ref']!);
  }
}

// 4. Генерация deep links
String createProductLink(String productId) {
  return 'https://myapp.com/product/$productId';
}

// 5. Открытие ссылок из приложения
void shareProduct(String productId) async {
  final link = createProductLink(productId);
  await launchUrl(Uri.parse(link));
}

Настройка платформ:

Android (android/app/src/main/AndroidManifest.xml):

<activity android:name=".MainActivity">
  <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="/product/" />
  </intent-filter>
</activity>

iOS (ios/Runner/Info.plist):

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>myapp</string>
    </array>
  </dict>
</array>
<key>FlutterDeepLinkingEnabled</key>
<true/>

Тестирование deep links:

# Android
adb shell am start -a android.intent.action.VIEW 
  -d "https://myapp.com/product/123"

# iOS (симулятор)
xcrun simctl openurl booted "https://myapp.com/product/123"

# iOS (устройство)
open "myapp://product/123"

Особенности из практики:

  1. Universal Links (iOS) требуют серверной настройки apple-app-site-association
  2. App Links (Android) требуют assetlinks.json на сервере
  3. Фоновая обработка через WidgetsBindingObserver
  4. Аналитика — отслеживание источников переходов
  5. Резервный вариант — открытие в браузере с кнопкой "Открыть в приложении"

В production-приложениях всегда добавляю обработку ошибок для битых ссылок и логирование всех переходов для аналитики.

Ответ 18+ 🔞

Давай разберём эту тему про deep links, а то народ часто путается, как будто на дворе 2002-й год и мы только учимся ссылки в интернете открывать. Ёпта, это же просто ссылка, которая тыкает пользователя прямо в нужное место твоего приложения, минуя главный экран. Типа, получил пуш-уведомление — тык — и ты уже в корзине, а не смотришь на логотип.

Полная реализация с go_router (делай так и не парься):

// pubspec.yaml зависимости:
// go_router: ^13.0.0
// uni_links: ^0.5.1
// url_launcher: ^6.0.0

import 'package:go_router/go_router.dart';
import 'package:uni_links/uni_links.dart';

// 1. Раскидываем маршруты, как карты на столе
final router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/product/:id',
      builder: (context, state) {
        final id = state.pathParameters['id']!;
        return ProductScreen(productId: id);
      },
    ),
    GoRoute(
      path: '/category/:name',
      builder: (context, state) {
        final name = state.pathParameters['name']!;
        return CategoryScreen(categoryName: name);
      },
    ),
  ],
  errorBuilder: (context, state) => ErrorScreen(error: state.error),
);

// 2. Ловим deep links прямо в main, чтобы не проёбать
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Холодный старт (приложение было закрыто)
  final initialUri = await getInitialUri();
  if (initialUri != null) {
    handleDeepLink(initialUri);
  }

  // Тёплый старт (приложение уже болталось в фоне)
  uriLinkStream.listen((Uri? uri) {
    if (uri != null) handleDeepLink(uri);
  });

  runApp(MyApp(router: router));
}

// 3. Функция, которая разбирает ссылку как Шерлок Холмс
void handleDeepLink(Uri uri) {
  // Смотри, чтобы сегментов было достаточно, а то будет тебе **хитрая жопа**
  if (uri.pathSegments.length >= 2) {
    final type = uri.pathSegments[0];
    final id = uri.pathSegments[1];

    switch (type) {
      case 'product':
        router.go('/product/$id');
        break;
      case 'category':
        router.go('/category/$id');
        break;
    }
  }

  // Выковыриваем query-параметры, если они есть
  final params = uri.queryParameters;
  if (params.containsKey('ref')) {
    analytics.trackReferral(params['ref']!); // Отслеживаем, откуда пришёл юзер
  }
}

// 4. Генерируем ссылки, чтобы ими делиться
String createProductLink(String productId) {
  return 'https://myapp.com/product/$productId';
}

// 5. Открываем эти ссылки где угодно
void shareProduct(String productId) async {
  final link = createProductLink(productId);
  await launchUrl(Uri.parse(link));
}

Настройка платформ (тут без этого нихуя не заработает):

Android (android/app/src/main/AndroidManifest.xml):

<activity android:name=".MainActivity">
  <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="/product/" />
  </intent-filter>
</activity>

iOS (ios/Runner/Info.plist):

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>myapp</string>
    </array>
  </dict>
</array>
<key>FlutterDeepLinkingEnabled</key>
<true/>

Тестирование (а то запустишь и будешь орать "не работает, ёб твою мать!"):

# Android
adb shell am start -a android.intent.action.VIEW 
  -d "https://myapp.com/product/123"

# iOS (симулятор)
xcrun simctl openurl booted "https://myapp.com/product/123"

# iOS (устройство)
open "myapp://product/123"

Особенности из боевого опыта, чувак:

  1. Universal Links на iOS — это отдельная песня. Нужен файл apple-app-site-association на твоём сервере, иначе будет просто открываться в Safari. Доверия ебать ноль к этой схеме, пока не проверишь десять раз.
  2. App Links на Android — тоже требуют assetlinks.json на сервере. Без этого — пидарас шерстяной, ссылка будет спрашивать "в каком приложении открыть?".
  3. Фоновая обработка — через WidgetsBindingObserver лови, когда приложение выходит на передний план, чтобы перепроверить ссылку. А то пользователь тыкнет, а у тебя ни хуя себе — навигация сломалась.
  4. Аналитика — обязательно пиши, откуда пришёл пользователь. Без этого ты как слепой кот.
  5. Резервный вариант — если приложения нет, открывай страницу в браузере с большой-пребольшой кнопкой "Открыть в приложении". Волнение ебать у пользователя, если он не поймёт, что делать.

В продакшене всегда накидывай обработку ошибок для битых ссылок и логируй всё, что движется. Чтобы когда придёт багрепорт "не открывается", ты не сам от себя охуел, а сразу посмотрел в логи и понял, в каком месте накрылся медным тазом твой код.

Видео-ответы