Slide 1

Slide 1 text

複数ドメインを一つのアプリに統合する Flutterの設計と運用 FlutterKaigi 2026 株式会社アサイン 須永高弘

Slide 2

Slide 2 text

自己紹介 須永 高弘 Takahiro Sunaga 執行役員 CTO キャリアプラットフォーム事業部 ・ 総合コンサルファームで、製造やメディアの顧客の戦略、業務、
 IT のプロジェクトを経験 ・ 若手転職支援アプリ ASSIGN の開発と運用、LLM の事業活用、
 エンジニア組織の開発に従事 ・ FlutterKaigi 2025 に登壇(顧客価値を実現する Flutter) ©ASSIGN Inc. All Rights Reserved 02

Slide 3

Slide 3 text

事業紹介 ©ASSIGN Inc. All Rights Reserved 03

Slide 4

Slide 4 text

解決したい課題 誰一人同じ答えがないキャリアを描く機会と方法が、十分に行き渡っていない ミッション 人の個性や可能性を 発揮するサポートを担う ©ASSIGN Inc. All Rights Reserved 解決したい課題 誰一人同じ答えがない キャリアを描く機会と方法が、 十分に行き渡っていない 04

Slide 5

Slide 5 text

目指す姿 新卒の就活から中途の転職まで、同じアプリでキャリアを支える キャリア アサイン ©ASSIGN Inc. All Rights Reserved 学生 新卒 中途 その先 就職活動 入社してからの数年 転職を考えるとき 次のキャリアを描く キャリアプラットフォームで、すべてのフェーズを一貫して支える 05

Slide 6

Slide 6 text

いまの姿 アプリの統合 2026年4月に、新卒向けと中途向けの2つのアプリを一つに統合した 統合前 統合後 新卒向けのアプリ キャリアプラットフォーム 2026年4月リリース 新卒向け 自己分析ワーク、就活ノウハウ
 企業・採用イベント検索、業界研究 中途向けのアプリ 共通 キャリアタイプ診断、スカウト 中途向け キャリアプラン作成、転職シナリオ、
 求人検索、キャリア学習動画 ©ASSIGN Inc. All Rights Reserved 06

Slide 7

Slide 7 text

Flutterを選んだ理由 2021年からFlutterで作り、統合した後も使い続けている Flutterの本質的な価値 描画から開発、性能、運用まで、プロダクトと体験の質を上げる仕組みが揃っている 1. UI=データという発想とImpellerで、画面の再現性が高い 2. テーマやモーションまで一貫し、どの端末でも同じ体験 3. Hot Reloadからテスト、Lint、静的解析まで揃っている 4. AOT、SIMD、FFIの基盤性能で、パフォーマンスに妥協がない 5. Firebaseとプラットフォーム連携が成熟し、運用しやすい ©ASSIGN Inc. All Rights Reserved FlutterKaigi 2025 須永の登壇 「顧客価値を実現するFlutter」 07

Slide 8

Slide 8 text

今日話すこと 複数ドメインを統合したアプリでリリースし続けるための開発、運用における取り組み 機能を作ってリリースするまで 新卒と中途の並行開発を止めない設計と運用 リリースして、利用者の手元に届いてから 健全さを保ち、次の改善点を決める計測 ©ASSIGN Inc. All Rights Reserved 08

Slide 9

Slide 9 text

01 新卒と中途の並行開発を止めない設計と運用 機能を作ってリリースするまで ©ASSIGN Inc. All Rights Reserved 09

Slide 10

Slide 10 text

統合したアプリで機能をリリースし続けるための2つの取り組み 統合しても新卒と中途に機能を届け続けるには、2つの取り組みが要る 取り組み 解消する課題 1 変更しやすい設計にする 変更の影響の広がりやすさ 2 新卒と中途の開発を止めない運用 2つのドメインの開発の干渉 ©ASSIGN Inc. All Rights Reserved 10

Slide 11

Slide 11 text

課題 統合前のアプリでは責務が曖昧なViewModelに処理が集中していた 責務が曖昧なViewModelがあり、1か所の修正が別の画面に影響していた View 1つのViewModelが肥大化していた app_view_model.dartは1,261行で、16のファイルから直接参照されていた ViewModel 状態を更新する場所が散らばっていた StateProviderは68件あり、1つを最大11のViewModelが読み書きしていた 処理をどこに書くかの基準が曖昧になっていた Model ©ASSIGN Inc. All Rights Reserved ViewModel、AsyncNotifier、StateNotifierの3つの書き方が混在していた 11

Slide 12

Slide 12 text

打ち手 責務によるレイヤーの分割 lib直下をpresentation、application、domain、data、coreの5つの責務で分ける ©ASSIGN Inc. All Rights Reserved presentation 利用者の操作を受け取り、状態を持ち、画面に反映する application ユースケースの手順を定め、複数のRepositoryをまとめる domain ビジネスのルールと振る舞いを持つモデルを表す data API、DB、キャッシュとのやり取りを担う core ロガー、エラーレポート、通信クライアントなど共通の基盤を提供する 12

Slide 13

Slide 13 text

打ち手 機能単位の分割 各レイヤーの中を機能の単位で分け、変更の影響を機能の列の中に閉じる エージェント詳細 採用イベント 新着求人 共通の画面と状態 新卒の画面と状態 中途の画面と状態 (エージェント詳細、表示の状態) (一覧と詳細、一覧の状態) (新着求人の一覧、一覧の状態) ユースケース ユースケース ユースケース (エージェントの取得) (おすすめイベントの取得) (新着求人の取得) モデル APIの応答の型を そのまま使う APIの応答の型を そのまま使う 新卒と中途の両方 presentation application domain data ©ASSIGN Inc. All Rights Reserved (エージェント) 新卒だけ 中途だけ 新卒 中途 新卒 中途 (新卒のAPI) (中途のAPI) (新卒のAPI) (中途のAPI) 13

Slide 14

Slide 14 text

打ち手 lintによるレイヤー境界の検査 レイヤー間の依存はlintで担保し、責務の判断だけをAIがレビューする PRを出す前 修正を繰り返す PRのあと 実装 lint AI レビュー AIが実装 lintループ lintで担保できない判断 同じlintでゲート 違反が無くなるまで 繰り返す 責務の分け方と 設計との整合 指摘があれば マージを止める CIで止める:application層のRiverpod依存など ©ASSIGN Inc. All Rights Reserved / CI lint develop マージ 警告に留める:色とフォントサイズ、画像の直書き 14

Slide 15

Slide 15 text

打ち手 lintによるレイヤー境界の検査 レイヤー間の依存はlintで担保し、責務の判断だけをAIがレビューする 1 検査の対象を application 層に絞る ファイルのパスに /lib/application/ を含む ものだけを検査する。UseCase を Riverpod に 依存させない決まりを、この層にだけ適用する 2 @riverpod の注釈を違反として報告する 注釈の名前が riverpod か Riverpod なら、 その箇所を IDE と CI で警告として表示する 3 違反の修正方法を警告のメッセージで示す 素 クラ P 定義 禁止 理由 上 読め UseCase は の スにし、 rovider presentation 層で する、と示す の を IDE で る ©ASSIGN Inc. All Rights Reserved 。 は class AvoidRiverpodInApplication extends AnalysisRule { static const _code = LintCode( 'avoid_riverpod_in_application', 'application 層では @riverpod を使えません', // 3. 違反の修正方法を警告のメッセージで示す correctionMessage: 'UseCase は素のクラスにし、' 'Provider は presentation 層で定義してください', ); LintCode get diagnosticCode => _code; void registerNodeProcessors(registry, context) { // 1. 検査の対象を application 層に絞る final path = context.definingUnit.file.path; if (!path.contains('/lib/application/')) return; registry.addAnnotation(this, _Visitor(this)); } } // 2. @riverpod の注釈を違反として報告する(_Visitor) void visitAnnotation(Annotation node) { if (!['riverpod', 'Riverpod'].contains(node.name.name)) return; rule.reportAtNode(node); } 15

Slide 16

Slide 16 text

結果 1か所の修正の影響を、その機能の範囲に限定できるようになった 新卒と中途が、もう一方を気にせず、それぞれの速さで改善を出せるようになった 打ち手 結果 責務でレイヤーを分ける 新卒と中途で共通の機能は、 1つの実装で両方の利用者に届けられる 機能で分け、新卒と中途の 違いは部品とデータ層へ 新卒だけ、中途だけの変更が、 もう一方のドメインに波及しにくい レイヤーの責務をlintで守る 新卒と中途の開発が並行して進んでも、 どちらのコードも同じ作りのまま保たれる ©ASSIGN Inc. All Rights Reserved 16

Slide 17

Slide 17 text

課題 2つのドメインの同時開発で、リリース待ちとコンフリクトが起きる 同じリポジトリでは、一方の都合でもう一方の開発とリリースが止まることがある 新卒 開発 リリース作業 共通コードを変更 もう一方の作業が終わるまで止まる 中途 開発 中途の修正を すぐに出せない 同じファイルを変更 コンフリクト 共通コードを触る 開発を始められない 同じファイルを変更 時間 ©ASSIGN Inc. All Rights Reserved 17

Slide 18

Slide 18 text

打ち手 トランクベース開発 トランクベース開発で、developを常にリリースできる状態に保つ タグ タグ main develop 新卒 中途 フラグOFF 新卒も中途も、小さな変更を 短いうちにdevelopへ戻す ©ASSIGN Inc. All Rights Reserved 未完成の機能は フラグをOFFにしたまま入れる developから いつでもリリースする 18

Slide 19

Slide 19 text

打ち手 トランクベース開発をFlutterで滞りなく進める工夫 developを守る運用のルールを、Flutterの開発環境とCIで支える 運用のルール Flutterでの工夫 1 マージ前のCIを必須にする dart formatと、自作のlintを含むflutter analyzeをCIで必ず通す 2 手元とCIの環境を揃える fvmでFlutterの版を固定し、CIも同じ版で動かす 3 こまめなマージを妨げない 生成ファイルをコミットし、CIでbuild_runnerを動かさない ©ASSIGN Inc. All Rights Reserved 19

Slide 20

Slide 20 text

打ち手 フィーチャーフラグによる出し分け 大きな変更はフラグで隠したままマージし、公開の時期と範囲をあとから決める Firebase Remote Config false true メリット 不具合のときに公開を止めて検証できる 致命的な不具合が出ても、falseにすれば影響を小さくできる 表示 非表示 ©ASSIGN Inc. All Rights Reserved 一部の利用者で先に検証してから広げられる カナリアリリースとして公開し、問題が無ければ全員に広げる 20

Slide 21

Slide 21 text

打ち手 フィーチャーフラグによる出し分け 大きな変更はフラグで隠したままマージし、公開の時期と範囲をあとから決める 1 キーを1つのクラスで宣言する Remote Config のキーをこのクラスにまとめ、 どのキーにも消すか残すかの宣言を付ける 2 消すフラグに期限を持たせる 期限はリリース日の30日後にする。 期限を過ぎると、自作の lint が CI を止める 3 残す設定値は理由を書いて分ける ストアレビューの位置のような設定値は、 フラグと分けて理由を書いて残す ©ASSIGN Inc. All Rights Reserved // 1. キーは全てこのクラスで宣言し、消すか残すかを決める abstract final class _RemoteConfigKeys { // 2. 消すフラグには、削除の期限とチケットを付ける @ExpiringFlag( key: 'release_new_home', expiresOn: '2026-12-15', // リリース日 + 30日 ticket: 'PROJ-123', ) static const String releaseNewHome = 'release_new_home'; // 3. 残す設定値には、残す理由を付ける @PermanentFlag( key: 'store_review_position', reason: 'ストアレビュー導線の位置を外から変える設定値', ) static const String storeReviewPosition = 'store_review_position'; } 21

Slide 22

Slide 22 text

打ち手 ドメインごとの分岐をUseCaseの1か所に閉じ込める APIと応答の違いは独立したRepositoryで吸収し、画面とUseCaseは共通にする 新卒のエージェント取得APIや 関連処理が変わっても、触るのはこの中だけ 表示の状態 UseCase 共通 共通 新卒か中途かで 処理を切り替える application 新卒の Repository 新卒のエージェント 取得API 中途の Repository 中途のエージェント 取得API data API presentation ©ASSIGN Inc. All Rights Reserved 22

Slide 23

Slide 23 text

打ち手 ドメインごとの分岐をUseCaseの1か所に閉じ込める APIと応答の違いは独立したRepositoryで吸収し、画面とUseCaseは共通にする 1 新卒と中途のRepositoryを両方受け取る UseCaseは新卒と中途のRepositoryを 両方持ち、画面からは1つのUseCaseに見える 2 isNewGraduateで使う方を選ぶ 利用者が新卒かどうかの属性で、 呼ぶRepositoryを切り替える 3 返す型は共通のAgentにする 新卒も中途も共通のEntityを返すので、 画面と状態はドメインを意識しない ©ASSIGN Inc. All Rights Reserved class FetchAgentUseCase { // 1. 新卒と中途のRepositoryを両方受け取る FetchAgentUseCase({ required NgAgentRepository ngRepository, required CcAgentRepository ccRepository, required bool isNewGraduate, }) : _ngRepository = ngRepository, _ccRepository = ccRepository, _isNewGraduate = isNewGraduate; final NgAgentRepository _ngRepository; final CcAgentRepository _ccRepository; final bool _isNewGraduate; // 2. isNewGraduateで使う方を選ぶ // 3. 返す型は共通のAgentにする TaskEither execute(String agentId) { if (_isNewGraduate) { return _ngRepository.fetchAgent(agentId); } return _ccRepository.fetchAgent(agentId); } } 23

Slide 24

Slide 24 text

結果 もう一方のドメインの変更を待たずにリリースしやすくなった コンフリクトが減り、新卒と中途が、もう一方を待たずにリリースしやすくなった 打ち手 ブランチを短く保ち、 こまめにdevelopへマージする ドメインごとの分岐を UseCaseの1か所に閉じ込める 大きな変更は フィーチャーフラグで出し分ける ©ASSIGN Inc. All Rights Reserved 結果 差分が小さいうちにdevelopへ入り、 レビューも取り込みも短く済む 同じファイルを同時に変えることが減る コンフリクトの起きた割合 12.1% → 9.4% 作りかけの機能はOFFのまま入れられ、 もう一方の完成を待たずにリリースしやすい 24

Slide 25

Slide 25 text

章まとめ 統合したアプリでリリースし続けるための2つの取り組み 変更の影響を機能とドメインの中に限定し、ドメインごとにリリースを進めやすくする 変更しやすい設計にする 新卒と中途の開発を止めない運用 変更の影響を、機能と ドメインの中に限定できる ドメインごとに、 開発とリリースを進めやすい 統合したアプリでも、新卒と中途の機能をリリースし続けられる ©ASSIGN Inc. All Rights Reserved 25

Slide 26

Slide 26 text

02 健全さを保ち、次の改善点を決める計測 リリースして、利用者の手元に届いてから ©ASSIGN Inc. All Rights Reserved 26

Slide 27

Slide 27 text

リリース後に測る3つ アプリの状態、操作の快適さ、使われ方 リリース後は、アプリの状態、操作の快適さ、使われ方の3つを測る 計測 解消する課題 1 アプリの状態を測る 共通の画面のエラーの見分けにくさ 2 操作の快適さを測る 新卒と中途の利用者の快適さの測りにくさ 3 使われ方を測る なぜ離れたかの見えにくさ ©ASSIGN Inc. All Rights Reserved 27

Slide 28

Slide 28 text

課題 共通の画面のエラーは、どこを修正すればよいかが見分けにくい 同じ画面でも、新卒と中途で呼ぶAPIと返る応答が違い、エラーの中身も違う ? ? ©ASSIGN Inc. All Rights Reserved 共通の画面で落ちたとき、 直前にどちらのAPIが何を返していたか ? 片方のドメインでは正常な応答まで、 エラーとして溜まっていないか ? 同じ機能でも、新卒と中途の どちらで、どんなエラーが起きているか 原因を切り分けられず、 不具合が利用者に 残り続ける 28

Slide 29

Slide 29 text

打ち手 ブレッドクラムの活用 画面遷移と通信をブレッドクラムに残し、クラッシュ直前の操作を追えるようにする ブレッドクラム:利用者が通った道筋を、小さな記録として順にためていくもの 画面遷移 ライフサイクル 画面A → 画面B 画面遷移 バックグラウンド 通信 応答 200 角丸の四角の1つが、ブレッドクラムの1件 画面B → 画面C ライフサイクル フォアグラウンド クラッシュ 通信 エラー 500 クラッシュの直前の記録として、一緒に届く(アプリ内で最大50件) ©ASSIGN Inc. All Rights Reserved 29

Slide 30

Slide 30 text

打ち手 ブレッドクラムの活用 画面遷移と通信をブレッドクラムに残し、クラッシュ直前の操作を追えるようにする 1 画面遷移を記録する GoRouter の observer として登録し、遷移のたびに 元と先の画面名を記録する。API の通信と、アプリの フォアグラウンドとバックグラウンドの切り替えも記録する 2 記録を 50 件まで保持する 古い記録から消し、直近の操作だけを保持する。 通信の URL はクエリを除いて記録し、 個人情報を含めない 3 記録をクラッシュの報告に添える リリース版だけ Crashlytics のログに書く。 クラッシュが起きると、直前の記録が 報告と一緒に送られる ©ASSIGN Inc. All Rights Reserved // 1. 画面遷移を記録する class NavigationBreadcrumbObserver extends NavigatorObserver { @override void didPush(Route route, Route? previousRoute) { BreadcrumbTracker.instance.trackNavigation( from: _routeName(previousRoute), to: _routeName(route)); } } // 2. 記録を 50 件まで保持する 3. 記録をクラッシュの報告に添える void add({required BreadcrumbType type, required String message}) { _breadcrumbs.add(Breadcrumb(type: type, message: message)); if (_breadcrumbs.length > 50) _breadcrumbs.removeAt(0); if (kReleaseMode) { FirebaseCrashlytics.instance.log('[${type.name}] $message'); } } 30

Slide 31

Slide 31 text

打ち手 APIエラーの3分類と送り先の使い分け APIのエラーを業務、システム、通信の3種類に分けて記録し、調べる順番を決める APIの応答 分類 400番台 業務エラー タイムアウト、接続エラー 通信エラー 500番台 システムエラー エラーの内訳 まず調べる先 60% 29% 11% アプリや利用者の状態 利用者の電波 サーバーの障害 Firebase Analytics:全ての通信を、分類を付けてAPIごとに集計する Crashlytics:404を除くエラーを非致命のエラーとして残し、修正の対象にする ©ASSIGN Inc. All Rights Reserved 31

Slide 32

Slide 32 text

打ち手 APIエラーの3分類と送り先の使い分け APIのエラーを業務、システム、通信の3種類に分けて記録し、調べる順番を決める 1 API のエラーを 3 種類に分類する 500 番台はシステムエラー、400 番台は業務エラー、 タイムアウトと接続エラーは通信エラー。 それ以外はシステムエラーに含める 2 分類した結果を Firebase Analytics に送信する 1 回の通信ごとに api_request_finished を送る。 パスの ID を :id に置き換え、ID が違っても 同じ API として件数を集計する 3 Crashlytics に残すエラーを絞る 正常な応答として送らない。それ以外のエラーを 非致命のエラーとして記録し、修正の対象にする 404 は ©ASSIGN Inc. All Rights Reserved // 1. API のエラーを 3 種類に分類する String _classifyErrorResult(DioException error) { final statusCode = error.response?.statusCode; if (statusCode != null) { if (statusCode >= 500) return 'system_error'; if (statusCode >= 400) return 'business_error'; } return switch (error.type) { DioExceptionType.connectionTimeout || DioExceptionType.sendTimeout || DioExceptionType.receiveTimeout || DioExceptionType.connectionError => 'network_error', _ => 'system_error', }; } // 2. 分類した結果を Firebase Analytics に送信する sendEvent(eventName: MonitoringEvents.apiRequestFinished, ...); // 3. Crashlytics に残すエラーを絞る(404 は送らない) if (failure is NetworkFailure && !failure.isRetryable) return failure; CrashReporter.instance.reportNonFatal(failure, stackTrace); 32

Slide 33

Slide 33 text

結果 共通の画面のエラーも、修正する場所を見分けられるようになった 直前のAPIの応答と、新卒と中途の機能ごとのエラーの傾向から、原因を絞り込める 打ち手 結果 ブレッドクラムで 直前の操作を残す 共通の画面で落ちても、 直前にどちらのAPIが何を返したかが分かる 送る前にエラーを振り分ける エラーではない応答は送らず、 修正すべきエラーが埋もれにくい APIのエラーを 3種類に分けて送る 新卒と中途のAPIを機能ごとに見比べ、 片方だけで起きているエラーを見つけられる ©ASSIGN Inc. All Rights Reserved 33

Slide 34

Slide 34 text

課題 不具合が無くても、新卒と中途の利用者の快適さは測れない 状態が正常でも、共通の画面の体験の悪化は、状態の指標には表れない 共通の画面での利用者の体験 アプリの状態 クラッシュ 0件 エラー 0件 API 正常に応答 快適に使えているか? ? ストレスなく使えているか? 新卒と中途で差はないか? 新卒と中途の両方が使う画面で、 快適かどうかが見えない 問題なく動いているように見える 問題なく動いていても、両方の利用者の体験の悪化に気づきにくい ©ASSIGN Inc. All Rights Reserved 34

Slide 35

Slide 35 text

打ち手 操作単位の待ち時間とjankの計測 新卒と中途の両方が使う操作と画面で、待ち時間とjankを測る 待ち時間 jank率 操作してから、結果が出るまでの時間 描画が遅れて、画面がかくついたフレームの割合 ここを測る タップ 16ms 結果が出る 16msを超えたフレームを、かくついたフレームとして数える 測る操作 会員登録 測る画面 エントリー送信 メッセージ送信 TOP メッセージ一覧 メッセージ詳細 1つのコードで両OSに出すので、OSごとの描画の差を測る ©ASSIGN Inc. All Rights Reserved 35

Slide 36

Slide 36 text

打ち手 操作単位の待ち時間とjankの計測 3つの操作で待ち時間を、TOPとメッセージの画面でjankを測る 1 フレームごとの描画時間を受け取る Flutter の SchedulerBinding に addTimingsCallback で登録する。 build と raster の時間を 1 フレームごとに受け取る 2 16ms を超えたフレームを数える // 1. SchedulerBinding.instance.addTimingsCallback(_onTimings); // 2. を超えたフレームを数える _onTimings(List for (final final timing isJanky in timings) (final session in session.totalFrames if { { > timing.rasterDuration for timings) = timing.buildDuration 画面ごとの集計を Firebase Analytics に送信する 画面を離れるときに、フレームの総数とかくついた数を screen_jank_summary として送る 16ms void build か raster のどちらかが 16ms(60fps の 1 フレーム)を超えたら、かくついたフレームとして 画面ごとに数える 3 フレームごとの描画時間を受け取る (isJanky) > const const Duration(milliseconds: Duration(milliseconds: _sessions.values) += 16) || 16); { 1; session.jankyFrames += 1; } } } // 3. 画面ごとの集計を useEffect(() return () Firebase Analytics に送信する { => unawaited(JankTrackingService.instance.stopTracking(sessionId)); }, ©ASSIGN Inc. All Rights Reserved [sessionId]); 36

Slide 37

Slide 37 text

結果 共通の画面の次の改善点が、Androidのjankだと分かった Androidのjankを改善すれば、新卒と中途の両方の利用者の体験を一度に良くできる 操作が完了するまでの時間(p90) 会員登録 2026年3月(リリース直後) 2026年9月 1409ms −29% 1005ms 2094ms エントリー送信 メッセージ送信 1831ms 1606ms 1295ms −13% −19% 画面のかくつき(jank率) iOSは 0.24% → 0.17% に下がった。 Androidは2%台で横ばいで、次の改善点になっている 成功した操作の値。月ごとの集計(Firebase Analytics、2026-10-20時点) ©ASSIGN Inc. All Rights Reserved 37

Slide 38

Slide 38 text

課題 どの画面で離れたかは計測できても、なぜ離れたかは測りにくい 同じ画面でも新卒と中途で目的が違い、離れた理由もドメインごとに違う ✓ 画面遷移と離脱箇所 測れる 新卒の利用者の画面 就職活動で企業を探す 共通の画面 新卒と中途の 利用者が通る 中途の利用者の画面 転職先の求人を比べる ? 理由 離脱 探している企業が 載っていない? 離脱 応募の意向が まだ高くない? 測りにくい 離れた理由が見えないと、どう修正するかの検討が手探りになる ©ASSIGN Inc. All Rights Reserved 38

Slide 39

Slide 39 text

打ち手 画面遷移とセクションの動きの計測 画面遷移とセクションごとの動きを新卒と中途で分けて見て、離れた理由を推測する セクション1 セクション2 セクション3 画面名を送信 画面名を送信 画面名を送信 画面名を送信 同じセッションのID Firebaseが自動で付ける 縦に長いレポートや求人の画面は、セクション単位でも送る 送った画面名とセクションは、利用者の属性で新卒と中途に分けて集計する ©ASSIGN Inc. All Rights Reserved 39

Slide 40

Slide 40 text

打ち手 画面遷移イベントの送信 画面を移るたびに画面名を送り、1回の利用の中で画面が移った順を見る 1 GoRouterにobserverを登録する 認証の前と後の2つのGoRouterの observers に FirebaseAnalyticsObserver を入れる。 画面ごとに送る処理を書かずに、遷移のたびに送る 2 クエリを外して、パスだけで照合する パラメータが違っても同じ画面として数える 3 パスを日本語の画面名に変える AppRouteCatalog に登録した日本語の画面名で送る。 登録の無いパスはそのまま送る ©ASSIGN Inc. All Rights Reserved // 1. GoRouter に observer を登録する return GoRouter( observers: [ routeObserver, AnalyticsEvent.observer, NavigationBreadcrumbObserver(), ScreenRenderingObserver(), ], static final observer = FirebaseAnalyticsObserver( analytics: analytics, nameExtractor: (settings) { final path = settings.name; if (path == null) return settings.name; // 2. クエリを外して、パスだけで照合する final purePath = Uri.parse(path).path; // 3. パスを日本語の画面名に変える for (final route in AppRouteCatalog.values) { if (route.path == purePath) { return route.screenNameJa; } } return settings.name; }, ); 40

Slide 41

Slide 41 text

打ち手 セクション単位の閲覧の計測 画面の真ん中の高さにあるセクションを、いま読まれているセクションとして送る 1 真ん中にあるセクションを調べる スクロールのたびに、画面の真ん中の高さに どのセクションがあるかを調べる 2 セクションが変わったときだけ送る 真ん中のセクションが変わったときだけ送り、 滞在時間は次に送った時刻との差で出す 3 最後まで届いたら1回送る いちばん下に届いたら1回だけ送る。 最後のセクションは真ん中まで上がらないので補う // 1. 画面の真ん中の高さに、このセクションがあるか bool centerLineOnSection(String name, double centerY) { final box = sectionKeys[name]?.currentContext ?.findRenderObject() as RenderBox?; if (box == null || !box.hasSize) return false; final top = box.localToGlobal(Offset.zero).dy; return centerY >= top && centerY < top + box.size.height; } // 2. 真ん中のセクションが変わったときだけ送る void emitSection(String name) { if (currentSection.value == name) return; currentSection.value = name; analytics.logSection(name); } // 3. 最下部に届いたら1回だけ送り、末尾のセクションを補う if (!bottomLogged.value && pos.pixels >= pos.maxScrollExtent - 4) { bottomLogged.value = true; analytics.logScrolledToBottom(); emitSection(CareerReportSectionNames.points); } ©ASSIGN Inc. All Rights Reserved 41

Slide 42

Slide 42 text

結果 離脱するまでの行動から、その理由の手がかりが得られた 1件だけ押した人の多くはダイアログで離れ、見比べた人ほど日程調整へ進んでいた 中途の新着求人でエントリーボタンを押した利用者の行き先 押す前に、求人のどこまで読んだか 仕事内容 応募条件 ボタンを押した利用者 1件だけ押した 2件押した 勤務地 給料 会社情報 相談の日程調整 へ進んだ ダイアログを 閉じて離れた 3件押した 4件以上押した 進んだ人と離れた人で読んだセクションを比べ、日程調整の前のやり取りを設計し直す ©ASSIGN Inc. All Rights Reserved 42

Slide 43

Slide 43 text

章まとめ アプリの状態、操作の快適さ、使われ方の計測 アプリの状態、操作の快適さ、使われ方を測ることで、次の改善点を決められる アプリの状態を測る 操作の快適さを測る 使われ方を測る 共通の画面のエラーも、 修正する場所を見分けられる 共通の画面の体験の悪化に 気づき、次の改善点が分かる 離脱するまでの行動から、 その理由の手がかりが得られる リリースしたあとのアプリを、健全な状態に保てる ©ASSIGN Inc. All Rights Reserved 43

Slide 44

Slide 44 text

まとめ 5つの課題に対する打ち手と結果 5つの課題は、設計と運用で防ぎ、リリース後の計測で見つけることで小さくできた 取り組み 新卒と中途の 並行開発を止めない 設計と運用 健全さを保ち、 次の改善点を決める 計測 課題 打ち手 結果 変更の影響の広がりやすさ レイヤーと機能の単位で分け、 境界をlintで守る 修正の影響を、 機能の範囲に限定できる 2つのドメインの開発の干渉 トランクベース開発、フラグ、 分岐をUseCaseに閉じ込める もう一方の変更を待たずに リリースしやすくなる 共通の画面のエラーの 見分けにくさ ブレッドクラムと APIエラーの3分類 共通の画面のエラーも、 修正する場所を見分けられる 新卒と中途の利用者の 快適さの測りにくさ 操作ごとの待ち時間と jankの計測 画面遷移と、 セクション単位の閲覧の送信 ぜ離れたかの見えにくさ な ©ASSIGN Inc. All Rights Reserved 次 改善点が、 dro d j kだ かる 離脱するまでの行動から、 理由の手がかりが得られる 共通の画面の の An i の an と分 44

Slide 45

Slide 45 text

キャリアを歩むあいだ、ずっと支え続けるアプリへ 設計と運用、計測を土台に、どの節目からでもキャリアを考え直せるアプリにする 学生 新卒 中途 その先 方向性をもって、 最初のキャリアを選べる 自分の言葉で、 キャリアプランを持てる 将来像に近づく 情報と機会が手に入る 専門性をもって、 次の一歩を開ける すべてのフェーズを、同じアプリで一貫して支える 新卒と中途の並行開発を止めない設計と運用 健全さを保ち、次の改善点を決める計測 変更しやすい設計にする アプリの状態を測る 新卒と中途の開発を止めない運用 操作の快適さを測る 使われ方を測る ©ASSIGN Inc. All Rights Reserved 45

Slide 46

Slide 46 text

ご案内 ブースはプラチナの7番、採用情報はQRから ブースのご案内 ホワイエ2、赤い枠がASSIGNのブース ©ASSIGN Inc. All Rights Reserved 採用情報 採用ページ job.persona-ats.com/ja/assign/jobs 46

Slide 47

Slide 47 text

ご清聴ありがとうございました ©ASSIGN Inc. All Rights Reserved 47