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 такой кусок:

CFBundleURLTypes


CFBundleTypeRole
Editor
CFBundleURLName
deeplink.flutter.dev
CFBundleURLSchemes

poc



Android


В экосистеме Android также есть два способа формирования ссылок с примерно такими же свойствами:

  • Deep Links — (так же, как и Custom URL schemes в iOS) позволяют использовать пользовательскую схему независимо от того, какой хост будет указан.
  • App Links — позволяют работать только со схемой https и с определенным хостом (так же, как Universal Links в iOS), и также необходимо подтверждение прав на использование этого хоста с помощью размещения на сервере Digital Asset Links JSON файла.


Для андроида я тоже решил не усложнять и использовал Deep Links. Добавим в AndroidManifest.xml вот это:


  
  
  
  


Таким образом мы сконфигурировали приложения для обеих платформ для схем 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 _stateController = StreamController();

  Stream get state => _stateController.stream;

  Sink 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 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(context);
    return StreamBuilder(
      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

© Habrahabr.ru