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