Новый мультиплатформенный фреймворк от Google – Flutter – уверенно набирает поклонников. Все больше людей интересуются этой технологией и пробуют ее как в pet-, так и в коммерческих проектах. Все больше статей и примеров появляется в рунете, но какое-то время назад я обратил внимание, что, в отличие от Medium, на Хабре в основном преобладают обзорные статьи, посвященные технологии в целом и ее преимуществам или новинкам представленным в последней версии. Текстов, посвященных конкретным кейсам, достаточно мало. Поэтому я решил, что нужно исправлять сложившуюся ситуацию. Начну не с самого распространенного кейса, но достаточно часто используемого – Deep Links.
Недавно передо мной возникла задача запуска Flutter-приложения с использованием deep links. Мне пришлось покопаться в документации и поэкпериментировать чтобы получить адекватное представление о том, как работать с ними во Flutter. В этой статье я сагрегировал результаты, чтобы тем, кто столкнется с такой же задачей, было проще разобраться.
Deep Links – это URL-адреса, которые дают пользователям возможность перейти к определенному контенту внутри мобильного приложения на iOS или Android. Это значит, что мы должны отслеживать, как было открыто приложение: стандартным способом или с помощью ссылки, и кроме того, приложение может быть уже открыто, когда был совершен переход. Значит, мы должны отслеживать переходы по ссылкам и в бэкграунде работающего приложения. Давайте разберемся, как лучше всего это сделать в Flutter.
Чтобы использовать Deep Links в нативной разработке, необходимо подготовить соответствующую конфигурацию в проекте. Для Flutter-приложения это делается абсолютно так же, как и в нативе.
В Apple-экосистеме существует два способа формирования таких ссылок: «Custom URL schemes» и «Universal Links».
Для примера я использую подход Custom URL schemes, так как он проще. Добавим в файл Info.plist такой кусок:
В экосистеме Android также есть два способа формирования ссылок с примерно такими же свойствами:
Для андроида я тоже решил не усложнять и использовал Deep Links. Добавим в AndroidManifest.xml вот это:
Таким образом мы сконфигурировали приложения для обеих платформ для схем
Итак, нативная конфигурация для каждой из платформ готова. Но кроме конфигурации нужно подготовить Platform Channels, благодаря которым нативная часть будет взаимодействовать с Flutter. И опять нужно подготовить свою реализацию как для Android, так и для iOS.
Начнем с Android. Нужно сделать всего ничего – всего лишь обработать входящий Intent в методе onCreate, создать MethodChannel и передавать в него URI, если приложение запущено через Deep Link.
В iOS все будет немного по-другому, хотя в целом то же самое: передача URI в приложение через MethodChannel. Реализовать я решил на Swift, так как с Objecttive-C дела у меня обстоят не очень хорошо)). Далее – измененный AppDelegate.swift
Так мы будем обрабатывать запуск приложения через Deep Link. А что, если переход по ссылке произошел, когда приложение уже запущено? Необходимо учесть и этот момент.
В Андроиде для этого мы переопределим метод onNewIntent и будем обрабатывать каждый входящий интент. Если это будет переход по ссылке, то будем кидать событие в созданный для этого EventChannel через специально созданный BroadcastReceiver.
Давайте сделаем то же самое в части iOS. В Swift мы должны создать FlutterStreamHandler и обработать любую ссылку, которую будем получать, пока приложение находится в фоновом режиме. Пора опять немного поменять AppDelegate.swift
Когда мы объединим обе части: часть для запуска приложения и часть для приложения в бэкграунде – мы будем контролировать все переходы пользователя по Deep Links.
На этом платформенная часть готова, настало время переходить к Flutter-части. Как вы, наверное, знаете, создавать приложения на флаттере можно с помощью разных архитектурных подходов. На эту тему написано уже много статей (например вот эта), но лично мне кажется, что чистый BLoC – наиболее подходящий подход. Поэтому я подготовлю отдельный BLoC, который будет обрабатывать эти ссылки. В результате мы получим абсолютно не привязанный к UI код и сможем обрабатывать получение ссылок там, где это будет удобно.
Специально для тех, у кого раньше не было опыта работы с BLoC и StreamBuilders, я подготовлю пример виджета, который будет работать с этим BLoC. В основе виджета лежит StreamBuilder, который перестраивает UI в зависимости от событий, получаемых из потока.
Тадам! Вот и все. Теперь все работает!
Для проверки запустим приложение тремя разными способами. Вручную и через Deep Links, сначала с URI
Есть и другие способы работы с Deep Links. Например, можно использовать для этого Firebase Dynamic Links. Есть отличная статья о том, как их использовать с Flutter. Еще есть готовая библиотека ‘uni-links’ для подключения Deep Links – можно использовать ее. А если вы не хотите быть зависимы от сторонних библиотек, всегда можно реализовать свою. Надеюсь, моя статья поможет вам в этом!
Исходный код описанного выше примера можно посмотреть здесь.
Если вы дочитали статью до этого места, то, скорее всего, вы интересуетесь Flutter-разработкой). Хочу рассказать про несколько ресурсов, которые могут быть вам полезны. Не так давно была создана пара русскоязычных подкастов, имеющих прямое отношение к Flutter-разработке. Рекомендую на них подписаться: Flutter Dev Podcast (канал в телеграме), там мы обсуждаем животрепещущие вопросы Flutter-разработки, и Mobile People Talks (канал в телеграме), там обсуждаем проблемы мобильной разработки в принципе, причем с разных точек зрения. Среди ведущих Mobile People Talks – разработчики iOS, Android, ReactNative и Flutter.
Недавно передо мной возникла задача запуска 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/channel";
@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/channel", 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/channel');
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.