[update April 19, 2020]

- due to the fact that Kotlin is now Google’s preferred language for Android app development and that Flutter Android API get some changes, the native Android part of example has been updated and moved from Java to Kotlin

[/update]

Some time ago I faced the task of launching a Flutter application using Deep links and I worked with the documentation and experimented to get an adequate impression of how to work with Deep Links in Flutter. In this article, I decided to aggregate the results of my investigation so that those who will face the same task in the future could find it easier to solve it with this.

Deep linking is the ability to link into a specific page inside of a native iOS or Android mobile app (as opposed to a mobile website). Deep links let you open up specific content pages (as opposed to a front page of a website) and pass through custom data (like promo codes, etc.) This means that we must handle the way, an application was opened, manually or using a link. In addition, the application may already be open when the link is clicked, this means we need to handle link clicks in the background of a running application. Let’s see how to do this best in Flutter.

A little bit about Deep Links configuration

Before you start working with links, it’s necessary to configure appropriate permissions. For the Flutter app, permissions are configured in exactly the same way, as the corresponding native configurations.

iOS: There are two ways of deep linking, “Custom URL schemes” and “Universal Links”.

Custom URL schemes allow work with any scheme, with no host specification. But it’s necessary to be sure that a scheme is unique, and this approach won’t work without an installed application. Custom URL schemas give you an opportunity to work with URL: your_scheme://any_host

Universal Links are a little bit more complex. They allow work with an https scheme only and with a specified host, entitlements and a hosted file — apple-app-site-association. Universal links give you an opportunity to start your app by URL: https://your_host

Let’s try the approach on Custom URL schemes, it’s easier. It’s necessary to add a piece to the Info.plist file (we configure the application for URI poc://deeplink.flutter.dev):

<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: There are also two ways with approximately the same meaning, “App Links” and “Deep Links”.

App Links allow to work with an https scheme and require a specified host, plus a hosted file. Look like Universal Links in iOS.

Deep Links allow to have a custom scheme and do not require a host or a hosted file, like Custom URL schemes in iOS

For Android, let’s go along a simpler path as well, and connect the Deep Links feature. Add the following piece to the android manifest:

<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

So the native configuration for each platform is ready now. But besides the configuration, it’s necessary to prepare Platform Channels for connecting Flutter with the native part. And again, we have to prepare a different implementation for each platform.

In Android for this, we have to catch the incoming Intent in the onCreate method, create a MethodChannel and send a URI into it if the application is launched via a deep link.

private val CHANNEL = "poc.deeplink.flutter.dev/channel" private var startString: String? = null override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {

GeneratedPluginRegistrant.registerWith(flutterEngine)



MethodChannel(flutterEngine.dartExecutor, CHANNEL).setMethodCallHandler { call, result ->

if (call.method == "initialLink") {

if (startString != null) {

result.success(startString)

}

}

}

} override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)



val intent = getIntent()

startString = intent.data?.toString()

}

In iOS, it will be a little bit different. But in general, we see the same: sending the URI into the application through MethodChannel. The following implementation is done using Swift. This is changed 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)

}

Thus, we handle the start of our application via the deep link. And what should be done if the link is followed when the application is running already? This case needs to be thought through and taken into account, too.

In Android, we will override onNewIntent method to process each incoming Intent; if this is a Deep Link following action, we will send an event into the EventChannel, especially added for this into configureFlutterEngine method, via a specially created Android BroadcastReceiver.

private val EVENTS = "poc.deeplink.flutter.dev/events"

private var linksReceiver: BroadcastReceiver? = null



override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {

GeneratedPluginRegistrant.registerWith(flutterEngine)



EventChannel(flutterEngine.dartExecutor, EVENTS).setStreamHandler(

object : EventChannel.StreamHandler {

override fun onListen(args: Any?, events: EventSink) {

linksReceiver = createChangeReceiver(events)

}



override fun onCancel(args: Any?) {

linksReceiver = null

}

}

)

} override fun onNewIntent(intent: Intent) {

super.onNewIntent(intent)

if (intent.action === Intent.ACTION_VIEW) {

linksReceiver?.onReceive(this.applicationContext, intent)

}

}





fun createChangeReceiver(events: EventSink): BroadcastReceiver? {

return object : BroadcastReceiver() {

override fun onReceive(context: Context, intent: Intent) { // NOTE: assuming intent.getAction() is Intent.ACTION_VIEW

val dataString = intent.dataString ?:

events.error("UNAVAILABLE", "Link unavailable", null)

events.success(dataString)

}

}

}

Let’s do the same in iOS part. In Swift, we should create FlutterStreamHandler and handle any link that we got when an application was in the background. Let’s modify AppDelegate.swift again



@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

}

}

When we combine both parts, for initiation and for background, we will control all Deep Links on which the transition was made.

Deep Links processing in Flutter BLoC

On this step, the platform part is done, let’s take a look at the Flutter part. As you probably know, we have an opportunity to make applications on Flutter using different architecture approaches. Many articles have been written on this topic already (for example this), but personally, I think that pure BLoC is the most convenient and suitable of them. Therefore, I am preparing BLoC that will notify each time about a Deep Link following action. In this way, it will turn out that our code is not tied to the UI, and we will be able to embed it to handle receiving links where it is convenient. I would recommend using it as part of the main BLoC unit that is working with the global state.

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

}

}

}

If you haven’t got any experience with the BLoCs and StreamBuilders before, I’ll prepare a code example of the widget that will work with this BLoC. At the core of the widget is a StreamBuilder, that rebuilds himself depending on the objects getting from the stream.

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

}

},

);

}

}

That’s all. Everything works! Let’s launch the application manually and then twice follow links poc://deeplink.flutter.dev and poc://deeplink.flutter.dev/parameter. There are screenshots of result.

In fact, there are other ways to work with Deep Links. For example, you can use Firebase Dynamic Links for this. The great article has been written about this by Nikita Gandhi. And besides, there is a library ‘uni-links’ for deep linking by Evo Stamatov. But if you do not want to be dependent on the 3rd party libraries, you can always implement your own solution. I hope my article will help you with this!

Source Code

You can checkout the source code of the example above from this github repo.

Finally, if you have any questions that you did not find the answer in the article, you can write to me on Twitter directly) I will be happy to help! :)