Deep Links and Flutter applications. How to handle them properly.

Aleksandr Denisov
Jul 3 · 7 min read

A little bit about Deep Links configuration

<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>
<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>

Platform Channels preparation

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();
}
}
@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)
}
  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);
}
;
}
};
}
}

@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 processing in Flutter BLoC

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}'.";
}
}
}
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}'))));
}
},
);
}
}

Source Code

Flutter Community

Articles and Stories from the Flutter Community

Aleksandr Denisov

Written by

EPAM Systems, Co-Head of Flutter Competency, organizer of GDG Nizhny Novgorod. Twitter: @ShuregDenisov

Flutter Community

Articles and Stories from the Flutter Community