本日はサイバーエージェントさんのFlutterエンジニアによる勉強会、 CA.flutter #1 に参加しました!会場はサイバーエージェントさんのアベマタワーズでした。
オンラインでの配信もされていて、YouTube Live で配信内容が見れるようになっているのもとても良かったです!(巻き戻しもできたのでブログまとめたい勢としては聞き逃した部分を確認できるのでめちゃくちゃありがたい……!)
質問は sli.do でリアルタイムに受け付けられていたのもとても親切な設計だと感じました!質問もすべて読み上げられないほどめちゃくちゃ盛り上がっていました…!
以下発表内容になります!
複雑な画面遷移を賢く管理する: AutoRouteGuardの活用法
発表者の ひろ(Hirokazu Tanaka) さんは、2023年新卒入社で、株式会社WinTicketでFlutterアプリ開発を行ってらっしゃるそうです。
auto_route
は Flutter のナビゲーションパッケージのひとつで、2023年現在、 go_router
に次いで人気のあるパッケージです。
Winticketでは、2021年3月にFlutter製アプリにリプレースした当初から auto_route
を使用されてらっしゃるそうです。
auto_route
の特徴的な機能として、 AutoRouteGuard
があります。 AutoRouteGuard
では、AutoRouteGuard
クラスを継承したクラスを定義して、ルーティングのよくある複雑さを解消しています。
たとえば、公式の以下の例では、画面遷移時に、未ログイン時はナビゲーションを中断してログインページに遷移、ログインが成功した場合のみだけ本来行おうとした遷移を実行、失敗した場合は中止しています。
class AuthGuard extends AutoRouteGuard { @override void onNavigation(NavigationResolver resolver, StackRouter router) { // ナビゲーションは、resolver.next() が true(ナビゲーションを続行)または false(ナビゲーションを中止)で呼び出されるまで一時停止される if (authenticated) { // 認証されている場合は、ナビゲーションを続行 resolver.next(true); } else { // 未認証の場合は、ログインページにリダイレクトする resolver.redirect(LoginRoute(onResult: (success) { // success == true の場合、ナビゲーションを続行、 // そうでない場合は中止する resolver.next(success); })); } } }
たとえばWinticketではVIPユーザーと呼ばれるユーザーには、 /campaign/vip
に遷移させるということをさせていることがあるようです。具体的には、以下のようにAutoRouteを定義しているそうです。
AutoRoute( path: '/campaign/vip', page: SecretVIPPage, guards: [ IsLoginGuard, IsVerificationGuard, IsVIPGuard, ] ),
これによって、ログイン済み、本人確認済み、VIPユーザーの条件を満たす、という3つのガードを画面遷移に適用して、さらにVIPユーザーでない場合はそれ用のページに飛ばす、といった動作が実現できているようです(こういう形で組み合わせできるのはいいですね)。
また、WinTicketではフィーチャーフラグを使ったtrunk-basedなブランチ運用をされているようなのですが、フィーチャーフラグの状態を判定するのにも AutoRouteGuard
を使われているそうです。
また、キーボードが閉じられるまで画面遷移を遅延させる AutoRouteGuard
も定義されているようです(これも面白い)(閉じる判定 WidgetsBinding.instance.window.viewInsets.bottom == 0
でいいんだ…)。
個人的には、ナビゲーションには go_router
か素のナビゲーションを使うことが多いのですが、go_router
にも未ログイン時にログインページにリダイレクトさせるような機能(参考)があるようで、使ったことがなかったので気づける良いきっかけになりました!
質問コーナー
Q. go_router
ではなく auto_router
を選定したのはなぜ?
当時の判断らしい
2021年3月のタイミングだと go_router は まだFlutter公式パッケージ傘下とかではなくて、まだメジャーな選択肢じゃなかった気がする(うろ覚え) #ca_flutter
— kumamo_tone (@kumamo_tone) 2023年11月14日
How To Improve UI Quality And Performance
発表者の なるお(naruogram)さんは、2024年入社予定。サイバーエージェント24卒内定者の方で、Amebaマンガに所属。Flutter だけでなく、 iOSアプリの開発も行ってらっしゃるそうです。
UIの品質とは、「そのサービスがユーザーにとってどれだけ使いやすく、理解しやすく、効率的であるか」と定義されているようで、具体的には以下のチェックリストが考えられるとのことです。
なるおさんの所属する Amebaマンガ では、 UIの品質を Visual Regression Test を使って、改善しているそうです。テストツールは playbook-ui/playbook-flutter
というサイバーエージェントの方が関わられてるツールと、 reg-suit を使っているそうです。
VRT では、以下のような点をチェックしているそうです。
- 異なるデバイスでもUIが崩れないか→VRTテストにおいてテストする端末を増やす
- 異なる状況でもUIが崩れないか→長い文字列や通信環境が悪くて画像が取得できなかったケースのStoryを用意する
- 端末の文字サイズによってUIが崩れないか→
TextScaleFactor
の値を設定し、複数パターンの文字サイズをテストする
また、 integration_test
を使ってパフォーマンス計測を行う取り組みもやってらっしゃるそうです。
await binding.traceAction( () async { await tester.scrollUntilVisible( itemFinder, 500.0, scrollable: listFinder, ); }, reportKey: 'scrolling_timeline', );
計測するためには、Widget Test のコード上で、 IntegrationTestWidgetsFlutterBinding.ensureinitialized()
で取得したインスタンスに対して traceAction()
を使用し、引数に reportKey
を文字列で指定します。
import 'package:flutter_driver/flutter_driver.dart' as driver; import 'package:integration_test/integration_test_driver.dart'; Future<void> main() { return integrationDriver( responseDataCallback: (data) async { if (data != null) { final timeline = driver.Timeline.fromJson( data['scrolling_timeline'] as Map<String, dynamic>, ); // タイムラインを、読みやすく理解しやすいタイムラインサマリーに変換 final summary = driver.TimelineSummary.summarize(timeline); // その後、タイムライン全体をjson形式でディスクに書き込む // このファイルは、Chromeブラウザのトレーシングツールで開くことができる // トレーシングツールは chrome://tracingに移動して開く // オプションで、includeSummaryをtrueに設定することでサマリーもディスクに保存する await summary.writeTimelineToFile( 'scrolling_timeline', pretty: true, includeSummary: true, ); } }, ); }
また、上記のテストドライバーを別ファイルに保存します。
もしテスト自体を integration_test/scrolling_test.dart
、テストドライバーをtest_driver/perf_driver.dart
というパスで保存していた場合、テストドライバーを指定して、以下のように実行するようです。
flutter drive \ --driver=test_driver/perf_driver.dart \ --target=integration_test/scrolling_test.dart \ --profile
さらに、おそらく上記例の summarize のかわりに summaryJson というプロパティを使用すると、平均/最長フレームビルド時間、平均/最長フレーム描画時間、平均CPU使用率、平均メモリ使用率などをJSON形式で出力することができるようです。(これはやってみたい、、!)
懇親会で登壇者のなるおさんに質問させていただいたのですが、公式の下記の Performance profiling
という記事が参考になりそうです。
個人的には、Flutter で VRT や PlayBook をガッツリ使った事例は貴重だと思うのでとても聞けてよかったです!また、 integration_test
でプロファイリングするのはぜひやってみたい!
integration_test で平均フレームビルド時間とか出せるんだ 適当にやってるとなんとなくスクロール速くなった気がする〜みたいな評価になりかねないからめっちゃ良い #ca_flutter
— kumamo_tone (@kumamo_tone) 2023年11月14日
小売アプリのためのアクセシビリティ
発表者のKoki Masuyama(masssun)さんは、今年1月に中途入社されて、AI 事業本部アプリ運用カンパニーというところでモバイルアプリチームのリードエンジニアをされているそうです。
アプリ運用カンパニーは小売り企業の方々と協業して、アプリを中心としたデジタルでの購買体験の実現を行う事業部とのことでした。
アクセシビリティの重要性からはじまり、まずエンジニア主導で行える Flutter におけるアクセシビリティ対応としては、
- スクリーンリーダー
- 文字サイズ
の対応がありそうとのことでした。
スクリーンリーダー
Widget ツリーに、「このテキストは何であるか」「ボタンをタップすると何が起こるのか」などの情報を付与します。
たとえば具体的には、Semantics
ウィジェットで囲んで label を設定するということができるようです。
ただ、AppBar
, Text
など標準Widgetの多くはそもそも Semantics
でラップされているようなので、使っていれば対応が済んでいるとのことでした。
そのため、Flutterでは特別にアクセシビリティ対応を行わなくても、スクリーンリーダーが対応ができる範囲が広いようです。しかし、それを踏まえた上で、いくつかのTipsがあるようでした。
cached_network_image
を使用している画像について、Semantics
で囲む (Image の場合は semanticsLabel という引数がある)- 複数のスタイルが適用されている
Text
は、別のText
として認識されてしまうが、まとめたい場合はMergeSemantics
やRichText
などを使う - アイコン付きのボタンについて、アイコン部分については読み上げさせたくないので、
ExcludeSemantics
ウィジェットでアイコン部分を囲む
文字サイズ
Text ウィジェットを使っていれば、フォントサイズはOSの設定に基づいてFlutterが自動的に計算してくれるので、これも特別な設定は必要なさそうとのことでした。
しかし、Layout overflow を起こさないような設計にする必要はあるとのことでした。そのための対応として、
SizedBox
で width, height を固定にしないConstrainedBox
で minWidth, minHeight を適切に設定Column
の children に設定されているWidgetは Wrap/Extended でラップすることで、画面端で改行されるようにする
などの工夫を行っているそうです。
個人的には、アクセシビリティはどうしても自分が当事者でないときに見落としがちな部分なので、Flutterでの具体的な事例や、気をつける観点などを知れる良い機会になったなと思いました!
おわりに
今回は CA.flutter の1回目の開催で、発表者の方々は新卒1年目、中途1年目、内定者の方ということでフレッシュな感じかと思いきや、かなり腕利きの方々という感じでとても勉強になりました!また、今所属しているYOUTRUSTでやっている勉強会(12/9(金)に第4回目をやります 今週 11/17(金) 13:00〜募集開始です)や、FlutterKaigi でお話した人とまた懇親会でお話できたりする機会になったのも嬉しかったです。また機会があれば参加したいです!