Flutterのuni_linksを使ってみた

久しぶりにブログ書きます!

Flutterでディープリンクを実装しようと思っており、実装方法を調べているとuni_linksというライブラリを見つけました。 今回はこのライブラリを使って、DeepLinksの実装をしていこうと思います。

uni_linksとは

pub.dev

ディープリンク(AndroidのAppLinks, DeepLinks、iOSのUniversalLinks CustomUrlScheme)を支援するプラグイン
中ではディープリンクが各プラットフォームに届いた際に、 MethodChannelEventChannel を用いて、Flutter側に通知しています。(違っていたらすみません。)

実装例

今回の実装例では、AndroidのDeepLinks、iOSのCustomUrlSchemeで実装していきます。

今回実装したコードはこちらです。 github.com

前提

遷移は下記の図の通りです。

f:id:idonuntius:20200503021453p:plain
遷移図

MainPageからExample1Page、Example2Page、Example3Pageに遷移ができ、各Exampleのページは自身のページと各Exampleページに遷移ができます。

またExample1Page、Example2Pageではリンクのクエリを表示するようにしています。

インストール

pubspec.yml に下記を記載する。

# ...

dependencies:

  # ...

  uni_links: ^0.2.0

# ...

iOS, Android側の設定

AndroidiOSディープリンクの情報を記載する必要があります。
各プラットフォームでディープリンクを実装する時と同じ方法です。

iOS

ios/Runner/Info.plist に記載します。

<plist version="1.0">
<dict>
    <!-- ↓↓ 追加 -->
    <key>CFBundleURLTypes</key>
    <array>
        <dict>
            <key>CFBundleTypeRole</key>
            <string>Editor</string>
            <key>CFBundleURLName</key>
            <string>example1</string>
            <key>CFBundleURLSchemes</key>
            <array>
                <string>com.idonuntius.deeplinkflutter</string>
            </array>
        </dict>
    </array>

    <!-- ... -->

</dict>

Android

android/app/src/main/AndroidManifest.xml に記載します。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.idonuntius.deep_link_flutter">
    <application
        android:name="io.flutter.app.FlutterApplication"
        android:label="deep_link_flutter"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>

            <!-->↓↓ 追加<-->
            <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="com.idonuntius.deeplinkflutter" />
            </intent-filter>
        </activity>

    <!-->...<-->

Flutter側の設定

Example1Page, Example2Page, Example3Pageの実装

今回は遷移したページを用意したいだけなので、3つとも StatelessWidget で実装。
example1_page.dartexample2_page.dart では、クエリを表示させたいので、 _query の値をコンストラクタで受け取る。
下記は example1_page.dart の例です。

import 'package:flutter/material.dart';

class Example1Page extends StatelessWidget {
  final String _query;

  Example1Page(this._query);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          'Example1',
        ),
      ),
      body: Center(
        child: Text(_query),
      ),
    );
  }
}

main.dartの修正

homeを MainPage() に変更しておきます。

import 'package:deep_link_flutter/view/main_page.dart';
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MainPage(), // ← ここを変更
    );
  }
}

下準備は終わりです。 ではメインのMainPageを実装していきます。

MainPage

前提

MainPageの部分の実装は下記の図です。

f:id:idonuntius:20200503021524p:plain
実装図

下記のように実装していきます。

  1. MainViewModelでuni_linksのメソッドを使いディープリンクを受け取る
  2. 受け取ったディープリンクリンクをRoutePatternに変換
  3. RoutePatternをStreamで流す
  4. MainPageで受け取ったRoutePatternをもとに遷移
RoutePattern

下記のように実装。
sealed class的なものを作成しました。
example1, example2, example3用の値を作成し、example1, example2ではqueryのパラメータを持つようにしています。

abstract class RoutePattern {
  RoutePattern();

  factory RoutePattern.example1(final String query) = RoutePatternExample1;

  factory RoutePattern.example2(final String query) = RoutePatternExample2;

  factory RoutePattern.example3() = RoutePatternExample3;

  R when<R>({
      final R Function(RoutePatternExample1 state) example1,
      final R Function(RoutePatternExample2 state) example2,
      final R Function(RoutePatternExample3 state) example3,
  }) {
    if (this is RoutePatternExample1) {
      return example1(this);
    } else if (this is RoutePatternExample2) {
      return example2(this);
    } else if (this is RoutePatternExample3) {
      return example3(this);
    } else {
      throw StateError;
    }
  }
}

class RoutePatternExample1 extends RoutePattern {
  final String query;

  RoutePatternExample1(this.query);
}

class RoutePatternExample2 extends RoutePattern {
  final String query;

  RoutePatternExample2(this.query);
}

class RoutePatternExample3 extends RoutePattern {}
MainViewModel

MainViewModelは下記のように実装。

class MainViewModel {
  final _routePatternController = StreamController<RoutePattern>();

  MainViewModel() {
    _loadNextRoute();
  }

  void dispose() {
    _routePatternController.close();
  }

  Stream<RoutePattern> get nextRoute => _routePatternController.stream;

  void _loadNextRoute() {
    getInitialUri().then((uri) {
      _streamRoutePattern(uri);
    });
    getUriLinksStream().listen((uri) {
      _streamRoutePattern(uri);
    });
  }

  void _streamRoutePattern(Uri uri) {
    if (uri != null) {
      switch (uri.host) {
        case 'example1':
          _routePatternController.sink.add(RoutePattern.example1(uri.query));
          break;
        case 'example2':
          _routePatternController.sink.add(RoutePattern.example2(uri.query));
          break;
        case 'example3':
          _routePatternController.sink.add(RoutePattern.example3());
          break;
      }
    }
  }
}

ディープリンクのURLはuni_linksの getInitialUri()getUriLinksStream()Uri型で受け取っています。2つのメソッドの説明は下記です。

  • getInitialUri : ディープリンクによってアプリが起動したとき呼ばれる
  • getUriLinksStream : アプリが起動している場合呼ばれる

他にもUri型ではなくString型で返すメソッドやStreamではなくFutureで返すメソッドなどもあるので、uni_linksのコードを確認してみてください。

github.com

受け取ったUri_streamRoutePatternRoutePattern に変換し、Streamで流します。

MainPage

下記のように実装しました。

class _MainPageState extends State<MainPage> {
  final _mainViewModel = MainViewModel();

  @override
  void initState() {
    super.initState();

    _mainViewModel.nextRoute.listen((routePattern) {
      Navigator.push(context, _nextPageRoute(routePattern));
    });
  }

  @override
  void dispose() {
    _mainViewModel.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          'Main',
        ),
      ),
      body: Container(),
    );
  }

  Route _nextPageRoute(RoutePattern routePattern) {
    Widget page;
    routePattern.when(
      example1: (pattern) => page = Example1Page(pattern.query),
      example2: (pattern) => page = Example2Page(pattern.query),
      example3: (pattern) => page = Example3Page(),
    );
    return MaterialPageRoute(builder: (context) => page);
  }
}

MainViewModelの nextRoute から流れてきた RoutePattern を取得し、その値を元に _nextPageRoute でRoute型に変換し遷移処理をしています。

動作確認

ちゃんと遷移することを確認できました。

f:id:idonuntius:20200503021601g:plain

最後に

自分で実装するとなると結構大変なディープリンクですが、uni_linksを使うと簡単に実装することができました。
間違えている部分があった場合は、教えていただけると幸いです。
最後まで読んでいただき、ありがとうございました。