June 30, 2021

FlutterでContextual(Deferred) Deep Linkする

Contextual(Context, Deferred) Deep Linkとは

この挙動の名前をGoogle, Appleははっきり定義はしていません。(多分)
広告系のSDKとかではDeferred Deep Linkとも呼ばれています。[1][2]
(日本ではContext Deep Linkと呼んでる人も多いような🤔)

簡単に説明すると通常のDeep Linkは特定のURLに遷移するとアプリを起動して、URLに応じて任意の画面を開く動作のことを指します。 一方でContextual Deep Linkは基本的な動作は通常のDeep Linkと同じなのですが、アプリがインストールされていない場合にストアにリダイレクトしてダウンロードさせた後に任意の画面を開きます。 ストア経由のダウンロードでもDeep Linkの情報を引き継いでいるためContextual Deep Linkと呼ばれることが多いようです。

ユーザ側の挙動としては特に違和感はありませんが、通常の実装(Install Referrerを参照しない場合)ではストアからダウンロードされて初回起動されるため遷移元の情報がなくDeep Linkの挙動はしません。

実現方法

FlutterでこのContextual Deep Linkを実現する一番簡単な方法はFirebaseのDynamic Links を使う方法です。

firebase_dynamic_links | Flutter Package

Flutter plugin for Google Dynamic Links for Firebase, an app solution for creating and handling links across multiple platforms.

ありがたいことにFirebase Dynamic Linksとこのライブラリを利用していれば、ストア経由でダウンロードした場合でも通常のDeep Linkと変わらない挙動をしてくれます。

ちなみに、Firebaseを使わない場合はInstall Reffererを使ってインストール後の処理を書いて、任意の画面にDeepLinkさせるのが一般的だと思います。
Androidは昔Broadcast Intentを使って実現する必要があり、若干面倒だったのですが最近ではPlay Install Referrer Library というのがあり、簡単に実装できるようになっています。Referrer自体は参照できますが、Deep Link部分との実装が分離してしまうためできれば一つにまとめたいです。

細かい設定方法はドキュメントに書いてあるのでここでは説明しません。

例えば https://myhost.com のURLをFirebase側に設定しておけば linkパラメータで任意のURLを渡すことができます。 渡すlinkのURLにパラメータをつけるとアプリ側でその値を元に処理できます。
apn, ibi, isiはアプリがインストールされていない場合にストアに飛ばすために必要なパラメータになります。詳しくはこのあたり に書いてあります。

パラメータをつけたDeep Linkの例はこのようになります。

https://myhost.com/?link=https://deeplink.com/path?payload=value&apn=android.app&ibi=ios.app&ibi=12345

ちなみにFirebase Dynamic Linksでは d=1 を付与するとURLに飛んだ際の挙動を視覚的に確認できます。

https://myhost.com/?link=https://deeplink.com/path?payload=value&apn=android.app&ibi=ios.app&ibi=12345&d=1

dynamic link preview

アプリがインストールされていない場合はStoreに飛ぶ等の挙動がとてもわかり易いです。例えば、apnやibiパラメータをつけない場合はStoreではなくWeb Linkに飛ぶなどが実際に試さなくてもわかるのでデバッグはとても助かりました。

Flutterでの実装

アプリ起動中またはバックグラウンドで該当リンクを開いた場合 onLink() でフックできます。
一方アプリがまだ起動していない状態でリンクを開いた場合は onLink() ではフックできないため getInitialLink() 経由で取得できます。
Contextual Deep Link等の場合は getInitialLink() にURLが入っているのでこちらのURLを使います。
parseするメソッドを用意しておいてこのような感じでやるのが良いかなと思います。
これでContextual Deep Linkにも対応できます。かなり簡単ですね。ありがとうGoogle様

 Future<void> init() async {
    FirebaseDynamicLinks.instance.onLink(
      onSuccess: (PendingDynamicLinkData? dynamicLink) async {
        _parseDeepLink(dynamicLink?.link);
      }, onError: (OnLinkErrorException e) {
        print(e.message);
      },
    );

    _parseDeepLink(
      (await FirebaseDynamicLinks.instance.getInitialLink())?.link,
    );
  }

  void _parseDeepLink(Uri? deepLink) {
    if (deepLink == null) {
      return;
    }

    // parse link
    // Navigator.push()等
  }

ログイン処理があるアプリの場合

すこしわかりづらいかもしれませんが、応用的な話をします。 ここは+αの部分になるので読み飛ばしても構いません。

上記の処理でDeep LinkのURLを取得はできますが、実際のプロダクトではそううまくはいきません。
例えば、アプリにはログインが必要で、ログインをしていない場合はログイン画面[Login Page]、ログインしている場合はホーム画面[Home Page]に遷移するアプリだとします。 ログインが必要なので、当然初回起動時にはユーザ登録が必要になります。
そうすると、Deep Linkの処理を起動時に行うと"ユーザ登録終了後"に任意の画面に遷移する処理を書く必要があります。 チュートリアルなどがあると複数画面またぐ必要があったり画面間でフラグを引き渡すのはあまりやりたくありません。

一般的だとは思いますが、私がアプリを作る際はよくUI(画面)の無い起動時用のページ(Splashの場合もある)としてLaunch Pageを用意します。 Launch Pageでは初期化の処理をして、ログインしていない場合はLogin Page, ログインしている場合はHome Pageに遷移する処理を行います。

               |--ログイン済み--> [Home Page]
[Launch Page] -|
               |-- 未ログイン --> [Login Page] --ログイン後--> [Home Page] 

Deep Linkの初期化自体はLaunch Page表示のタイミングで行います。 その場合に困るのがログインしていない場合のDeep Linkです。ログインしていない場合はDeep Link先の画面を開けないためログイン後に遷移させたいです。

今携わっているアプリではDispathcerを作っていて、Deep Linkの処理自体はLaunchのタイミングで行いますが、Deep Linkのパース後の遷移の動作はHome Pageで行っていて、Home PageのViewModelがDispatchされてくる値をObserveしています。

abstract class DeepLinkDispatcher {
  void closeStream();

  Stream<InAppDeepLink> getDeepLinkStream();

  void notifyDeepLink(InAppDeepLink inAppDeepLink);
}

class DeepLinkDispatcherImpl extends DeepLinkDispatcher {
  final BehaviorStreamNotifier<InAppDeepLink> _deepLinkStream =
      BehaviorStreamNotifier<InAppDeepLink>();

  @override
  void closeStream() {
    _deepLinkStream.close();
  }

  @override
  Stream<InAppDeepLink> getDeepLinkStream() {
    return _deepLinkStream.stream;
  }

  @override
  void notifyDeepLink(InAppDeepLink inAppDeepLink) {
    _deepLinkStream.add(inAppDeepLink);
  }
}
{
  ...

  final DeepLinkDispatcher deepLinkDispatcher;

  Future<void> init(void argument) async {
    FirebaseDynamicLinks.instance.onLink(
        onSuccess: (PendingDynamicLinkData? dynamicLink) async {
      _dispatchDeepLink(dynamicLink?.link);
    }, onError: (OnLinkErrorException e) async {
      print(e.message);
    });

    _dispatchDeepLink(
      (await FirebaseDynamicLinks.instance.getInitialLink())?.link,
    );
  }

  void _dispatchDeepLink(Uri? deepLink) {
    if (deepLink == null) {
      return;
    }
    
    // parse
    final inAppDeepLink = InAppDeepLink.parse(deepLink.toString());
    deepLinkDispatcher.notifyDeepLink(inAppDeepLink);
  }

  ...
}
{
  ...

  void _observeDeepLink() {
    _observeDeepLinkService(null, (InAppDeepLink inAppDeepLink) {
      // Navigator.push()等
    });
  }
  ...
}

このやり方のポイントはDispathcerのBehaviorStreamNotifierの部分になります。
DartにはRxにあるBehaviorSubject のようなものはありません。 そのため、通常のStreamで実装してしまうと起動時にDeep Linkをパースして瞬時にDispatchするので、Home Pageを開いた段階ではDeep Linkの情報を受け取れません(Dispatchの瞬間にObserverがいないため)。
そこで BehaviorStreamNotifier を使うことでObserve時に値を受け取れるため、Contextual Deep Linkでも通常のDeep LinkでもHome Pageが開いた段階で最後の値を受け取って任意の画面に遷移できます。 BehaviorStreamNotifierの実装はこちらを参考にしてみてください。

DartのStreamでRxのBehavior Subjectを再現する - AABrain

Rxを使わずにDartのStreamだけでBehavior Subjectを再現します

まとめ

Firebase Dynamic Linksを使うことでContextual Deep Linkを比較的簡単に実装できました。
もし仮にDynamic Linksを使わずにDeep Linkを実現している場合はInstall Referrerの実装をMethod Channel経由で行う必要があり結構大変だと思います。(AndroidはReferrerのライブラリがありました) Flutterに限らずアプリでDeep Linkを実現したいなら現状Dynamic Linksを使う方が楽になるのでオススメです。


[1] Business Insider
[2] Wikipedia

© AAkira 2021