Submit Search
マッチングアプリ『Omiai』の Flutter へのリプレイスの挑戦 (FlutterKaigi 2024)
0 likes
1,118 views
Kosuke Saigusa
FlutterKaigi 2024 において「マッチングアプリ『Omiai』の Flutter へのリプレイスの挑戦」のテーマで発表したスライドです。
Engineering
Read more
1 of 88
Download now
Downloaded 22 times
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Most read
47
48
Most read
49
50
51
52
53
54
Most read
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
More Related Content
PDF
Twitterのリアルタイム分散処理システム「Storm」入門
AdvancedTechNight
PDF
Open vSwitchソースコードの全体像
Sho Shimizu
PDF
コンテナを突き破れ!! ~コンテナセキュリティ入門基礎の基礎~(Kubernetes Novice Tokyo #11 発表資料)
NTT DATA Technology & Innovation
PDF
Application Re-Architecture Technology ~ StrutsからSpring MVCへ ~
Yuichi Hasegawa
PPTX
Wiresharkの解析プラグインを作る ssmjp 201409
稔 小林
PDF
今からはじめる! Linuxコマンド入門
VirtualTech Japan Inc.
PDF
Cassandra導入事例と現場視点での苦労したポイント cassandra summit2014jpn
haketa
PDF
YJTC19 B-1 パスワードレス普及への取り組み/ヤフーのデータ戦略を支えるID連携 #yjtc
Yahoo!デベロッパーネットワーク
Twitterのリアルタイム分散処理システム「Storm」入門
AdvancedTechNight
Open vSwitchソースコードの全体像
Sho Shimizu
コンテナを突き破れ!! ~コンテナセキュリティ入門基礎の基礎~(Kubernetes Novice Tokyo #11 発表資料)
NTT DATA Technology & Innovation
Application Re-Architecture Technology ~ StrutsからSpring MVCへ ~
Yuichi Hasegawa
Wiresharkの解析プラグインを作る ssmjp 201409
稔 小林
今からはじめる! Linuxコマンド入門
VirtualTech Japan Inc.
Cassandra導入事例と現場視点での苦労したポイント cassandra summit2014jpn
haketa
YJTC19 B-1 パスワードレス普及への取り組み/ヤフーのデータ戦略を支えるID連携 #yjtc
Yahoo!デベロッパーネットワーク
What's hot
(9)
DOC
Le pendule de Foucault, une invitation à « venir voir tourner la Terre »
Labbe Caroline
PPTX
BOMB DETECTION ROBOT BY USING GSM & GPS
JOLLUSUDARSHANREDDY
PDF
SRv6 Mobile User Plane : Initial POC and Implementation
Kentaro Ebisawa
PDF
IoT Based Solar Water Pump Controller
IJSRED
PPT
RFID Basics
fizzyjazzy
PDF
Real Time Hand Gesture Recognition Based Control of Arduino Robot
ijtsrd
PPTX
Rfid based attendance system
eskkarthik
PPT
Chapter 7 review jeopardy nomenclature only
dirksr
PDF
FPGAで作るOpenFlow Switch (FPGAエクストリーム・コンピューティング 第6回) FPGAX#6
Kentaro Ebisawa
Le pendule de Foucault, une invitation à « venir voir tourner la Terre »
Labbe Caroline
BOMB DETECTION ROBOT BY USING GSM & GPS
JOLLUSUDARSHANREDDY
SRv6 Mobile User Plane : Initial POC and Implementation
Kentaro Ebisawa
IoT Based Solar Water Pump Controller
IJSRED
RFID Basics
fizzyjazzy
Real Time Hand Gesture Recognition Based Control of Arduino Robot
ijtsrd
Rfid based attendance system
eskkarthik
Chapter 7 review jeopardy nomenclature only
dirksr
FPGAで作るOpenFlow Switch (FPGAエクストリーム・コンピューティング 第6回) FPGAX#6
Kentaro Ebisawa
Ad
Similar to マッチングアプリ『Omiai』の Flutter へのリプレイスの挑戦 (FlutterKaigi 2024)
(20)
PPTX
ひと漕ぎで二度おいしい!? Flutterを使ったモバイルアプリ開発への期待と実態と付き合い方(NTTデータ テクノロジーカンファレンス 2020 発表資料)
NTT DATA Technology & Innovation
PDF
【2018/09/11】PAYでのReact Nativeにおける APIクライアント実装 について
Natsuki Yamanaka
PDF
Flutter_Forward_Extended_Kyoto-Keynote_Summary
cch-robo
PPTX
Flutter (フラッター)
fujita noriko
PDF
Flutterアプリ開発におけるモジュール分割戦略
Yamashita Takeshi
PDF
appengine ja night #24 Google Cloud Endpoints and BigQuery
Ryo Yamasaki
KEY
Inside frogc in Dart
Goro Fuji
PDF
Flutterやってみよう
Ryuto Yasugi
PDF
Flutterで単体テストを行う方法とGitHub Actionsを使った自動化
Shinnosuke Tokuda
PDF
iPhoneとAndroidのアプリ開発最新潮流
Rakuten Group, Inc.
PDF
Flutter のリアクティブ戦略 set state 〜 redux まで
cch-robo
PDF
オープンソースエコシステム #demodaytokyo
Shuichi Tsutsumi
PDF
Api
Jun Chiba
PDF
関西FirefoxOS勉強会6thGiG「アプリ間通信」
Noritada Shimizu
PDF
The Essence of Using Ruby on Rails in Corporations
Koichiro Ohba
PDF
超高速でflutterアプリをビルドする
ssuser34abd0
PDF
Google I/O 2021 Flutter 全体報告
cch-robo
PDF
Flutterを体験してみませんか
cch-robo
KEY
スマートフォンアプリケーション開発の最新動向
Tsutomu Ogasawara
PDF
Building a Flutter Development Environment with VSCode and Useful Extensions
Shotaro Suzuki
ひと漕ぎで二度おいしい!? Flutterを使ったモバイルアプリ開発への期待と実態と付き合い方(NTTデータ テクノロジーカンファレンス 2020 発表資料)
NTT DATA Technology & Innovation
【2018/09/11】PAYでのReact Nativeにおける APIクライアント実装 について
Natsuki Yamanaka
Flutter_Forward_Extended_Kyoto-Keynote_Summary
cch-robo
Flutter (フラッター)
fujita noriko
Flutterアプリ開発におけるモジュール分割戦略
Yamashita Takeshi
appengine ja night #24 Google Cloud Endpoints and BigQuery
Ryo Yamasaki
Inside frogc in Dart
Goro Fuji
Flutterやってみよう
Ryuto Yasugi
Flutterで単体テストを行う方法とGitHub Actionsを使った自動化
Shinnosuke Tokuda
iPhoneとAndroidのアプリ開発最新潮流
Rakuten Group, Inc.
Flutter のリアクティブ戦略 set state 〜 redux まで
cch-robo
オープンソースエコシステム #demodaytokyo
Shuichi Tsutsumi
Api
Jun Chiba
関西FirefoxOS勉強会6thGiG「アプリ間通信」
Noritada Shimizu
The Essence of Using Ruby on Rails in Corporations
Koichiro Ohba
超高速でflutterアプリをビルドする
ssuser34abd0
Google I/O 2021 Flutter 全体報告
cch-robo
Flutterを体験してみませんか
cch-robo
スマートフォンアプリケーション開発の最新動向
Tsutomu Ogasawara
Building a Flutter Development Environment with VSCode and Useful Extensions
Shotaro Suzuki
Ad
マッチングアプリ『Omiai』の Flutter へのリプレイスの挑戦 (FlutterKaigi 2024)
1.
FlutterKaigi2024 マッチングアプリ『Omiai』の Flutterへのリプレイスの挑戦 Kosuke Saigusa(株式会社 Omiai) 1
2.
自己紹介 Kosuke Saigusa (@kosukesaigusa) 株式会社
Omiai Flutter テックリード FlutterNinjas 2024, FlutterKaigi 2023 等で登壇 FlutterGakkai, 東京 Flutter ハッカソン運営 2
3.
Omiaiについて 3
4.
昨今のマッチングアプリと市場 4
5.
エニトグループについて 5
6.
ホールディングス経営体制 6
7.
本セッションのゴール 7
8.
本セッションのゴール マッチングアプリ『Omiai』を題材に 大規模アプリのFlutterへのリプレイスを 成功に導くアプローチを考える 8
9.
なぜ『Omiai』はFlutterにリプレイスするのか? 9
10.
なぜ『Omiai』はFlutterにリプレイスするのか? 💬 背景 2012 年から長年運営されてきたサービス iOS・Android
(, Web) の各プラットフォームの古いコードベースが抱える多くの技 術的負債や仕様差異 サーバサイドの負債の解消・仕様変更もクライアントアプリが理由で進めづらい ✅ 目標 Flutter へのリプレイスによる単一コードベース化と技術的負債の返済 リプレイス中も機能追加は止めない toC アプリとしてあるべき素早く・細かい開発サイクルでの開発・検証を実現する 10
11.
どのようにリプレイスするのか? 11
12.
どのようにリプレイスするのか? ✅ 負債化を繰り返さず、高い開発生産性を維持し続ける アーキテクチャ、CI、テスト、ドキュメンテーションなどで多くの工夫 ✅ Flutter
経験の無いメンバーや新規参画メンバーが早期に活躍できる Flutter 経験者ゼロ・業務委託中心組織から正社員採用によるチームの拡大へ ✅ リプレイス中も機能追加は止めない Add-to-App を利用して既存アプリから Flutter をモジュールとして呼び出す 主要なユーザー体験から順に、新しい機能を追加しながら Flutter にリプレイスす る 12
13.
陥りがちな現実 13
14.
PRレビューで防ぐしかないコード規約違反 ElevatedButton( onPressed: () async
{ await someRepositoryMethod(); }, /** 省略 */ ); UI 層から直接 Repository のメソッドを呼び出すのを禁止している場合 コード規約で縛り、PR レビューで防ぐしか方法がない 見逃されたコードが生き残ると、コード規約が曖昧に 14
15.
UIとロジックの分離の失敗 import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; Future<void>
someLogic(BuildContext context) async { await doSomething(); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(AppLocalizations.of(context).success)), ); } 業務ロジックの実装に BuildContext が依存する ロジックのユニットテストが Dart のユニットテストで書けない 15
16.
不自然な依存 import 'package:dio/dio.dart'; ElevatedButton( onPressed: ()
async { try { await doSomething(); } on DioException catch (e) { /** 省略 */ }, }, /** 省略 */ ), UI 層が特定の HTTP クライアントパッケージに依存するのは不自然(知るべきでな い) 16
17.
曖昧な例外ハンドリングの方針 ElevatedButton( onPressed: () async
{ try { await doSomething(); } on APIException catch (e) { await showDialog<void>( context: context, builder: (context) => AlertDialog(content: Text('${e.statusCode}')), ); }, }, /** 省略 */ ), UI 層で HTTP 通信が起因の例外を捕捉するのは OK? 捕捉した例外をどのようにユーザー体験に反映するかの方針が明確でない 図らずエラーコードなどの情報をユーザーに見せてしまう 17
18.
依存を誤って負債化 class SomeRepository { /**
省略 */ Future<Something> fetchSomething({/** 省略 */}) async { final accessToken = _ref.read(accessTokenProvider); final response = await _client.request(/** 省略 */); /** 省略 */ } } 通信層が、業務知識としての認証情報に依存する 通信層のユニットテストを書くのに、業務知識をモックする必要がある もしこの依存が許されるなら、あらゆる業務知識に同様に依存し始める危険がある 18
19.
依存を誤って負債化 class PaginatedFetcher<T> { /**
省略 */ Future<List<T>> fetchNextPage(int page, int perPage) async { /** 省略 */ final items = await fetch(/** 省略 */); /** 省略 */ await ref.read(someUseCaseProvider).doSomething(); return items; } } 当初の汎用的な基盤実装が、いつの間にか意図しない対象に依存するようになる 汎用性は失われ、責務が曖昧な負債コードに 19
20.
クライアント・サーバサイドアプリの密結合 ElevatedButton( onPressed: () async
{ try { await doSomething(); } on APIException catch (e) { await showDialog<void>( context: context, builder: (context) => AlertDialog(content: Text(e.message)), ); }, }, /** 省略 */ ), ユーザーに知らせるエラーメッセージに、サーバサイドからのレスポンスデータが そのまま利用される クライアントアプリとサーバサイドアプリの境界が曖昧 20
21.
どのように防ぐ? 21
22.
可能な限り仕組みで防ぎたい! 22
23.
方針 ✅ マルチパッケージ構成 レイヤーごとにパッケージを分ける 各パッケージの責務と許可される依存を厳密にする Flutter の世界と
Dart の世界、クライアントアプリとサーバサイドアプリの境界を 明確にする ✅ ユニットテストを十分に継続的に書く 業務ロジック層およびそれより抽象的な層で、ユニットテストがカバレッジ 100% で記述できる ✅ 実装を可能な限りパターン化する 典型的な実装をパターン化し、新規参画メンバーの早期の活躍を実現する 23
24.
パッケージ構成 24
25.
パッケージ一覧(抜粋) レイヤーごとにパッケージを分ける 各パッケージに許される直接的な依存は矢印の先のパッケージのみに限定される 他にも Unimplemented なインターフェースのみを定義し、各パッケージに利用さ せるパッケージなどもある ※
ことば遣いは Flutter 公式の"Architecting Flutter apps" や Android Developers の "Guide to app architecture" に類似 25
26.
パッケージ一覧(app) Flutter アプリのエントリポイントとしての実装 ProviderScope.overrides による依存の注入 UI
層としてのユーザー体験を実装 ウィジェットツリーの構築、画面遷移、各画面やコンポーネントの描画とインタ ラクション 26
27.
パッケージ一覧(base_ui) テキストスタイル、色などのスタイルガイドの実装 画像、ボタン、AppBar、TextField などの基礎的な UI
部品の実装 27
28.
パッケージ一覧(domain) クライアントアプリの業務概念を表すエンティティの定義 ユーザー体験として利用する例外型の定義 データの取得や保存などを含む業務ロジックの記述 28
29.
パッケージ一覧(repository) データソースとのやり取りの実装 接続先のデータソースの情報は表出させない 29
30.
パッケージ一覧(system) 3rd パーティのツールをラップして基礎的な実装を記述 例:HTTP クライアント、Shared
Preferences, Firebase Analytics など 3rd パーティのツールとの依存は例外も含めて閉じ込めて外部に表出させない 不要な機能のインターフェースは塞いでシンプルにする 30
31.
パッケージ一覧(Flutterの世界) app, base_ui パッケージは
Flutter に直接依存する Flutter エンジニアとしてコードを書く 31
32.
パッケージ一覧(Dartの世界) domain, repository, system
パッケージは Flutter には直接依存しない Dart エンジニアとしてコードを書く Dart 言語と riverpod や freezed にさえ慣れれば Flutter 経験が浅くても書ける 32
33.
パッケージ一覧(サーバサイドとの境界) repository パッケージがサーバサイドとの境界として働く サーバサイドの負債は repository
パッケージで吸収し切り、app, domain に流入 させない 33
34.
各パッケージの実装内容 34
35.
base_ui テキストスタイル、色などのスタイルガイドの実装 画像、ボタン、AppBar、TextField などの基礎的な UI
部品の実装 35
36.
base_uiが依存するパッケージの例 dependencies: extended_image: flutter: sdk: flutter flutter_svg: dev_dependencies: flutter_gen_runner: Flutter への依存:
direct 依存パッケージの例: extended_image: 画像の基盤実装 flutter_gen: 静的アセットのコード生成 36
37.
base_uiの実装例 /// アプリで使用する色一覧。 abstract interface
class AppColor { /// プライマリーの 01 の赤色。 static const Color primary01 = Color(0xFFFF3159); /// プライマリーの 02 の青色。 static const Color primary02 = Color(0xFF3F9AD1); /// セカンダリーの 01 のピンク色。 static const Color secondary01 = Color(0xFFFE8D8B); /** 省略 */ } Figma と同名で定義された TextStyle, Color などのスタイルガイド 37
38.
base_uiの実装例 /// アプリ内で共通で用いられる [ElevatedButton]. class
CommonElevatedButton extends StatelessWidget { /// S サイズの [CommonElevatedButton] を生成する。 const CommonElevatedButton.s({/** 省略 */}); /// S サイズのアイコン付きの [CommonElevatedButton] を生成する。 const CommonElevatedButton.sWithIcon({/** 省略 */}); /** 省略 */ } ボタン、画像、AppBar、TextField などの基礎的な UI 部品 Figma のコンポーネント定義と一致するインターフェース 38
39.
system 3rd パーティのツールをラップして基礎的な実装を記述 外部パッケージへの依存を閉じ込めて、外部に表出させない 39
40.
systemが依存するパッケージの例 dependencies: dio: firebase_analytics: firebase_remote_config: shared_preferences: Flutter への依存: transitive 各種の
3rd パーティのツールに依存する 40
41.
systemの実装例 class HttpClient { HttpClient({required
String baseUrl, Map<String, dynamic>? headers}) : _client = DioHttpClient(baseUrl: baseUrl, headers: headers); Future<HttpResponse<dynamic>> request(/** 省略 */) async {/** 省略 */} } HTTPClient クラスの例 内部で dio パッケージへ依存 41
42.
systemの実装例 @freezed sealed class HttpResponse<T>
with _$HttpResponse<T> { const factory HttpResponse.success({ required T data, required Map<String, List<String>> headers, }) = SuccessHttpResponse<T>; const factory HttpResponse.failure({ T? data, required Object e, required ErrorStatus status, }) = FailureHttpResponse<T>; } HTTP リクエストの結果を表現する HttpResponse クラスの例 Result 型のような形で HTTP リクエストの成功・失敗を表現 42
43.
systemの実装例 class HttpClient { /**
省略 */ Future<HttpResponse<dynamic>> request({/** 省略 */}) async { try { final response = await _client.request<dynamic>(/** 省略 */); return HttpResponse.success(/** 省略 */); } on DioException catch (e) { /** 省略 */ return HttpResponse.failure(/** 省略 */); } } } HTTP リクエストの結果として HttpResponse を返す dio への依存は例外も含めて外部には表出させない 43
44.
repository データソースとのやり取りの実装 接続先のデータソースの情報は表出させない 44
45.
repositoryが依存するパッケージ dependencies: freezed_annotation: json_annotation: riverpod: system: path: ../system dev_dependencies: freezed: json_serializable: riverpod_generator: Flutter への依存:
transitive system パッケージに依存して、HTTP クライアントなどを利用する json_serializable, freezed で Dto の定義を行う 45
46.
repositoryの実装例 @freezed sealed class RepositoryResult<T>
with _$RepositoryResult<T> { const factory RepositoryResult.success(T data) = SuccessRepositoryResult<T>; const factory RepositoryResult.failure( Object e, { FailureRepositoryResultReason? reason, ErrorDto? errorDto, }) = FailureRepositoryResult; } Repository による通信結果を表現する RepositoryResult クラスの例 Result 型のような形で成功・失敗を表現 46
47.
repositoryの実装例 class AccountDetailRepository { /**
省略 */ Future<RepositoryResult<AccountDetailDto>> fetchAccountDetail() async { final response = await _ref.read(httpClientProvider).request(/** 省略 */); switch (response) { case SuccessHttpResponse(:final data): final dto = AccountDetailDto.fromJson(data as Map<String, dynamic>); return RepositoryResult.success(dto); case FailureHttpResponse(/** 省略 */): /** 省略 */ return RepositoryResult.failure(/** 省略 */); } } } HttpResponse の成功・失敗を switch 文で分岐して、 RepositoryResult を返す 47
48.
repositoryの実装例 @freezed class FooDto with
_$FooDto { const factory FooDto({ @JsonKey(name: 'inappropriate_field_name') required String someFieldName, @Default([]) List<String> things, @flexibleBoolConverter required bool isSomething, @flexibleDateTimeConverter required DateTime someDateTime, }) = _FooDto; factory FooDto.fromJson(Map<String, dynamic> json) => _$FooDtoFromJson(json); } Dto として HTTP レスポンスの型定義をする 不適切なフィールド名や型を変換して、サーバサイドの負債を持ち込まない 48
49.
domain クライアントアプリの業務概念を表すエンティティの定義 ユーザー体験として利用する例外型の定義 データの取得や保存などを含む業務ロジックの記述 49
50.
domainが依存するパッケージ dependencies: freezed_annotation: repository: path: ../repository riverpod: riverpod_annotation: dev_dependencies: freezed: riverpod_generator: Flutter への依存:
transitive repository パッケージに依存してデータにアクセスする freezed で業務概念のエンティティを定義する riverpod で状態管理ロジックを実装する 50
51.
domainの実装例 @freezed class Member with
_$Member { const factory Member({ required int userId, /** 省略 */ required LogInStatus loginStatus, }) = _Member; factory Member.fromDto(MemberDto dto) => Member( userId: dto.userId, /** 省略 */ loginStatus: LogInStatus.fromDateTime(dto.lastAccessedAt), ); } マッチングアプリにおけるお相手メンバーの業務概念の定義の例 HTTP レスポンスに対応する Dto からエンティティを生成する 51
52.
domainの実装例 enum LogInStatus { active, recent, inactive, ; factory
LogInStatus.fromDateTime(DateTime lastAccessedAt) {/** 省略 */} } お相手メンバーのログインステータス(直近どのくらいアクティブか)の定義 最終ログイン日時というサーバサイド (DB) 上の事実から、ログインステータスと いうクライアントアプリにおける業務概念を計算する 52
53.
domainの実装例 @riverpod Future<Member> memberDetail(MemberDetailRef ref,
{required int userId}) async { final result = await ref.read(memberRepositoryProvider).fetchMember(userId); switch (result) { case SuccessRepositoryResult(:final data): return Member.fromDto(data); case FailureRepositoryResult(/** 省略 */): /** 省略 */ } } riverpod の関数で取得処理のロジックを記述する repository による取得結果を switch 文で分岐して処理する 53
54.
domainの実装例 @Riverpod(keepAlive: true) class AccountDetailNotifier
extends _$AccountDetailNotifier { /** 省略 */ Future<void> updateAccountDetail() async { final result = await ref.read(accountDetailRepositoryProvider).updateAccountDetail(/** 省略 */); switch (result) { case SuccessRepositoryResult(/** 省略 */): /** 省略 */ case FailureRepositoryResult(/** 省略 */): /** 省略 */ } } } riverpod のクラス (Notifier) で状態管理のロジックを記述する 54
55.
domainの実装例 class SendLikeUseCase { /**
省略 */ Future<void> invoke(/** 省略 */) async { final result = await _ref.read(likeRepositoryProvider).sendLike(/** 省略 */); switch (result) { /** 省略 */ case FailureRepositoryResult(/** 省略 */): /** 省略 */ throw InsufficientPointsToSendLikeException(); } } } class InsufficientPointsToSendLikeException implements Exception {} お相手に「いいね!」を送信するロジック(状態管理を伴わない) 業務ロジックとして取り扱うべき例外を定義し、スローする 55
56.
app Flutter アプリのエントリポイントとしての実装 ProviderScope.overrides による依存の注入 UI
層としてのユーザー体験を実装 ウィジェットツリーの構築、画面遷移、各画面やコンポーネントの描画、ユーザ ー操作とのインタラクション 56
57.
appが依存するパッケージ dependencies: auto_route: base_ui: path: ../base_ui domain: path: ../domain flutter: sdk:
flutter flutter_hooks: hooks_riverpod: dev_dependencies: auto_route_generator: Flutter への依存: direct base_ui, domain パッケージに依存する auto_route, flutter_hooks など UI 層で利用するパッケージに依存する 57
58.
appの実装例 @RoutePage() class MemberProfilePage extends
StatelessWidget { const MemberProfilePage({ @PathParam('userId') required this.userId, super.key, }); static String resolvePath({required String userId}) => '/users/$userId'; final int userId; @override Widget build(BuildContext context) {/** 省略 */} } auto_route パッケージを利用してルート定義 58
59.
appの実装例 domain 層の取得ロジックを ref.watch
してユーザーが目にする体験を実装する @override Widget build(BuildContext context, WidgetRef ref) { // メンバー情報を取得する。 final memberAsyncValue = ref.watch(memberDetailProvider(userId: userId)); return switch (memberAsyncValue) { AsyncData(value: final member) => /** 省略 */, AsyncError() => /** 省略 */, _ => /** 省略 */, }; } 59
60.
appの実装例 CommonFilledButton.sWithIcon( onPressed: () async
{ try { await _ref.read(sendLikeUseCaseProvider).invoke(/** 省略 */); } on InsufficientPointsToSendLikeException catch (e) { await _showErrorDialog(/** 省略 */); } /** 省略 */ } /** 省略 */ ) UI を通じたインタラクションをトリガーに domain 層のロジックを呼び出す domain 層に定義された例外を実現したいユーザー体験に従って処理する 60
61.
パッケージ構成 repository - system
間 dio パッケージを通じて HTTP リクエストを行う dio パッケージへの依存は例外も含めて repository に表出しない Result 型のような HttpResponse 型をインターフェースとすることで、repository 層での switch による成功・失敗のハンドリングが強制、画一化される 61
62.
パッケージ構成 domain - repository
間 repository のメソッドを呼び出してデータを読み書きする Result 型のような RepositoryResult 型をインターフェースとすることで、 domain 層での switch による成功・失敗のハンドリングが強制、画一化される domain 層では Dto から業務知識を表すエンティティを生成し、サーバサイドの負 債は持ち込まない 62
63.
パッケージ構成 app - domain
間 domain は業務概念を表すデータと業務ロジックを提供する app はユーザー体験の実現のために domain のロジックを利用し、業務概念のデー タを読み書きする domain は業務知識として取り扱うべき例外をスローする app はそれを捕捉してユーザー体験に反映する 63
64.
Tips:アクセストークンを通信層で利用する 別パッケージにアクセストークンを取得するインターフェースのみを定義する: @riverpod Future<String?> Function() extractAccessToken(Ref
ref) => throw UnimplementedError(); インターフェースを通じてアクセストークンを取得して利用する: class SomeRepository { const SomeRepository(this._ref); final Ref _ref; Future<Something> fetchSomething() async { final accessToken = await _ref.read(extractAccessTokenProvider); /** 省略 */ } } 64
65.
Tips:アクセストークンを通信層で利用する アプリの動作時は app の
ProviderScope.overrides で振る舞いを上書きする domain 層で保持・管理されているアクセストークンを必要なときに取り出す ProviderScope.overrides( overrides: [ extractAccessTokenProvider.overrideWith( (ref) => () async => (await ref.read(authNotifierProvider.future)) .accessToken ), ], ); 65
66.
Tips:アクセストークンを通信層で利用する 通信層のユニットテストでは、 ProviderContainer で振る舞いを上書きする ProviderContainer
createContainer({ /** 省略 */ bool setAccessToken = true, }) { final container = ProviderContainer( overrides: [ accessTokenProvider.overrideWith( (_) => () async => setAccessToken ? 'sample-access-token' : null, ), /** 省略 */ ], /** 省略 */ ); addTearDown(container.dispose); return container; } 66
67.
ArchitectingFlutterappsとの比較 67
68.
パッケージを分けることで被る不利益? 🤔 build_runner を各パッケージで実行するのが面倒 仕方なし。エイリアスやタスクランナーなどの工夫を! 🤔
同じ機能に関する実装が Explorer 上で遠く見える ファイル検索の仕方を工夫すると Explorer 上でファイルを探すことがなくなる 🤔 冗長なコードが増える可能性がある サーバサイドの概念、命名、テーブル定義、HTTP インターフェース、クライアン トアプリで取り扱う概念に差がない場合など 🤔 難しそう・うまく運用できるか不安 CI や Melos へのキャッチアップは一定必要だが、単一パッケージの構成に戻すの は簡単! 68
69.
ここまでのまとめ 69
70.
ここまでのまとめ パッケージを分けることで... ✅ 依存に関する誤った実装を仕組み上防ぐことができる ✅ 各レイヤーの責務が明確になり、ユニットテストも容易に書ける ✅
Result 相当のインターフェース定義と合わせて、レイヤーを跨ぐ例外ハンドリン グがパターン化される ✅ 「いま自分は Flutter エンジニアなのか、Dart エンジニアなのか」の意識を明確 にして開発できる ✅ クライアントアプリとサーバサイドアプリとの境界が明確になり、サーバサイド の負債をクライアントアプリに持ち込まなくなる 70
71.
リプレイスの成功のための更なる取り組み 71
72.
リプレイスの成功のための更なる取り組み ✅ ユニットテスト domain および、それより抽象的なレイヤーでテストカバレッジ
100% CI で継続的にカバレッジを計測 ✅ ドキュメンテーション public_member_api_docs を全面的に有効化して doc comment を 100% 記述 ✅ 実装の可能な限りのパターン化 マッチングアプリはそんなに難しくない! 生成 AI を活用する試み 72
73.
ユニットテスト 73
74.
ユニットテストの対象 出典:「自動テストの種類の曖昧さが少ない「テストサイズ」という分類」(ログミーTech) 74
75.
ユニットテスト matrix を組んで各パッケージのユニットテストを CI
で継続的に実行する 75
76.
ユニットテスト 業務ロジック層およびそれより抽象的な層でのユニットテストのカバレッジが 100% であることを PR
上で確認する 76
77.
ドキュメンテーションコメント 77
78.
public_member_api_docs 78
79.
ドキュメンテーションコメント /// firebase_analytics パッケージが提供する、Firebase
Analytics の各機能をラップしたクラス。 /// /// ほとんど firebase_analytics のインターフェースの再実装のような内容になっているが、 /// /// - system パッケージ以外が firebase_analytics パッケージに直接依存しないようにする /// - 分析ログの送信という性質上、利用する側に await させず、例外ハンドリングもさせない(例外が /// 起きても呼び出し側の処理を止めない)ようにする /// - 一種の腐敗防止層としての実装とする /// /// ことなどを目的としている。 class FirebaseAnalyticsClient { /// [FirebaseAnalyticsClient] を生成する。 const FirebaseAnalyticsClient(this._firebaseAnalytics); final FirebaseAnalytics _firebaseAnalytics; /// Firebase Analytics にログを送信する。 /// /// 本メソッドの返り値型は `void` で定義されており、[unawaited] で [_logEvent] を呼び /// 出すことで、もし例外が発生しても呼び出し側の処理を止めない(ただし、グローバルエラー /// ハンドラーを定義すれば、キャッチできる)ようにしている。 void logEvent({required String name, required Map<String, Object>? parameters}) => unawaited(_logEvent(name: name, parameters: parameters)); /** 省略 */ } 79
80.
doccommentを100%記述 public_member_api_docs の lint
ルールを全面的に有効化 パッケージ分割と合わせて、「パッケージを作るようにアプリケーションを作る」 意識を重視 インターフェースが完璧で、ドキュメンテーションが十分で、ユニットテストが十 分に書かれた実装に依存する安心・優れた開発体験 ソースコードが「実行可能な仕様書」としての理想に近づく 80
81.
実装の可能な限りのパターン化 81
82.
実装の可能な限りのパターン化 ✅ スニペットの充実 Omiai では合計
20 個のスニペットを定義 各レイヤーで必要になる典型的な実装はすべてスニペット化済み ✅ 堅牢な設計がパターン化を支える 例外ハンドリングもスニペット化 ユニットテストもスニペット化 doc comment もスニペットに含まれている 開発者は、インターフェース設計やあるべき業務概念を考えること、UI 層の作り込 みに集中できる スニペットでカバーできない実装が見つかれば、不確実性の高いタスクとして認識 可能 82
83.
例:Repositoryの実装 83
84.
例:Repositoryのテスト 84
85.
試み:生成AIの活用 85
86.
VSCodeのスニペット part '${TM_FILENAME_BASE}.g.dart'; class ${TM_FILENAME_BASE/(.*)_use_case$/${1:/pascalcase}/}UseCase
{ /** 省略 */ } TM_FILENAME_BASE や TM_DIRECTORY のような Variables が利用可能 86
87.
まとめ 87
88.
まとめ ✅ 大規模アプリのリプレイスの成功のための堅牢な設計 Architecting Flutter
apps や Riverpod の公式ドキュメントをベースに 更にパッケージ分割で負債を仕組みで未然に防ぐ 定めた対象に対してテストを十分に記述し、継続的にカバレッジなどを把握する ドキュメンテーションコメントを必須にして「実行可能な仕様書」の理想へ ✅ 堅牢な設計の副次的な恩恵 各レイヤーの責務や依存が明確で、それぞれの実装が単純化・パターン化される スニペットの充実は開発体験の向上と品質の維持に有効で、生成 AI の活用も期 待される 88
Download