Как обрабатывать deep linking в приложении Flutter?
Deep linking во Flutter: настраиваете intent filter (Android) и CFBundleURLSchemes (iOS), используете go_router с path-параметрами для маршрутизации. Пакет app_links обрабатывает cold start через getInitialLink() и warm start через uriLinkStream. Главный риск — пропустить один из двух кейсов запуска.
Что такое deep linking во Flutter
Deep link — это URL, который открывает конкретный экран приложения. Flutter поддерживает три варианта: custom scheme (myapp://product/42), universal links на iOS (https://example.com/product/42 через Associated Domains) и App Links на Android (через Digital Asset Links). С Flutter 3.x рекомендуемый подход — использовать go_router, который нативно интегрируется с механизмом deep link обеих платформ.
Базовая настройка с go_router
# pubspec.yaml
dependencies:
go_router: ^13.0.0
// lib/router.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
// Этот путь откроется при deep link: myapp://product/42
path: '/product/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ProductScreen(productId: id);
},
),
GoRoute(
path: '/promo',
builder: (context, state) {
// Query параметры: myapp://promo?code=SALE20
final code = state.uri.queryParameters['code'];
return PromoScreen(code: code);
},
),
],
);
// lib/main.dart
import 'package:flutter/material.dart';
import 'router.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp.router(
routerConfig: router,
title: 'Deep Link Demo',
);
}
Настройка платформ
Android — добавляем intent filter в AndroidManifest.xml:
<!-- android/app/src/main/AndroidManifest.xml -->
<activity android:name=".MainActivity" ...>
<!-- Custom scheme: myapp://product/42 -->
<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>
<!-- App Links: https://example.com/product/42 -->
<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="example.com" />
</intent-filter>
</activity>
iOS — добавляем custom scheme в Info.plist:
<!-- ios/Runner/Info.plist -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
Для Universal Links нужен файл apple-app-site-association на сервере и включение Associated Domains в Xcode Capabilities.
Обработка ссылки, когда приложение уже запущено
go_router слушает incoming links автоматически через Router механизм Flutter. Если нужен кастомный перехват (например, для аналитики), используем app_links:
import 'package:app_links/app_links.dart';
import 'package:go_router/go_router.dart';
class DeepLinkService {
final GoRouter router;
DeepLinkService(this.router);
late final AppLinks _appLinks;
Future<void> init() async {
_appLinks = AppLinks();
// Ссылка, по которой приложение было запущено (cold start)
final initialUri = await _appLinks.getInitialLink();
if (initialUri != null) {
_handleLink(initialUri);
}
// Ссылки, когда приложение уже работает (warm start)
_appLinks.uriLinkStream.listen(
_handleLink,
onError: (err) => debugPrint('Deep link error: $err'),
);
}
void _handleLink(Uri uri) {
// Логируем для аналитики
debugPrint('Incoming link: $uri');
// Навигируем через go_router
router.go(uri.path, extra: uri.queryParameters);
}
}
Подводные камни
- Cold start vs warm start. Ссылка при запуске (приложение не было в памяти) приходит через
getInitialLink(). Ссылка при возврате из фона — через stream. Пропустить один из кейсов — частая ошибка. - Навигация до готовности роутера. Если
router.go()вызвать до инициализации MaterialApp, получим exception. Инициализируйте DeepLinkService вinitStateили после первого frame черезWidgetsBinding.addPostFrameCallback. - Аутентификация и редиректы. Приватные экраны должны проверять auth state. В
go_routerиспользуйтеredirectколбэк — он перехватит deep link и отправит на логин с сохранением исходного пути. - Universal Links на iOS требуют HTTPS. Домен должен отдавать
apple-app-site-associationна/.well-known/apple-app-site-associationсо строгим Content-Typeapplication/json. Любая ошибка — iOS fallback на браузер. - App Links на Android требуют верификации.
autoVerify=trueзапускает верификацию при установке. Еслиassetlinks.jsonнедоступен или содержит ошибку, ссылки открываются в браузере, а не в приложении. - Back stack. При open по deep link пользователь ожидает кнопку «Назад» на главный экран.
go_routerне добавляет автоматически родительские маршруты — используйтеShellRouteили явно управляйте стеком черезGoRouter.pushвместоgo. - Тестирование на симуляторе. На iOS Simulator команда
xcrun simctl openurl booted 'myapp://product/42', на Android Emulator —adb shell am start -W -a android.intent.action.VIEW -d 'myapp://product/42'. Без тестирования обоих платформ баги в cold start незаметны.
Common mistakes
- Сводить «обрабатывать deep linking в приложении Flutter» к синтаксису и не объяснять frame scheduling.
- Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии flutter-28.
- Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.
What the interviewer is testing
- Формулирует точную модель для «обрабатывать deep linking в приложении Flutter» и подтверждает ее корректным примером.
- Умеет связать ответ с platform channel, тестированием и отладкой на устройстве.
- Называет ограничения подхода flutter-28, включая производительность, память и сопровождение.