
Недавно передо мной возникла задача запуска Flutter-приложения с использованием deep links. Мне пришлось покопаться в документации и поэкпериментировать чтобы получить адекватное представление о том, как работать с ними во Flutter. В этой статье я сагрегировал результаты, чтобы тем, кто столкнется с такой же задачей, было проще разобраться.
Deep Links – это URL-адреса, которые дают пользователям возможность перейти к определенному контенту внутри мобильного приложения на iOS или Android. Это значит, что мы должны отслеживать, как было открыто приложение: стандартным способом или с помощью ссылки, и кроме того, приложение может быть уже открыто, когда был совершен переход. Значит, мы должны отслеживать переходы по ссылкам и в бэкграунде работающего приложения. Давайте разберемся, как лучше всего это сделать в Flutter.
Первым делом – конфигурация
Чтобы использовать Deep Links в нативной разработке, необходимо подготовить соответствующую конфигурацию в проекте. Для Flutter-приложения это делается абсолютно так же, как и в нативе.
iOS
В Apple-экосистеме существует два способа формирования таких ссылок: «Custom URL schemes» и «Universal Links».
- Custom URL schemes – позволяют использовать пользовательскую схему, независимо от того, какой хост будет указан. Этот подход наиболее прост, но есть нюансы: необходимо быть уверенным, что схема уникальна, и, кроме того, ссылка не будет работать без установленного приложения. Если использовать Custom URL schemes, то можно будет использовать ссылки типа:
your_scheme://any_host - Universal Links – чуть более сложный подход. Они позволяют работать только со схемой https и с определенным хостом, но необходимо подтверждение прав на использование этого хоста, для чего на сервере необходимо разместить файл – apple-app-site-association. Universal Links дают вам возможность запустить приложение по URL:
https://your_host, а в случае отсутствия установленного приложения предложит установить его из стора или открыть ссылку в браузере.
Для примера я использую подход Custom URL schemes, так как он проще. Добавим в файл Info.plist такой кусок:
<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleTypeRole</key> <string>Editor</string> <key>CFBundleURLName</key> <string>deeplink.flutter.dev</string> <key>CFBundleURLSchemes</key> <array> <string>poc</string> </array> </dict> </array>
Android
В экосистеме Android также есть два способа формирования ссылок с примерно такими же свойствами:
- Deep Links – (так же, как и Custom URL schemes в iOS) позволяют использовать пользовательскую схему независимо от того, какой хост будет указан.
- App Links – позволяют работать только со схемой https и с определенным хостом (так же, как Universal Links в iOS), и также необходимо подтверждение прав на использование этого хоста с помощью размещения на сервере Digital Asset Links JSON файла.
Для андроида я тоже решил не усложнять и использовал Deep Links. Добавим в AndroidManifest.xml вот это:
<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="poc" android:host="deeplink.flutter.dev" /> </intent-filter>
Таким образом мы сконфигурировали приложения для обеих платформ для схем poc и сможем обрабатывать в них URL poc://deeplink.flutter.dev
Готовим Platform Channels
Итак, нативная конфигурация для каждой из платформ готова. Но кроме конфигурации нужно подготовить Platform Channels, благодаря которым нативная часть будет взаимодействовать с Flutter. И опять нужно подготовить свою реализацию как для Android, так и для iOS.
Начнем с Android. Нужно сделать всего ничего – всего лишь обработать входящий Intent в методе onCreate, создать MethodChannel и передавать в него URI, если приложение запущено через Deep Link.
private static final String CHANNEL = "poc.deeplink.flutter.dev/cnannel"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); GeneratedPluginRegistrant.registerWith(this); Intent intent = getIntent(); Uri data = intent.getData(); new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler( new MethodChannel.MethodCallHandler() { @Override public void onMethodCall(MethodCall call, MethodChannel.Result result) { if (call.method.equals("initialLink")) { if (startString != null) { result.success(startString); } } } }); if (data != null) { startString = data.toString(); } }
В iOS все будет немного по-другому, хотя в целом то же самое: передача URI в приложение через MethodChannel. Реализовать я решил на Swift, так как с Objecttive-C дела у меня обстоят не очень хорошо)). Далее – измененный AppDelegate.swift
@UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { private var methodChannel: FlutterMethodChannel? override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? ) -> Bool { let controller = window.rootViewController as! FlutterViewController methodChannel = FlutterMethodChannel(name: "poc.deeplink.flutter.dev/cnannel", binaryMessenger: controller) methodChannel?.setMethodCallHandler({ (call: FlutterMethodCall, result: FlutterResult) in guard call.method == "initialLink" else { result(FlutterMethodNotImplemented) return } }) GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) }
Так мы будем обрабатывать запуск приложения через Deep Link. А что, если переход по ссылке произошел, когда приложение уже запущено? Необходимо учесть и этот момент.
В Андроиде для этого мы переопределим метод onNewIntent и будем обрабатывать каждый входящий интент. Если это будет переход по ссылке, то будем кидать событие в созданный для этого EventChannel через специально созданный BroadcastReceiver.
private static final String EVENTS = "poc.deeplink.flutter.dev/events"; private BroadcastReceiver linksReceiver; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); GeneratedPluginRegistrant.registerWith(this); new EventChannel(getFlutterView(), EVENTS).setStreamHandler( new EventChannel.StreamHandler() { @Override public void onListen(Object args, final EventChannel.EventSink events) { linksReceiver = createChangeReceiver(events); } @Override public void onCancel(Object args) { linksReceiver = null; } } ); } @Override public void onNewIntent(Intent intent){ super.onNewIntent(intent); if(intent.getAction() == android.content.Intent.ACTION_VIEW && linksReceiver != null) { linksReceiver.onReceive(this.getApplicationContext(), intent); } } private BroadcastReceiver createChangeReceiver(final EventChannel.EventSink events) { return new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { // NOTE: assuming intent.getAction() is Intent.ACTION_VIEW String dataString = intent.getDataString(); if (dataString == null) { events.error("UNAVAILABLE", "Link unavailable", null); } else { events.success(dataString); } ; } }; } }
Давайте сделаем то же самое в части iOS. В Swift мы должны создать FlutterStreamHandler и обработать любую ссылку, которую будем получать, пока приложение находится в фоновом режиме. Пора опять немного поменять AppDelegate.swift
@UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { private var eventChannel: FlutterEventChannel? private let linkStreamHandler = LinkStreamHandler() override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? ) -> Bool { let controller = window.rootViewController as! FlutterViewController eventChannel = FlutterEventChannel(name: "poc.deeplink.flutter.dev/events", binaryMessenger: controller) GeneratedPluginRegistrant.register(with: self) eventChannel?.setStreamHandler(linkStreamHandler) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } override func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool { eventChannel?.setStreamHandler(linkStreamHandler) return linkStreamHandler.handleLink(url.absoluteString) } } class LinkStreamHandler:NSObject, FlutterStreamHandler { var eventSink: FlutterEventSink? // links will be added to this queue until the sink is ready to process them var queuedLinks = [String]() func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { self.eventSink = events queuedLinks.forEach({ events($0) }) queuedLinks.removeAll() return nil } func onCancel(withArguments arguments: Any?) -> FlutterError? { self.eventSink = nil return nil } func handleLink(_ link: String) -> Bool { guard let eventSink = eventSink else { queuedLinks.append(link) return false } eventSink(link) return true } }
Когда мы объединим обе части: часть для запуска приложения и часть для приложения в бэкграунде – мы будем контролировать все переходы пользователя по Deep Links.
Обработка Deep Links во Flutter
На этом платформенная часть готова, настало время переходить к Flutter-части. Как вы, наверное, знаете, создавать приложения на флаттере можно с помощью разных архитектурных подходов. На эту тему написано уже много статей (например вот эта), но лично мне кажется, что чистый BLoC – наиболее подходящий подход. Поэтому я подготовлю отдельный BLoC, который будет обрабатывать эти ссылки. В результате мы получим абсолютно не привязанный к UI код и сможем обрабатывать получение ссылок там, где это будет удобно.
class DeepLinkBloc extends Bloc { //Event Channel creation static const stream = const EventChannel('poc.deeplink.flutter.dev/events'); //Method channel creation static const platform = const MethodChannel('poc.deeplink.flutter.dev/cnannel'); StreamController<String> _stateController = StreamController(); Stream<String> get state => _stateController.stream; Sink<String> get stateSink => _stateController.sink; //Adding the listener into contructor DeepLinkBloc() { //Checking application start by deep link startUri().then(_onRedirected); //Checking broadcast stream, if deep link was clicked in opened appication stream.receiveBroadcastStream().listen((d) => _onRedirected(d)); } _onRedirected(String uri) { // Here can be any uri analysis, checking tokens etc, if it’s necessary // Throw deep link URI into the BloC's stream stateSink.add(uri); } @override void dispose() { _stateController.close(); } Future<String> startUri() async { try { return platform.invokeMethod('initialLink'); } on PlatformException catch (e) { return "Failed to Invoke: '${e.message}'."; } } }
Специально для тех, у кого раньше не было опыта работы с BLoC и StreamBuilders, я подготовлю пример виджета, который будет работать с этим BLoC. В основе виджета лежит StreamBuilder, который перестраивает UI в зависимости от событий, получаемых из потока.
class PocWidget extends StatelessWidget { @override Widget build(BuildContext context) { DeepLinkBloc _bloc = Provider.of<DeepLinkBloc>(context); return StreamBuilder<String>( stream: _bloc.state, builder: (context, snapshot) { if (!snapshot.hasData) { return Container( child: Center( child: Text('No deep link was used '))); } else { return Container( child: Center( child: Padding( padding: EdgeInsets.all(20.0), child: Text('Redirected: ${snapshot.data}')))); } }, ); } }
Тадам! Вот и все. Теперь все работает!
Для проверки запустим приложение тремя разными способами. Вручную и через Deep Links, сначала с URI poc://deeplink.flutter.dev, а потом с poc://deeplink.flutter.dev/parameter. Вот скриншоты того, что получилось:

Есть и другие способы работы с Deep Links. Например, можно использовать для этого Firebase Dynamic Links. Есть отличная статья о том, как их использовать с Flutter. Еще есть готовая библиотека ‘uni-links’ для подключения Deep Links – можно использовать ее. А если вы не хотите быть зависимы от сторонних библиотек, всегда можно реализовать свою. Надеюсь, моя статья поможет вам в этом!
Source Code
Исходный код описанного выше примера можно посмотреть здесь.
Немного полезной информации
Если вы дочитали статью до этого места, то, скорее всего, вы интересуетесь Flutter-разработкой). Хочу рассказать про несколько ресурсов, которые могут быть вам полезны. Не так давно была создана пара русскоязычных подкастов, имеющих прямое отношение к Flutter-разработке. Рекомендую на них подписаться: Flutter Dev Podcast (канал в телеграме), там мы обсуждаем животрепещущие вопросы Flutter-разработки, и Mobile People Talks (канал в телеграме), там обсуждаем проблемы мобильной разработки в принципе, причем с разных точек зрения. Среди ведущих Mobile People Talks – разработчики iOS, Android, ReactNative и Flutter.

ссылка на оригинал статьи https://habr.com/ru/company/epam_systems/blog/461239/
Добавить комментарий