Handle deep links in the Android apps with flutter
Tags: deeplinks, android, flutter
Published: 2020-09-06 03:33:15

# What are deep links?

Deep links are a type of link that send users directly to an app instead of a website.

# Why we need this?

To provide a seamless user experience in both web and mobile. Another reason is that when a business has a web site and a mobile app, it can escape the “Mobile first” web design patters by using deep links.

# Implementation

find `AndroidManifest.xml` file under `android/app/src/main`

Add these links inside `<activity>`

```
 <intent-filter android:label="InviteLink">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data
        android:scheme="https"
        android:host="only4.dev"
        android:pathPrefix="/invite"
    />
</intent-filter>
```

Android operating systems provides APIs to register certain action to some apps, for example when we click on a `mp3` file Android prompts user to select an app from the list of supported apps. 

If I click on a link which starts from `only4.dev/invite` it will always prompt to open this link with the mobile app.

## Lets handle the link in the app

There can be 2 types of events: 
 1. When the app is not running
 2. when the app is already running.

We create 2 types of messages for that and also create a Broadcast receiver.

**A broadcast receiver (receiver) is an Android component which allows you to register for system or application events. All registered receivers for an event are notified by the Android runtime once this event happen**

```java
private static final String CHANNEL = "initial";
private static final String EVENTS = "eventWhileAppIsRunning";
private String startString;
private BroadcastReceiver linksReceiver;
```

Let’s handle the cases. We create a new `MethodChanenel` for the 1st case and new `EventChannel` for the 2nd case.

**On the Android side `MethodChannel` Android (API) and on the iOS side `FlutterMessageChannel` (API) is used for receiving method calls and sending back result. If required, method calls can also be sent in the reverse direction, with the Android/IOS platform acting as client and method implemented in Dart.**

**An `EventChannel` is used when you want to stream data. This results in having a Stream on the Dart side of things and being able to feed that stream from the native side. (In other words when the app is already running).**
```java
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);
          }
        }
      }
    }
  );

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

  if (data != null) {
    startString = data.toString();
    if(linksReceiver != null) {
      linksReceiver.onReceive(this.getApplicationContext(), intent);
    }
  }
}
```

Take your time to read and understand this.

ake your time to understand this if you need to implement deep links.

here is final MainActivity.java
```java
package my.app.com;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;

import io.flutter.app.FlutterActivity;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugins.GeneratedPluginRegistrant;
import io.flutter.plugins.GeneratedPluginRegistrant;

public class MainActivity extends FlutterActivity {
  private static final String CHANNEL = "initial";
  private static final String EVENTS = "eventWhileAppIsRunning";
  private String startString;
  private BroadcastReceiver linksReceiver;

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

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

    if (data != null) {
      startString = data.toString();
      if(linksReceiver != null) {
        linksReceiver.onReceive(this.getApplicationContext(), intent);
      }
    }
  }

  @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);
        }
      }
    };
  }
}
```
## Next step is to pass these events to flutter code

We create a `StreamController()` in a `DeepLinkBloc` class.

Create the constructer for this class

```dart
DeepLinkBloc() {
    startUri().then(_onRedirected);
    stream.receiveBroadcastStream().listen((d) => _onRedirected(d));
  }
```

When ever we receive an event it calls the `_onRedirected` method. Now we finally have the initial link in the flutter code.

Here’s the full code for the class.
```dart
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';

abstract class Bloc {
  void dispose();
}

class DeepLinkBloc extends Bloc {
  DeepLinkBloc() {
    startUri().then(_onRedirected);
    stream.receiveBroadcastStream().listen((d) => _onRedirected(d));
  }

  // Initial event channel
  static const platform = MethodChannel('initial');

  // Runtime Event channel
  static const stream = EventChannel('eventWhileAppIsRunning');

  final StreamController<String> _stateController = StreamController();

  Stream<String> get state => _stateController.stream;

  Sink<String> get stateSink => _stateController.sink;

  //Adding the listener into constructor

  void _onRedirected(String uri) {
    debugPrint(uri);
    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}'.";
    }
  }
}
```

Now simply wrap the widgets in `Provider<DeepLinkBloc>.value(value: _bloc)` where you want to use the links.

Lets create a new widget which uses this provider.

```dart
class DeepLinkWrapper extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final _bloc = Provider.of<DeepLinkBloc>(context);

    return StreamBuilder<String>(
      stream: _bloc.state,
      builder: (context, snapshot) {
        // if app is started normally, no deep link is clicked show your old home page widget
        if (!snapshot.hasData) {
        return Container(
          child: const HomePage(),
        );
      } else {
        final splitInviteLink = snapshot.data.split('/');
        final inviteToken = splitInviteLink[splitInviteLink.length - 1];

        return RegisterStartPage(key: UniqueKey(),inviteToken: inviteToken,);
      }
    });
  }
}
```
Just this last change and we are good to go. Make this modification in your `main.dart` file.

```dart
 home: Scaffold(
   body: Provider<DeepLinkBloc>(
     builder: (context) => _bloc,
     dispose: (context, bloc) => bloc.dispose(),
     child: DeepLinkWrapper()
   )
 )
```

## Here is the code for the final `MyApp` class in main.dart.

```dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final _bloc = DeepLinkBloc();

    return MultiProvider(
      providers: [
        Provider<DeepLinkBloc>.value(value: _bloc),
        // ...other providers
      ],
      child: MaterialApp(
        debugShowCheckedModeBanner: false,
        localizationsDelegates: const [
          AppLocalizationsDelegate(),
          GlobalMaterialLocalizations.delegate,
          GlobalWidgetsLocalizations.delegate,
        ],
        supportedLocales: const [
          Locale('de'),
          Locale('en'),
        ],
        routes: {
          HomePage.route: (context) => const HomePage(),
          LoginPage.route: (context) => LoginPage(),
        },
        home: Scaffold(
          body: Provider<DeepLinkBloc>(
            create: (context) => _bloc,
            dispose: (context, bloc) => bloc.dispose(),
            child: DeepLinkWrapper()
          )
        )
      ),
    );
  }
}
```