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

FlutterKaigi 2026 本人確認アプリをFlutterで再設計したら評価が1.2か...

TRUSTDOCK
October 29, 2026
42

FlutterKaigi 2026 本人確認アプリをFlutterで再設計したら評価が1.2から4.0に改善した話

TRUSTDOCK

October 29, 2026

Transcript

56 slides
  1. FlutterKaigi 2026 FlutterKaigi 2026 | DAY1 15:40-16:10 本⼈確認アプリを Flutterで再設計したら、 評価が1.2から4.0に改善した話

    株式会社TRUSTDOCK デジタルIDウォレット部 部⻑ ⿇⽣ 真吾 株式会社TRUSTDOCK デジタルIDウォレット部 Flutterエンジニア 陳 信瑜
  2. オープニング|⾃⼰紹介 ⾃⼰紹介とプロダクト紹介 ⿇⽣ 真吾 株式会社TRUSTDOCK:eKYC(オンライン本⼈確認)専⾨機関 350万DL突破「TRUSTDOCKアプリ」の開発裏話 株式会社TRUSTDOCK デジタルIDウォレット部 部⻑ 結果

    • 2025年7⽉よりEMとして参画 • 2026年1⽉より現職 呼出し 撮影‧ICチップ読み取り SDK 開始‧完了 TRUSTDOCKアプリ API • Web系フルスタックエンジニア • 基盤構築(認証認可、データ、検索) SDKの結果とAPIをつないで本⼈確認を進める 「本⽇は技術課題の裏にある、 事業と組織の意思決定を語ります」 3
  3. 序章|本⼈確認アプリという題材 本⼈確認という重い題材 Hardware Control High Reliability Friction Management ハードウェアコントロール ⾼信頼性

    摩擦管理 カメラ(⾝分証の厚み撮影)、 NFC (マイナンバーカードICチッ ブ読み 取り)の精密な制御 本⼈確認の⼿続きで求められる⼿順 を守り、データを確実に連携 ユーザーにとって「⾯倒な作業」 で あり、僅かなエラーで即離脱 端末の機能に深く触る → 途中で処理が⽌まりやすい 6
  4. 第1章|評価1.2が事業に与えていた影響 不満は提案の場まで届く END USER 利⽤者 確認の途中で落ちる OSで機能‧品質が違う ★1 レビュー CLIENT

    導⼊企業 ストア評価が 導⼊判断の材料に この 評価は…? US TRUSTDOCK 提案時の信頼度が下がる 結果:新規営業活動における極めて深刻な逆⾵ 12
  5. 第2章|なぜそこまで壊れたのか 当時の開発体制 社内(TRUSTDOCK) 1名 iOSチーム Androidチーム 1.5名 2.5名 PdM マネージャー

    1名 1名 外部パートナー(全⾯委託) 全⾯委託 計6名 マネジメント担当のみ 開発は全⾯的に外部委託∕iOSとAndroidは別々のチーム 15
  6. 第2章|なぜそこまで壊れたのか 構造的なサイロ化 iOSチーム 委託先メンバーA SPEC iOSアプリ 仕様の解釈違い 仕様‧要件 TRUSTDOCK RESULT

    コード共有やコミュニケーションが弱い Androidチーム 委託先メンバーB 実装の挙動差 Androidアプリ 品質のばらつき OS間で機能差‧品質差が恒常的に⽣まれる 16
  7. 第2章|なぜそこまで壊れたのか ⾞輪の再発明と品質の真空地帯 01 ⾞輪の再発明(重複実装) SDK ⾝元確認ロジック アプリ = ⾝元確認ロジック ⽚⽅を直しても、もう⽚⽅にバグが残る

    02 品質の真空地帯(QA不在) 外部 パートナーが 納品 QA‧ 受⼊テスト = 不在 そのまま ユーザーへ ユーザーの⼿元で初めて不具合が発覚 重複実装と、作られたものを検証‧担保する主体がいない 17
  8. 第3章|Flutter再設計を選んだ事業判断 なぜ、Flutterだったのか 純粋な技術的優位性「だけ」で 選んだわけではない ORGANIZATION TECHNOLOGY 組織 技術 外部依存の脱却 単⼀チーム化

    Flutter 単⼀コード 品質の統制 コントロールを社内に取り戻す 「内製化」 「組織を再構築するための 最適なプラットフォーム」 としての選択 20
  9. 第3章|Flutter再設計を選んだ事業判断 なぜFlutterを選んだのか? 01 02 03 採⽤市場 内製化 品質差の解消 候補者プールを作りやすかった 開発のコントロールを社内に戻す

    OS間の差が構造的に消える 当時:iOS/Androidエンジニアの採⽤市場が枯渇∕初期メンバーを⼀気に集められる地盤があった 21
  10. 第3章|Flutter再設計を選んだ事業判断 選ばなかった選択肢 当時の判断軸 社内親和性 React Native Kotlin Multiplatform Xamarin Flutter

    成熟度 採⽤市場 社内にReact/Webの親和性なし ⾒送り 成熟度 当時は成熟度が不⾜ ⾒送り 将来性‧採⽤市場 衰退傾向‧C#⼈材なし ⾒送り 採⽤‧内製化‧品質差解消に合致 採⽤ 社内親和性 すべてに合致 技術の優劣ではなく「⾃分たちの組織で回せるか」で判断 22
  11. 第3章|Flutter再設計を選んだ事業判断 構造的サイロ化の強制リセット BEFORE AFTER 外部委託‧OS別の分業体制 社内の1つのチーム iOSチーム iOSアプリ Flutter Flutterエンジニア

    + QA(社内) 内製化 Androidチーム Androidアプリ コード2つ‧仕様解釈もバラバラ クロスプラットフォームによる 「単⼀コードベース」化 単⼀コードベース iOS/Android を同じ仕様で 機能差が消滅し、全員が同じ仕様に向き合う 「1つのチーム」へ 23
  12. 第3章|Flutter再設計を選んだ事業判断 結果 評価1.2からの脱却 App Store App Store 1.2 Flutter Rewrite

    体制刷新 ⾝元確認ロジックをSDKに寄せ 重複ロジックを整理‧改善 致命的なクラッシュが激減し、 両OSの品質が均質化 4.0+ 現在のApp Store評価は 「4.0以上」で安定 24
  13. 第4章|1.2→4.0 その後 技術だけの魔法ではない 評価 4.0の内訳 事業側の後押し マーケティング施策 Business ‧iOSのみストア評価をリセット ‧インセンティブ配布キャンペーンを併⽤

    (効果は狙い通りでない⾯も) * キャンペーンは、レビュー投稿‧評価を条件にしていません 本質的な品質改善 Technology ⼟台 アプリ⾃体の基本品質向上が⾼評価の⼟台になった 技術⾯の改善が⼤前提だが、事業側の泥臭い後押しも存在した 26
  14. 第5章|まとめ 今の「4.0以上」を⽀えているもの App Store評価 SUSTAINED ⼀過性のキャンペーン 終了後も、評価4.0は 維持され続けている 4.0以上 致命的クラッシュ

    iOS Androidの の激減 機能差解消 ROOT CAUS 評価1.2は 「体制の問題」であり、 4.0は「作り⽅を変えた結果」 ⼟台:開発体制と作り⽅の再設計 組織構造を正しくリファクタリングすれば、 プロダクトは必ず応えてくれる 28
  15. ⾃⼰紹介 陳 信瑜 株式会社TRUSTDOCK デジタルIDウォレット部 GitHub:Ricky-yu Flutterエンジニア X:@ts00484515 • 本⼈確認(eKYC)アプリの設計改善‧運⽤

    • iOS‧Android‧Flutterアプリでの開発経験 • 前職でFlutterKaigi2025のLTに登壇 「MVVMからMVVM-Cに変えてみた話」 31
  16. 旧アプリの構成 OSごとの制約を、それぞれのアプリで扱っていた 旧ネイティブApp|OSごとに実装 iOS Swift Android Kotlin UI 表⽰‧遷移 UI

    表⽰‧遷移 App∕SDKの外側 Navigator∕loadi ng 案内‧処理再開 UseCase∕Reposit ory ⼿順‧AppError Remote通信 App API SDK呼出 ネイティブSDK Navigator∕Paus eHandler 案内‧処理再開 UseCase∕Reposit ory ⼿順‧AppError Remote通信 App API SDK呼出 ネイティブSDK OSごとの制約を、それぞれのAppで⽀えていた 35
  17. 設計の全体像 Flutter版で分けた、AppとSDKの責任 Flutter(Dart)共通のApp UI 表⽰‧遷移 Stateを監視し、 表⽰を更新 MVVM + Clean

    Architecture ViewModel State Remote DataSource App API SDK別 Local DataSource Plugin Swift∕Kotlin UseCase ⼿順‧再試⾏ Repository AppErrorへ変換 Channel ネイティブSDK (2種類) iOS∕Androidへ接続 SDKごとの接続を分け、アプリ側の判断を両OSで共有する 36
  18. 01 SDK連携と共通フロー SDKとApp APIをつなぐ本⼈確認フロー iOS∕Android × 2種類のSDK × App API

    緑:SDK ⽩:アプリ ① 事業者への資料提出(事業者⽤SDK) アプリ 同意API SDK 撮影‧読取‧提出 アプリ 提出完了API 提出完了画⾯ 必要な結果 がそろう 本⼈確認完了API 成功後に完了画⾯ SDKの提出後も、アプリの提出完了APIが残る ② 認証アカウント(eKYC SDK) SDKを起動 読取‧本⼈確認 SDKの最終結果 開始IDで呼ぶ 本⼈確認開始API SDKの終了と、アプリの完了条件を分けて扱う 38
  19. ① エラー責務の整理 外部のエラーをAppErrorへ変換する ⽐較する点:外部の失敗を、アプリで扱う型へ変える場所 旧iOS:HTTP応答をAppErrorへ // HTTPステータスによる変換(抜粋) case 400: throw

    AppError.illegalArgument(appErrorInfo) case 400: throw AppError.notFound 旧Android:通信例外をAppErrorへ is IOException -> AppError.Io(AppErrorDetail(reason = this)) // 通信例外をAppErrorへ変換 Flutter:Repositoryから共通変換へ } on GrpcError catch (e) { throw getAppErrorFromGrpcError(e, ApiType .completeVerificationSubmission); } // 共通変換関数の一部 case StatusCode.invalidArgument: return InvalidArgumentError(e, type: type); 外部エラーをAppErrorへそろえ、両OSで判断する 41
  20. ① エラー責務の整理 分類できない失敗の、ユーザーへの案内 ⽐較する点:例外の⽂⾔を、どこでユーザー向けにするか 旧iOS:例外の説明を表⽰ // AppError外のエラーの表示 message: error.localizedDescription, //

    title・callbackなどは省略 旧Android:⼀部の例外の⽂⾔を表⽰ } else if ( it.localizedMessage?.isNotEmpty() == true ) { it.localizedMessage } Flutter:変換先に案内を⽤意 // gRPCの共通変換関数 default: return UnknownError(error: e); // UnknownErrorのmessage引数 message: i18n.text .didw__common_error_server_description, 未知のエラーにも、共通のユーザー向け案内を⽤意 42
  21. ① エラー責務の整理 変換処理を、通信や画⾯から離して確かめる 新設計の確認:事業者への提出の完了APIのテスト エラーが返る流れ エラーを起こし、型と案内を確認する DataSource(mock) GrpcError Repository(実装) AppError

    expect(型‧共通メッセージ) // Providerの設定・Repositoryの取得は省略 when(mockDataSource.completeVerification(any)) .thenThrow(const GrpcError.unauthenticated()); try { await repository.completeVerification(publicId); } on ApiError catch (e) { expect(e, isA<UnauthenticatedError>()); expect(e.message, i18n.text .didw__common_error_auth_quit_description); } 通信だけを差し替え、実際の変換処理を確かめる 43
  22. ② 状態とUIの整合 処理中の表⽰を、どの状態に合わせるか ⽐較する点:Appのloadingを表⽰‧終了する⽅法 旧iOS:共通の通知からローディング表⽰を操作 // loadingOverlayの通知内 if show {

    me.loadingHUD.show() } else { me.loadingHUD.hide() } 旧Android:Pause中の操作を保留 fragmentHandler.post { progress.show(fm, tag) } Flutter:機能のStateから表⽰ final state = ref.watch( verifyConsentViewModelProvider); // StateをScreenへ渡す(省略) // ScreenのStack内 if (verifyConsentViewEntity.isLoading) const CommonLoading(), 表⽰と実⾏の条件を、同じStateにそろえる 45
  23. ② 状態とUIの整合 SDKとApp API、必要な結果がそろうまで待つ 具体例:開始IDの通知と、SDKの最終結果が別々に届く Flutter ViewModel SDK App API

    ❶ ID監視後、SDKを起動 ❷ 開始ID(EventChannel) ❸ Appの開始API ❹ 最終結果(MethodChannel) 開始APIの終了を待つ ❺ 応答とApp側ID ❻ App側IDで完了API SDKとAPIの終了を待ち、App側で次の処理を決める 46
  24. ② 状態とUIの整合 待つ条件を、ViewModelのコードへ置く 共通フローの具体例:開始IDの通知と、SDKの最終結果 開始IDの通知を受ける処理 final result = await _startAccountVerificationUseCase.invoke(

    verificationId: id, accountVerificationPolicyId: policyId); if (result.isSuccess) { _userIdentityVerificationId = result.getResult .userIdentityVerificationId; } completer.complete(); SDKの最終結果を受ける処理 // 外側でCompleter<void>()を作成 final sdkResult = await _startSdkUseCase.invoke(); // SDK結果がcompletedの分岐 case CoreEkycResultStatus.completed: await completer.future; await _handleCompletedResult( sdkResult.getResult); 待ち合わせの条件を、⼀つのViewModelに置く 47
  25. ② 状態とUIの整合 SDK完了と、Appの完了画⾯を分ける 業務の結果をStateへ⼊れ、UIが表⽰を決める ViewModel:起動処理の⼊⼝と終了時 if (state.isLoading) return; state =

    state.copyWith(isLoading: true); try { // SDKとApp APIの処理(省略) } finally { await verificationIdSubscription ?.cancel(); state = state.copyWith(isLoading: false); } UI:Stateで操作と表⽰を合わせる 処理中 isLoading 完了API成功 isCoreEkycComplete 失敗の案内 errorMessage CommonLoading 登録完了画⾯ エラー案内 Appの成功をStateへ反映してから、完了画⾯へ進む 48
  26. ③ 通信リトライ戦略 旧iOS‧Androidと、提出完了処理のリトライ OSごとに違った再試⾏と、Flutter版の業務ルール 旧iOS:最終提出の試⾏数を指定 .executeToCompleteAllSubmission() .retry(maxAttemptCount) // maxAttemptCount =

    5 旧Android:通信層で固定待機 Thread.sleep(WAITING_TIMER_SECOND) call.clone().enqueue(this) // 1秒ずつ、最大3回の再試行 Flutter:完了UseCaseで判断 } on NetworkError catch (e) { if (_retryManager.canRetry()) { await Future<void>.delayed( _retryManager.duration); return invoke(verificationId); } else { return UseCaseResult(failure: e); } } 再試⾏の条件はUseCase、待ち時間はRetryManager 50
  27. ③ 通信リトライ戦略 失敗した提出完了APIから、⼿続きを続ける 新設計の適⽤例:事業者への提出の完了API 通信エラー 担当:UseCase NetworkError 上限内で待つ 完了APIを再実⾏ 待ち時間:RetryManager

    Appの認証エラー 担当:ViewModelとUI 認証エラー ユーザーが再認証 完了APIを再実⾏ 撮影を繰り返さず、失敗したApp APIから再開する 51
  28. 設計の学び 三つの変更と、その効果 設計 旧コードで⾒る点 Flutter版で変えた判断 効果 ① エラー エラーの分類と案内を OSごとの部品に実装

    共通の変換関数で整理し 未知エラーもAppErrorへ 実際の変換を 通信‧画⾯なしで確認 ② 状態とUI iOS:通知で表⽰を操作 Android:Pause中は保留 Stateで表⽰‧実⾏条件と 完了後の遷移を判断 Appの表⽰を共通化 App完了後に画⾯へ ③ リトライ iOS:試⾏数を指定 Android:固定1秒‧3回 完了UseCaseが対象を判断 待ち時間は別の部品で計算 原因に合う⼿順で 完了APIから再開 Appの判断を共有し、⼀つの改善を両OSへ届ける 52