Flutter製AndroidアプリのテストをTest Labで動かすメモ

書いてみる

TODOアプリを想定してテストコードを書きます。

void main() {
   IntegrationTestWidgetsFlutterBinding.ensureInitialized();
 
   group('group', () {
     testWidgets('start', (WidgetTester tester) async {
       app.main();
       await tester.pumpAndSettle();
 
       // 要素を確認する
       expect(find.byType(FloatingActionButton), findsWidgets);
       expect(find.text("メモ一覧"), findsOneWidget);
 
       // 新規作成ボタンをタップする
       await tester.tap(find.byType(FloatingActionButton));
       await tester.pumpAndSettle();
 
       // 内容を入力し、戻る
       await tester.enterText(find.byType(TextField), "new text");
       await tester.pumpAndSettle();
       await tester.tap(find.byType(IconButton));
       await tester.pumpAndSettle();
 
       // メモが反映されていることを確認する
       expect(find.text('new text'), findsOneWidget);
     });
   });
 }

ローカルで試す

バイスを立ち上げた上で、下記を実行します。

flutter test integration_test

※容量が足りないと言われることが多いので、その際はエミュレータの wipe dataを実行します。

Test Labで実行する

docs.flutter.dev

基本的には上記のドキュメント通りに進められたものの、以下の二つで若干つまづきました。

  • 依存関係を追加した後に、Gradle Syncはしなくて問題ない(癖でGradle Syncを押して、wrapper taskがないよと怒られました)
  • ビルド時のコマンドの-Ptarget=の後は"file path"で囲む必要がある
./gradlew app:assembleDebug -Ptarget="integration_test/<name>_test.dart"

PagedIterableのメモ

概要

PagedIterableのコードを確認したため、備忘録として残しておきます。

PagedIterableは、 マイクロソフトのライブラリにおいて定義されているページング処理を行うためのクラスです。

使い方

iteratorを使用することで、ページ毎にアクセスできるようになっています。

 val iterator = hogeClient.listHoges.byPage().iterator()
 while (true) {
        if (!iterator.hasNext()) break
                val page = iterator.next()
 }

また、foreachやmap, flatMap等を使用することで全ての結果を一度に取得することもできます。

 val result = hogeClient.listHoges.byPage().flatMap { // do something }

byPage()を呼ぶ時呼ばない時の挙動の違い

byPage()による違いが気になったので、少しコードを追ってみました。

結論

結論としては、

  • byPageを呼ぶ...pageIdを用いて取得したページの値(リスト)をPagedResponseとして返却します。次のnext()でも、次ページを取得&返却します。
  • byPageを呼ばない...ページ毎に取得すること自体は変わりません。しかし、1ページ分の値を取得し、そのレスポンスの中のアイテムを一つずつ返却します。1ページの中のアイテムを返し終わった後、次のページを取得する流れで処理が進みます。

コードの内容

コード内の主な登場人物は

  • PagedIterable
  • PagedResponseIterable
  • PageItemIterator
  • PagedResponseIterator

の4つです。

byPageを使用する場合の動作

  1. ClientクラスのlistHoge()がpagedIterableを返却する。
  2. byPage()がPagedResponseIterableを返却する
  3. PagedResponseIterableのiterator()がPageResponseIteratorを返却する
  4. PageResponseIterator#next()を呼び、実際のページ取得処理が始まる。
  5. 取得結果からpageIdを取得し、保存する。
  6. 次のnext()で、同pageIdを使用して後続のページを取得する。 という流れになっています。

byPage()を使用せず、pagedIterable#iteratorを呼ぶ場合の動作

  1. PagedIterable#iterator()を呼び、PageItemIteratorを生成する。
  2. PageItemIteratorの引数として、PagedIterable#byPage()#iteratorを呼び、渡す。 (結局、byPageは呼ばれている)
  3. PageResponseIterator#next()を呼び、実際のページ取得処理が始まる。
  4. 取得結果をDequeに入れる
  5. 取得結果からpageIdを取得し、保存する。
  6. Dequeに値があればpopする
  7. 次のnext()で、Dequeに値があればpopする。Dequeが空であればpageIdを使用して後続のページを取得する。

という流れになっています。

byPageという名前の通り、 page毎の値を返すか、アイテム一つずつを返すかの違いになっているようです。

Flutterでpath_providerを使用する際のメモ

概要

Flutterでsqliteを入れる流れでpath_providerを使用した際に少し詰まったため、メモを残しています。

path_providerを依存関係に追加した際、No implementation found for method getApplicationDocumentsDirectory when app is in the backgroundとエラーが出てしまう

バックグラウンドで実装が見つからないよ、と言われてもという感じですが、以下のissueがありました。 github.com

以下のコメントを確認すると、 github.com

Add dependencies on path_provider_android and path_provider_ios.
At the beginning of your background isolate entry point, add something like:
if (Platform.isAndroid) PathProviderAndroid.registerWith();
if (Platform.isIOS) PathProviderIOS.registerWith();

とあるため、依存関係の追加とコードの追加をします。

path_provider_iosがdiscontinuedになっている

新しく、path_provider_foundationが用意されているようなので、こちらを使用します。 pub.dev

RustのコードをAndroidで動かしてみる

はじめに

Rustに興味が湧いてきたため、普段触っているAndroidで動かしてみました。

Hello worldを返す

Android NDK で Rust は使えるのか?調査してみた|NAVITIME_Tech|note

こちらの記事を参考に、ほぼそのままRustのファイルを作成できました。 ありがとうございます!!

一点、rust-android-gradleが新しくなっていたようで、 0.9.3に上げる必要がありそうです。

他のコードを試す

TODO

NotificationHub#startを呼んだ際、何が起きているのか

概要

先日、Notification HubsのAndroid用の現行SDKを調べる機会があったのですが、 NotificationHub#startを呼んだだけで基本的な処理が完了するようになっており、とても驚きました。

この記事では、そのstartを呼んだ際に何が起きているのか*1を備忘録としてまとめておきます。 ※Firebase関連の説明は省きます

対象

Androidアプリ開発者でNotification Hubsの現行SDKを使う方

Notification Hubsとは

Notification Hubsとは、AndroidiOS等のプラットフォームへの通知を管理するAzureのサービスです。
詳細はこちら

登場する単語

  • NotificationHub ...Notification Hubs関連の処理をまとめるクラス
  • Installation ...デバイスへの通知処理を管理するためのデータのまとまり
  • InstallationAdapter ...Installationを送信することで、デバイスの登録処理を行うクラス
  • 〇〇Visitor ...Installationで使用するデータを保存するクラス
  • バイストーク ...FCM等から払い出される各デバイスに一意な文字列
    NotificationHub内では、PushChannelVisitorが管理する

Installationに関してはAzure docsの説明が参考になります。

従来のRegistrationと比較すると、プッシュ通知に必要なプロパティをひとまとめにした強化版とのことです。

大まかな処理の流れ

startを呼んだ際の流れは、

  1. NotificationHub#startを呼ぶことで、Applicationの登録やInstallationAdapterクラスの生成、NotificationHubクラスの生成等が行われる
  2. NotificationHubExtension#fetchPushChannelが呼ばれる
  3. (fetchPushChannel内で)FirebaseMessagingService#getTokenが呼ばれ、受け取ったデバイストークンをPushChannelVisitorに保存する
  4. (fetchPushChannel内で)NotificationHub#beginInstanceInstallationUpdateが呼ばれ、Installationの登録処理が始まる
  5. (beginInstanceInstallationUpdate内で)〇〇Visitorから各パラメータ(デバイストークンやタグ等)を取得し、新しいInstallationを作成する
  6. (beginInstanceInstallationUpdate内で)InstallationAdapter#saveInstallationが呼ばれる
  7. (saveInstallation内で)Notification Hubリソースに対して同Installationを登録するリクエストを送信する

となっています。

また、二回目以降(デバイストークンが更新された場合)の登録処理だと、 FirebaseMessagingServiceを継承したFirebaseReceiver#onNewTokenが呼ばれたタイミングで、受け取ったデバイストークンをPushChannelVisitorに保存します。
その後はstartが呼ばれた際の登録処理と同じ流れのようです。

シーケンス

NotificationHub#startのシーケンス図
NotificationHub#startのシーケンス図
※省略した部分

  • InstallationAdapterの区別(DebounceInstallationAdapter, NotificationHubInstallationAdapter, PushChannelValidationAdapter)
  • NotificationHubとNotificationHubExtensionの区別
  • 登録リクエストを送信する部分の詳細
  • 保持しているデバイストークンと同じデバイストークンを受け取ったケース

その他のメモ

  • アプリ側にconnectionStringを保持する必要があるので、要注意
  • DebounceInstallationAdapterは(同じ内容の)登録処理が何度も走るのを防ぐもの
  • Installation登録処理のインターバルは2000ミリ秒
  • 同じ内容のInstallation処理を送るための待ち時間のデフォルトは一日
  • この現行SDKを使用してInstallationを登録した場合、Installationの有効期限は90日になっている
  • Notification Hubsのテスト送信において、特定のinstallationIdを持つデバイスに通知を送信したい場合は "$installationId:{<対象のinstallationId>}" をタグに設定する
  • Notification Hubsのテスト送信において、特定のuserIdを持つデバイスに通知を送信したい場合は "$userId:{<対象のinstallationId>}" をタグに設定する
  • ドキュメントの設定手順を見る限り、Firebase MessagingのレガシーAPIを使用しているようなので、notification messageのkeyにバッジカウントが存在しない
  • 初期化を遅延させたい場合は、firebase_messaging_auto_init_enabledをfalseにしても意味はなく、NotificationHub#setIsEnabledを呼ぶ必要がある(そうしないと、NotificationHub初期化時にgetTokenが呼ばれてしまう)

参考

*1:decompileしたコードを読んだだけのため、間違い等あると思います

2022年の振り返り

2022年にやったことを書いていこうと思います。

技術関連

Androidアプリ開発

直近はJetpack ComposeでのUI作成やプッシュ通知機能の追加等をやっていました。

Droidkaigi

日英翻訳の難しさとオフラインイベントの良さを痛感した日々でした。

放送大学

〇〇入門という言葉に毎回騙されています。
今期受けた中だと、記号論理学が楽しいです。タブローの簡潔さに驚かされました。

ネットワークスペシャリスト

ギリギリで合格していました。(良かった、、、、)


読んだ本

  • ピープルウェア
  • Real World HTTP(WIP)
  • 法の概念(学生時代に教授から薦められたにも関わらず、ずっと積読してました)
  • 思考と行動における言語
  • プログラマの本懐
  • アジャイルサムライ
  • 戦闘妖精・雪風(友人に薦められて)

Real World HTTP

Go言語なんて自分にとって遠い存在だろうと思っていましたが、比較的馴染みやすい言語でした。 Goのことを少しだけ好きになりました。


上記以外

LiSAのライブ

Eve&Birthに参加してきました。めっちゃ良かったです。

SAOイベント

SAO FULL DIVE, THE ART OF SWORD ART ONLINE, Dive to Stage, スケルツォ, SAO アニメ10周年記念BOX(イベント?)と盛りだくさんで充実した土日だった気がします。ユナのARシーンを観た時は泣きました。

自然と人のダイアローグ

THE ART OF SWORD ART ONLINEの帰りにふらっと立ち寄りました。 セーヌ河の朝という作品がとても綺麗だったため、ポストカードを買いました。

ぼっち・ざ・ろっく

とんでもなく良かったです。漫画とアルバムも買いました。

TOEIC

久しぶりに受験したところ、900を超えました🎉


来年

ネットワークとRust、Go言語を頑張ります。