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を使うと簡単に実装することができました。
間違えている部分があった場合は、教えていただけると幸いです。
最後まで読んでいただき、ありがとうございました。

Swiftで書いて覚えるTDDを読んだ

TDDを社内で少しずつ学んでいく中で、もっとTDDについて学びたいと思い、「Swiftで書いて覚えるTDD」を読みました。
この本では実際にツーカードポーカーの実装をしていきます。

booth.pm

下記が本を読みながら書いた自分のコードです。

github.com

そもそもTDDって?

TDD(テスト駆動開発: test-driven development)は開発手法です。 機能が期待する結果を、プロダクトコードよりも先に失敗するコードを書き、実装のゴールを設定します。
(プロダクトコードよりも先にテストを書くことを テストファースト を呼ぶ)

テストしやすい = 良い設計 であることが多く、テストを先に書くことで良い設計を作ることができます。

詳しくは「Swiftで書いて覚えるTDD」にも書かれているので、気になる方は読んでみてください。

TDDのサイクル

TDDのサイクルは下記のよう行います。

  1. 設計
  2. テストコードを書く(レッド: テスト失敗)
  3. プロダクションコードを書く(グリーン: 成功)
  4. リファクタリング(グリーン: 成功)

このサイクルを レッド・グリーンリファクタリング と呼ぶみたいです。

本の構成

  1. TDDとは
  2. 書いて覚えるTDD
  3. 2018年現在のSwiftでのTDD開発
  4. 参考文献

実際に書くのが2です。

本のまとめ

ToDoリスト

実際に作っていく中で、設計時にToDoリストを作るのがいいなと思いました。 例えば、下記の要件があった場合、

・任意のカード 1 枚の文字列表記を取得してください。
・スート (suit) と ランク (rank) を与えて カード (card) を生成してください。 ・生成したカードから文字列表記 (notation) を取得してください。

下記のようにToDoに落とし込んでいました。

- [ ] Card を定義して、インスタンスを作成する
- [ ] Card のインスタンスから文字列表記 (notation) を取得する

こうすることで、やるべきことを忘れずに目の前のことに集中できることや、その仕事が終わったかどうかを確認することができます。
アジャイル開発におけるストーリを、もっと詳しくかつ技術よりにした感じかなという印象を受けました。

Mock

3章からTDDを実践していく中での課題点が書かれていました。
ここで紹介していたのがCuckooというモックライブラリです。(クークーと呼ぶみたいです)

github.com

CuckooはJavaのMockitoにインスパイヤされたライブラリで、メリットとして

  • モックをしなくてもよい
  • 使い方がなじみやすい

があるのですが、デメリットとして

  • モック化するファイルの指定が少しめんどくさい
  • ジェネリクス非対応

らしいです。
使い所としてRxSwiftを使っていないプロジェクトと書いてあり、理由としてRxSwiftでジェネリクスを使った開発には適していないからだそうです。
※ 現在もまだ対応していないようです。

Androidも開発している自分にとって、Mockitoとほぼ同じ形で実装できる嬉しい反面、RxSwiftでジェネリクスを時々使用しているので辛いという印象。
現在まだCuckoo使ってのRxSwiftのジェネリクス部分のテストは書けていない状況です・・・。

まとめ

Swiftで書いて覚えるTDDを読んでみて、実際に書けるのは身につきやすいのでいいなと思いました。
メソッドなどの小さい単位でレッド・グリーンリファクタリングをすることで、すごく心持ちがよく実装ができると思います。 TDDは知っているけど、実際にやったことがないという初心者の方におすすめの本だなと感じました。

potatotips#44に参加しました!(Android編)

遅くなりましたが先月株式会社エウレカさんで開催されたpotatotips#44にAndroidブログ枠として参加させていただきました。

potatotips.connpass.com

自分はiOSのアプリしか開発したことがなく知識不足なので、間違っている部分がありましたらご指摘をお願いいたします。

Danger for Android

@wasabeef_jpさん

speakerdeck.com

Danger自体は聞いたことがあったのですが、実際どのようなものなのかは知りませんでした。
今回の発表ではCIを使用してGithub上でコードレビューを自動化させるというお話でした。

Gemfileで必要なGemをインストール

Dangerfileにレビューの項目とAndroid lintの設定

CIに組み込む

という流れでした。

警告が出たコードに関して直接PRに提示してくれるのは便利だと感じました。
また空白は行数チェックなどは人の目でチェックするのではなく自動化して任せる方が良いですね。

AndroidKiosk端末化 ~ダイジェスト版~

@tomoya0x00さん

qiita.com

Kioskと聞いて駅のあれだと思ってしまいました(笑)
スライドにも書いてあるのですが、wikiで調べたところKiosk端末下記のようなもののことらしいです。

インタラクティブなキオスクは、通信、商取引、エンターテイメント、または教育のための情報およびアプリケーションへのアクセスを提供する特殊なハードウェアおよびソフトウェアを特徴とするコンピュータ端末である。

ref.) https://en.wikipedia.org/wiki/Interactive_kiosk

今回の発表の内容は下記の内容を実現するため実装方法のお話でした。

  • 通常操作では、専用アプリ(Kioskアプリ)以外の画面には遷移できない
  • 端末を再起動しても、強制的にKioskアプリが立ち上がる
  • 特定の手順を踏むと、Kioskアプリ以外の画面に遷移できる

最近見かけるようになった飲食店の注文するためのタブレットようなものでしょうか。

実装はAndroid 5.0で追加されたDevice Owner、Screen Pinningを使用するという方法でした。
またDevice Ownerを使用する場合は端末の初期化が必要のようです。

今回発表されたソースについてはこちらに上げてくださっています。

github.com

VectorDrawable導入しようと思ったけど断念した話

TakuSemba(仙波拓 (@takusemba) | Twitter)さん

(資料は上げられていませんでした。)

今回はAbemaTVでVectorDrawableを導入しようしたが、制限やバグで導入を断念したお話でした。

まずVectorDrawableはpathが800文字までという制限があるらしいです。
AbemaTVでは100個ほどの画像があるらしく、pathが超えてしまったsvgを手作業で修正していったというお話しをしていました。

また下記のURLのような「VectorDrawableのfillType="evenOdds"が5.0~6.0でうまく表示されない問題」の影響があり断念せざる終えなかったとおっしゃっていました。

https://issuetracker.google.com/issues/67754527

vector画像を使用出来れば複数のサイズの画像を用意する必要が無くなるのでかなり楽になりますが、まだまだ導入は辛いようでした。
iOSでもvector画像を使用出来るので試してみたいと思います。

Color blending on Android

@shaunkawanoさん

speakerdeck.com

2つの色を混ぜたい時にどうやるのか、というお話でした。
方法としてAndroidのsupport.v4.graphicsで提供している ColorUtilsblendARGB を使用して変更していくというものでした。

ColorUtils  |  Android Developers

ColorUtils.blendARGB(color1, color2, ratio)

だけで色同士を混ぜられるのは便利ですね。
また ViewPager#onPageScrolled との相性が良いかもとのことでした。
スクロールなどで徐々に色を変更したい時に ratio を動的に変更すれば良いのでiOSに比べて楽に実装ができるのではないのかなと思いました。

Twitterでは「RGB」を使用するより「HSB(色相、彩度、明度)」の方が良いという意見もありました。

発表のソースはこちらに上げてくださっています。

github.com

REPLACE WITH DAGGER.ANDROID

@kettsun0123さん

speakerdeck.com

Android開発はしたことがないので、発表中のコードの内容はよく理解できませんでした。
DaggerはDIライブラリで、iOSでいうSwinjectなどみたいなものでしょうか。

Dagger2からdagger.androidにすることでシンプルに書けるようようです。

終わりに

ブログにまとめるにあたって、もっとAndroidの知識が分かっていればと思いました。
iOS開発も楽しいですが、Androidでは自由がきいていろいろな実装が出来るんだな感じたので触れてみたいなと思いました。

次のpotetotipsではiOSで発表させていただくので、とても楽しみです!
今後ともよろしくお願いします。