DEV Community

loading...

Handle deep links in the Android apps with flutter

mohitkyadav profile image Mohit Kumar Yadav Updated on ・5 min read

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>
Enter fullscreen mode Exit fullscreen mode

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

private static final String CHANNEL = "initial";
private static final String EVENTS = "eventWhileAppIsRunning";
private String startString;
private BroadcastReceiver linksReceiver;
Enter fullscreen mode Exit fullscreen mode

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

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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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);
        }
      }
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Next step is to pass these events to flutter code

We create a StreamController() in a DeepLinkBloc class.

Create the constructer for this class

DeepLinkBloc() {
    startUri().then(_onRedirected);
    stream.receiveBroadcastStream().listen((d) => _onRedirected(d));
  }
Enter fullscreen mode Exit fullscreen mode

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.

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}'.";
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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,);
      }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Just this last change and we are good to go. Make this modification in your main.dart file.

 home: Scaffold(
   body: Provider<DeepLinkBloc>(
     builder: (context) => _bloc,
     dispose: (context, bloc) => bloc.dispose(),
     child: DeepLinkWrapper()
   )
 )
Enter fullscreen mode Exit fullscreen mode

Here is the code for the final MyApp class in main.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()
          )
        )
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Discussion (3)

pic
Editor guide
Collapse
thephenomenal10 profile image
sahyog saini

how that _bloc comes in main file , it gives undefined in my code,
please reply ASAP

Collapse
mohitkyadav profile image
Mohit Kumar Yadav Author

@sahyog I updated the post with full code of MyApp class.

Collapse
devhammed profile image
Hammed Oyedele

Well, there is a package that does just that with support for iOS too: pub.dev/packages/uni_links