Upgrade to Pro — share decks privately, control downloads, hide ads and more …

複数ドメインを一つのアプリに統合するFlutterの設計と運用_FlutterKaigi202...

 複数ドメインを一つのアプリに統合するFlutterの設計と運用_FlutterKaigi2026登壇資料(須永高弘)

FlutterKaigi 2026(2026年10月29日)スポンサーセッションの登壇資料です。

■ このトークで話すこと
昨年のFlutterKaigiで、私は「顧客価値をSLO/KPIで定義し、Flutterで設計を逆算する」話をし、「長期的なキャリア支援プラットフォームへ」という挑戦を掲げました。
今年はその挑戦を実際に進めた話です。
新卒と中途を一つのアプリに統合する開発で直面した課題を、どんな技術でどう解き、どうなったかまで、課題→技術→結果でお話しします。

■ 前提
新卒と中途はもともと別アプリでしたが、一貫してキャリアを支えるために2026年に一つのアプリへ統合しました。
いまは1アプリ・1リポジトリで、登録動線までは共通、その先は基本的に事業ごとに分かれます。
共通部分の変更は両方に影響します。
これを、各自が既存プロダクトの責任者を兼ねる少人数のチームで開発しています。
このあとの手法は、すべてこの「1リポジトリ・少人数・共通部分あり」という前提の上に成り立っています。

■ 課題1:2つの事業を、一つのアプリで開発する
【課題】根幹の体験は新卒でも中途でも共通で作りたい一方、事業フェーズの違う中途と新卒では必要な機能も計画も違う。共通と個別が入り混じった開発を一つのプロジェクトで進めるのが難しい。
【解決】アプリの中を層で分け、共通の体験と事業ごとの機能を切り分けて扱えるようにした。機能が増えても各層の責務が崩れないことを狙ってこの形にした。
【結果】共通体験と事業別機能を、それぞれの計画で並行して足せるようになった。

■ 課題2:開発とリリースが、互いを止めないようにする
【課題】1アプリ・1リポジトリにしたことで、片方の開発が他方のリリースを止め、コンフリクトが増え、片方の不具合が全体に響く。
【解決】developを常にリリース可能に保つブランチ運用と機能フラグでブロックとコンフリクトを抑え、何かあればRemote Configのフラグで公開を止め、検証してから広げられるようにした。
【結果】チーム間でリリースがブロックし合う状況を減らせた。

■ 課題3:リリースした後に、次の改善点を決められるようにする
【課題】リリースした後のアプリの動作、操作の速さ、使われ方が見えないと、不具合の原因も次に改善する点も決められない。
【解決】Flutter側でブレッドクラムとAPIエラーの分類、操作ごとの待ち時間とjank、画面遷移のイベントを送り、計測できるようにした。
【結果】不具合の原因を切り分け、次の改善点と次に改善する画面を決められるようになった。

■ まとめ
個々の技術の使い方ではなく、顧客に届けたい体験(事業の課題)を起点に技術を選ぶ考え方。
昨年は単体アプリの体験品質で示したこの考え方を、今年は複数の事業を一つに統合する構造レベルに広げました。

Avatar for 須永高弘

須永高弘

October 29, 2026

More Decks by 須永高弘

Other Decks in Programming

Transcript

  1. 自己紹介 須永 高弘 Takahiro Sunaga 執行役員 CTO キャリアプラットフォーム事業部 ・ 総合コンサルファームで、製造やメディアの顧客の戦略、業務、


    IT のプロジェクトを経験 ・ 若手転職支援アプリ ASSIGN の開発と運用、LLM の事業活用、
 エンジニア組織の開発に従事 ・ FlutterKaigi 2025 に登壇(顧客価値を実現する Flutter) ©ASSIGN Inc. All Rights Reserved 02
  2. 目指す姿 新卒の就活から中途の転職まで、同じアプリでキャリアを支える キャリア アサイン ©ASSIGN Inc. All Rights Reserved 学生

    新卒 中途 その先 就職活動 入社してからの数年 転職を考えるとき 次のキャリアを描く キャリアプラットフォームで、すべてのフェーズを一貫して支える 05
  3. いまの姿 アプリの統合 2026年4月に、新卒向けと中途向けの2つのアプリを一つに統合した 統合前 統合後 新卒向けのアプリ キャリアプラットフォーム 2026年4月リリース 新卒向け 自己分析ワーク、就活ノウハウ


    企業・採用イベント検索、業界研究 中途向けのアプリ 共通 キャリアタイプ診断、スカウト 中途向け キャリアプラン作成、転職シナリオ、
 求人検索、キャリア学習動画 ©ASSIGN Inc. All Rights Reserved 06
  4. 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
  5. 打ち手 責務によるレイヤーの分割 lib直下をpresentation、application、domain、data、coreの5つの責務で分ける ©ASSIGN Inc. All Rights Reserved presentation 利用者の操作を受け取り、状態を持ち、画面に反映する

    application ユースケースの手順を定め、複数のRepositoryをまとめる domain ビジネスのルールと振る舞いを持つモデルを表す data API、DB、キャッシュとのやり取りを担う core ロガー、エラーレポート、通信クライアントなど共通の基盤を提供する 12
  6. 打ち手 機能単位の分割 各レイヤーの中を機能の単位で分け、変更の影響を機能の列の中に閉じる エージェント詳細 採用イベント 新着求人 共通の画面と状態 新卒の画面と状態 中途の画面と状態 (エージェント詳細、表示の状態)

    (一覧と詳細、一覧の状態) (新着求人の一覧、一覧の状態) ユースケース ユースケース ユースケース (エージェントの取得) (おすすめイベントの取得) (新着求人の取得) モデル APIの応答の型を そのまま使う APIの応答の型を そのまま使う 新卒と中途の両方 presentation application domain data ©ASSIGN Inc. All Rights Reserved (エージェント) 新卒だけ 中途だけ 新卒 中途 新卒 中途 (新卒のAPI) (中途のAPI) (新卒のAPI) (中途のAPI) 13
  7. 打ち手 lintによるレイヤー境界の検査 レイヤー間の依存はlintで担保し、責務の判断だけをAIがレビューする PRを出す前 修正を繰り返す PRのあと 実装 lint AI レビュー

    AIが実装 lintループ lintで担保できない判断 同じlintでゲート 違反が無くなるまで 繰り返す 責務の分け方と 設計との整合 指摘があれば マージを止める CIで止める:application層のRiverpod依存など ©ASSIGN Inc. All Rights Reserved / CI lint develop マージ 警告に留める:色とフォントサイズ、画像の直書き 14
  8. 打ち手 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
  9. 結果 1か所の修正の影響を、その機能の範囲に限定できるようになった 新卒と中途が、もう一方を気にせず、それぞれの速さで改善を出せるようになった 打ち手 結果 責務でレイヤーを分ける 新卒と中途で共通の機能は、 1つの実装で両方の利用者に届けられる 機能で分け、新卒と中途の 違いは部品とデータ層へ

    新卒だけ、中途だけの変更が、 もう一方のドメインに波及しにくい レイヤーの責務をlintで守る 新卒と中途の開発が並行して進んでも、 どちらのコードも同じ作りのまま保たれる ©ASSIGN Inc. All Rights Reserved 16
  10. 打ち手 トランクベース開発 トランクベース開発で、developを常にリリースできる状態に保つ タグ タグ main develop 新卒 中途 フラグOFF

    新卒も中途も、小さな変更を 短いうちにdevelopへ戻す ©ASSIGN Inc. All Rights Reserved 未完成の機能は フラグをOFFにしたまま入れる developから いつでもリリースする 18
  11. 打ち手 トランクベース開発を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
  12. 打ち手 フィーチャーフラグによる出し分け 大きな変更はフラグで隠したままマージし、公開の時期と範囲をあとから決める Firebase Remote Config false true メリット 不具合のときに公開を止めて検証できる

    致命的な不具合が出ても、falseにすれば影響を小さくできる 表示 非表示 ©ASSIGN Inc. All Rights Reserved 一部の利用者で先に検証してから広げられる カナリアリリースとして公開し、問題が無ければ全員に広げる 20
  13. 打ち手 フィーチャーフラグによる出し分け 大きな変更はフラグで隠したままマージし、公開の時期と範囲をあとから決める 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
  14. 打ち手 ドメインごとの分岐を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<Failure, Agent> execute(String agentId) { if (_isNewGraduate) { return _ngRepository.fetchAgent(agentId); } return _ccRepository.fetchAgent(agentId); } } 23
  15. 結果 もう一方のドメインの変更を待たずにリリースしやすくなった コンフリクトが減り、新卒と中途が、もう一方を待たずにリリースしやすくなった 打ち手 ブランチを短く保ち、 こまめにdevelopへマージする ドメインごとの分岐を UseCaseの1か所に閉じ込める 大きな変更は フィーチャーフラグで出し分ける

    ©ASSIGN Inc. All Rights Reserved 結果 差分が小さいうちにdevelopへ入り、 レビューも取り込みも短く済む 同じファイルを同時に変えることが減る コンフリクトの起きた割合 12.1% → 9.4% 作りかけの機能はOFFのまま入れられ、 もう一方の完成を待たずにリリースしやすい 24
  16. 課題 共通の画面のエラーは、どこを修正すればよいかが見分けにくい 同じ画面でも、新卒と中途で呼ぶAPIと返る応答が違い、エラーの中身も違う ? ? ©ASSIGN Inc. All Rights Reserved

    共通の画面で落ちたとき、 直前にどちらのAPIが何を返していたか ? 片方のドメインでは正常な応答まで、 エラーとして溜まっていないか ? 同じ機能でも、新卒と中途の どちらで、どんなエラーが起きているか 原因を切り分けられず、 不具合が利用者に 残り続ける 28
  17. 打ち手 ブレッドクラムの活用 画面遷移と通信をブレッドクラムに残し、クラッシュ直前の操作を追えるようにする ブレッドクラム:利用者が通った道筋を、小さな記録として順にためていくもの 画面遷移 ライフサイクル 画面A → 画面B 画面遷移

    バックグラウンド 通信 応答 200 角丸の四角の1つが、ブレッドクラムの1件 画面B → 画面C ライフサイクル フォアグラウンド クラッシュ 通信 エラー 500 クラッシュの直前の記録として、一緒に届く(アプリ内で最大50件) ©ASSIGN Inc. All Rights Reserved 29
  18. 打ち手 ブレッドクラムの活用 画面遷移と通信をブレッドクラムに残し、クラッシュ直前の操作を追えるようにする 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
  19. 打ち手 APIエラーの3分類と送り先の使い分け APIのエラーを業務、システム、通信の3種類に分けて記録し、調べる順番を決める APIの応答 分類 400番台 業務エラー タイムアウト、接続エラー 通信エラー 500番台

    システムエラー エラーの内訳 まず調べる先 60% 29% 11% アプリや利用者の状態 利用者の電波 サーバーの障害 Firebase Analytics:全ての通信を、分類を付けてAPIごとに集計する Crashlytics:404を除くエラーを非致命のエラーとして残し、修正の対象にする ©ASSIGN Inc. All Rights Reserved 31
  20. 打ち手 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
  21. 結果 共通の画面のエラーも、修正する場所を見分けられるようになった 直前のAPIの応答と、新卒と中途の機能ごとのエラーの傾向から、原因を絞り込める 打ち手 結果 ブレッドクラムで 直前の操作を残す 共通の画面で落ちても、 直前にどちらのAPIが何を返したかが分かる 送る前にエラーを振り分ける

    エラーではない応答は送らず、 修正すべきエラーが埋もれにくい APIのエラーを 3種類に分けて送る 新卒と中途のAPIを機能ごとに見比べ、 片方だけで起きているエラーを見つけられる ©ASSIGN Inc. All Rights Reserved 33
  22. 課題 不具合が無くても、新卒と中途の利用者の快適さは測れない 状態が正常でも、共通の画面の体験の悪化は、状態の指標には表れない 共通の画面での利用者の体験 アプリの状態 クラッシュ 0件 エラー 0件 API

    正常に応答 快適に使えているか? ? ストレスなく使えているか? 新卒と中途で差はないか? 新卒と中途の両方が使う画面で、 快適かどうかが見えない 問題なく動いているように見える 問題なく動いていても、両方の利用者の体験の悪化に気づきにくい ©ASSIGN Inc. All Rights Reserved 34
  23. 打ち手 操作単位の待ち時間とjankの計測 新卒と中途の両方が使う操作と画面で、待ち時間とjankを測る 待ち時間 jank率 操作してから、結果が出るまでの時間 描画が遅れて、画面がかくついたフレームの割合 ここを測る タップ 16ms

    結果が出る 16msを超えたフレームを、かくついたフレームとして数える 測る操作 会員登録 測る画面 エントリー送信 メッセージ送信 TOP メッセージ一覧 メッセージ詳細 1つのコードで両OSに出すので、OSごとの描画の差を測る ©ASSIGN Inc. All Rights Reserved 35
  24. 打ち手 操作単位の待ち時間とjankの計測 3つの操作で待ち時間を、TOPとメッセージの画面でjankを測る 1 フレームごとの描画時間を受け取る Flutter の SchedulerBinding に addTimingsCallback

    で登録する。 build と raster の時間を 1 フレームごとに受け取る 2 16ms を超えたフレームを数える // 1. SchedulerBinding.instance.addTimingsCallback(_onTimings); // 2. を超えたフレームを数える _onTimings(List<FrameTiming> 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
  25. 結果 共通の画面の次の改善点が、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
  26. 課題 どの画面で離れたかは計測できても、なぜ離れたかは測りにくい 同じ画面でも新卒と中途で目的が違い、離れた理由もドメインごとに違う ✓ 画面遷移と離脱箇所 測れる 新卒の利用者の画面 就職活動で企業を探す 共通の画面 新卒と中途の

    利用者が通る 中途の利用者の画面 転職先の求人を比べる ? 理由 離脱 探している企業が 載っていない? 離脱 応募の意向が まだ高くない? 測りにくい 離れた理由が見えないと、どう修正するかの検討が手探りになる ©ASSIGN Inc. All Rights Reserved 38
  27. 打ち手 画面遷移とセクションの動きの計測 画面遷移とセクションごとの動きを新卒と中途で分けて見て、離れた理由を推測する セクション1 セクション2 セクション3 画面名を送信 画面名を送信 画面名を送信 画面名を送信

    同じセッションのID Firebaseが自動で付ける 縦に長いレポートや求人の画面は、セクション単位でも送る 送った画面名とセクションは、利用者の属性で新卒と中途に分けて集計する ©ASSIGN Inc. All Rights Reserved 39
  28. 打ち手 画面遷移イベントの送信 画面を移るたびに画面名を送り、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
  29. 打ち手 セクション単位の閲覧の計測 画面の真ん中の高さにあるセクションを、いま読まれているセクションとして送る 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
  30. 結果 離脱するまでの行動から、その理由の手がかりが得られた 1件だけ押した人の多くはダイアログで離れ、見比べた人ほど日程調整へ進んでいた 中途の新着求人でエントリーボタンを押した利用者の行き先 押す前に、求人のどこまで読んだか 仕事内容 応募条件 ボタンを押した利用者 1件だけ押した 2件押した

    勤務地 給料 会社情報 相談の日程調整 へ進んだ ダイアログを 閉じて離れた 3件押した 4件以上押した 進んだ人と離れた人で読んだセクションを比べ、日程調整の前のやり取りを設計し直す ©ASSIGN Inc. All Rights Reserved 42
  31. まとめ 5つの課題に対する打ち手と結果 5つの課題は、設計と運用で防ぎ、リリース後の計測で見つけることで小さくできた 取り組み 新卒と中途の 並行開発を止めない 設計と運用 健全さを保ち、 次の改善点を決める 計測

    課題 打ち手 結果 変更の影響の広がりやすさ レイヤーと機能の単位で分け、 境界をlintで守る 修正の影響を、 機能の範囲に限定できる 2つのドメインの開発の干渉 トランクベース開発、フラグ、 分岐をUseCaseに閉じ込める もう一方の変更を待たずに リリースしやすくなる 共通の画面のエラーの 見分けにくさ ブレッドクラムと APIエラーの3分類 共通の画面のエラーも、 修正する場所を見分けられる 新卒と中途の利用者の 快適さの測りにくさ 操作ごとの待ち時間と jankの計測 画面遷移と、 セクション単位の閲覧の送信 ぜ離れたかの見えにくさ な ©ASSIGN Inc. All Rights Reserved 次 改善点が、 dro d j kだ かる 離脱するまでの行動から、 理由の手がかりが得られる 共通の画面の の An i の an と分 44
  32. キャリアを歩むあいだ、ずっと支え続けるアプリへ 設計と運用、計測を土台に、どの節目からでもキャリアを考え直せるアプリにする 学生 新卒 中途 その先 方向性をもって、 最初のキャリアを選べる 自分の言葉で、 キャリアプランを持てる

    将来像に近づく 情報と機会が手に入る 専門性をもって、 次の一歩を開ける すべてのフェーズを、同じアプリで一貫して支える 新卒と中途の並行開発を止めない設計と運用 健全さを保ち、次の改善点を決める計測 変更しやすい設計にする アプリの状態を測る 新卒と中途の開発を止めない運用 操作の快適さを測る 使われ方を測る ©ASSIGN Inc. All Rights Reserved 45