Flutterのuni_linksを使ってみた
久しぶりにブログ書きます!
Flutterでディープリンクを実装しようと思っており、実装方法を調べているとuni_linksというライブラリを見つけました。 今回はこのライブラリを使って、DeepLinksの実装をしていこうと思います。
uni_linksとは
ディープリンク(AndroidのAppLinks, DeepLinks、iOSのUniversalLinks CustomUrlScheme)を支援するプラグイン。
中ではディープリンクが各プラットフォームに届いた際に、 MethodChannel
や EventChannel
を用いて、Flutter側に通知しています。(違っていたらすみません。)
実装例
今回の実装例では、AndroidのDeepLinks、iOSのCustomUrlSchemeで実装していきます。
今回実装したコードはこちらです。 github.com
前提
遷移は下記の図の通りです。
MainPageからExample1Page、Example2Page、Example3Pageに遷移ができ、各Exampleのページは自身のページと各Exampleページに遷移ができます。
またExample1Page、Example2Pageではリンクのクエリを表示するようにしています。
インストール
pubspec.yml
に下記を記載する。
# ... dependencies: # ... uni_links: ^0.2.0 # ...
iOS, Android側の設定
AndroidとiOSにディープリンクの情報を記載する必要があります。
各プラットフォームでディープリンクを実装する時と同じ方法です。
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.dart
と example2_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の部分の実装は下記の図です。
下記のように実装していきます。
- MainViewModelでuni_linksのメソッドを使いディープリンクを受け取る
- 受け取ったディープリンクリンクをRoutePatternに変換
- RoutePatternをStreamで流す
- 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のコードを確認してみてください。
受け取ったUriを _streamRoutePattern
で RoutePattern
に変換し、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型に変換し遷移処理をしています。
動作確認
ちゃんと遷移することを確認できました。
最後に
自分で実装するとなると結構大変なディープリンクですが、uni_linksを使うと簡単に実装することができました。
間違えている部分があった場合は、教えていただけると幸いです。
最後まで読んでいただき、ありがとうございました。
Swiftで書いて覚えるTDDを読んだ
TDDを社内で少しずつ学んでいく中で、もっとTDDについて学びたいと思い、「Swiftで書いて覚えるTDD」を読みました。
この本では実際にツーカードポーカーの実装をしていきます。
下記が本を読みながら書いた自分のコードです。
そもそもTDDって?
TDD(テスト駆動開発: test-driven development)は開発手法です。
機能が期待する結果を、プロダクトコードよりも先に失敗するコードを書き、実装のゴールを設定します。
(プロダクトコードよりも先にテストを書くことを テストファースト
を呼ぶ)
テストしやすい = 良い設計 であることが多く、テストを先に書くことで良い設計を作ることができます。
詳しくは「Swiftで書いて覚えるTDD」にも書かれているので、気になる方は読んでみてください。
TDDのサイクル
TDDのサイクルは下記のよう行います。
- 設計
- テストコードを書く(レッド: テスト失敗)
- プロダクションコードを書く(グリーン: 成功)
- リファクタリング(グリーン: 成功)
このサイクルを レッド・グリーンリファクタリング
と呼ぶみたいです。
本の構成
- TDDとは
- 書いて覚えるTDD
- 2018年現在のSwiftでのTDD開発
- 参考文献
実際に書くのが2です。
本のまとめ
ToDoリスト
実際に作っていく中で、設計時にToDoリストを作るのがいいなと思いました。 例えば、下記の要件があった場合、
・任意のカード 1 枚の文字列表記を取得してください。 ・スート (suit) と ランク (rank) を与えて カード (card) を生成してください。 ・生成したカードから文字列表記 (notation) を取得してください。
下記のようにToDoに落とし込んでいました。
- [ ] Card を定義して、インスタンスを作成する - [ ] Card のインスタンスから文字列表記 (notation) を取得する
こうすることで、やるべきことを忘れずに目の前のことに集中できることや、その仕事が終わったかどうかを確認することができます。
アジャイル開発におけるストーリを、もっと詳しくかつ技術よりにした感じかなという印象を受けました。
Mock
3章からTDDを実践していく中での課題点が書かれていました。
ここで紹介していたのがCuckooというモックライブラリです。(クークーと呼ぶみたいです)
CuckooはJavaのMockitoにインスパイヤされたライブラリで、メリットとして
- モックをしなくてもよい
- 使い方がなじみやすい
があるのですが、デメリットとして
- モック化するファイルの指定が少しめんどくさい
- ジェネリクス非対応
らしいです。
使い所としてRxSwiftを使っていないプロジェクトと書いてあり、理由としてRxSwiftでジェネリクスを使った開発には適していないからだそうです。
※ 現在もまだ対応していないようです。
Androidも開発している自分にとって、Mockitoとほぼ同じ形で実装できる嬉しい反面、RxSwiftでジェネリクスを時々使用しているので辛いという印象。
現在まだCuckoo使ってのRxSwiftのジェネリクス部分のテストは書けていない状況です・・・。
まとめ
Swiftで書いて覚えるTDDを読んでみて、実際に書けるのは身につきやすいのでいいなと思いました。
メソッドなどの小さい単位でレッド・グリーンリファクタリングをすることで、すごく心持ちがよく実装ができると思います。
TDDは知っているけど、実際にやったことがないという初心者の方におすすめの本だなと感じました。
potatotips#44に参加しました!(Android編)
遅くなりましたが先月株式会社エウレカさんで開催されたpotatotips#44にAndroidブログ枠として参加させていただきました。
自分はiOSのアプリしか開発したことがなく知識不足なので、間違っている部分がありましたらご指摘をお願いいたします。
Danger for Android
Danger自体は聞いたことがあったのですが、実際どのようなものなのかは知りませんでした。
今回の発表ではCIを使用してGithub上でコードレビューを自動化させるというお話でした。
Gemfileで必要なGemをインストール
↓
Dangerfileにレビューの項目とAndroid lintの設定
↓
CIに組み込む
という流れでした。
警告が出たコードに関して直接PRに提示してくれるのは便利だと感じました。
また空白は行数チェックなどは人の目でチェックするのではなく自動化して任せる方が良いですね。
AndroidのKiosk端末化 ~ダイジェスト版~
Kioskと聞いて駅のあれだと思ってしまいました(笑)
スライドにも書いてあるのですが、wikiで調べたところKiosk端末下記のようなもののことらしいです。
インタラクティブなキオスクは、通信、商取引、エンターテイメント、または教育のための情報およびアプリケーションへのアクセスを提供する特殊なハードウェアおよびソフトウェアを特徴とするコンピュータ端末である。
ref.) https://en.wikipedia.org/wiki/Interactive_kiosk
今回の発表の内容は下記の内容を実現するため実装方法のお話でした。
最近見かけるようになった飲食店の注文するためのタブレットようなものでしょうか。
実装はAndroid 5.0で追加されたDevice Owner、Screen Pinningを使用するという方法でした。
またDevice Ownerを使用する場合は端末の初期化が必要のようです。
今回発表されたソースについてはこちらに上げてくださっています。
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
2つの色を混ぜたい時にどうやるのか、というお話でした。
方法としてAndroidのsupport.v4.graphicsで提供している ColorUtils
の blendARGB
を使用して変更していくというものでした。
ColorUtils | Android Developers
ColorUtils.blendARGB(color1, color2, ratio)
だけで色同士を混ぜられるのは便利ですね。
また ViewPager#onPageScrolled
との相性が良いかもとのことでした。
スクロールなどで徐々に色を変更したい時に ratio
を動的に変更すれば良いのでiOSに比べて楽に実装ができるのではないのかなと思いました。
Twitterでは「RGB」を使用するより「HSB(色相、彩度、明度)」の方が良いという意見もありました。
Color BlendってRGBよりHSBの方がふさわしいような気もするけど、どう違うんだろう……。 #potatotips
— Kaoru (@TachibanaKaoru) 2017年10月25日
発表のソースはこちらに上げてくださっています。
REPLACE WITH DAGGER.ANDROID
Android開発はしたことがないので、発表中のコードの内容はよく理解できませんでした。
DaggerはDIライブラリで、iOSでいうSwinjectなどみたいなものでしょうか。
Dagger2からdagger.androidにすることでシンプルに書けるようようです。
終わりに
ブログにまとめるにあたって、もっとAndroidの知識が分かっていればと思いました。
iOS開発も楽しいですが、Androidでは自由がきいていろいろな実装が出来るんだな感じたので触れてみたいなと思いました。
次のpotetotipsではiOSで発表させていただくので、とても楽しみです!
今後ともよろしくお願いします。