Flutter в примерах. Deep Links в приложениях на Flutter

от автора

Новый мультиплатформенный фреймворк от Google – Flutter – уверенно набирает поклонников. Все больше людей интересуются этой технологией и пробуют ее как в pet-, так и в коммерческих проектах. Все больше статей и примеров появляется в рунете, но какое-то время назад я обратил внимание, что, в отличие от Medium, на Хабре в основном преобладают обзорные статьи, посвященные технологии в целом и ее преимуществам или новинкам представленным в последней версии. Текстов, посвященных конкретным кейсам, достаточно мало. Поэтому я решил, что нужно исправлять сложившуюся ситуацию. Начну не с самого распространенного кейса, но достаточно часто используемого – Deep Links.
image

Недавно передо мной возникла задача запуска 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. Вот скриншоты того, что получилось:

image

Есть и другие способы работы с 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.

image


ссылка на оригинал статьи https://habr.com/ru/company/epam_systems/blog/461239/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *