SlideShare a Scribd company logo
FlutterKaigi2024
マッチングアプリ『Omiai』の
Flutterへのリプレイスの挑戦
Kosuke Saigusa(株式会社 Omiai)
1
自己紹介
Kosuke Saigusa (@kosukesaigusa)
株式会社 Omiai Flutter テックリード
FlutterNinjas 2024, FlutterKaigi 2023 等で登壇
FlutterGakkai, 東京 Flutter ハッカソン運営
2
Omiaiについて
3
昨今のマッチングアプリと市場
4
エニトグループについて
5
ホールディングス経営体制
6
本セッションのゴール
7
本セッションのゴール
マッチングアプリ『Omiai』を題材に
大規模アプリのFlutterへのリプレイスを
成功に導くアプローチを考える
8
なぜ『Omiai』はFlutterにリプレイスするのか?
9
なぜ『Omiai』はFlutterにリプレイスするのか?
💬 背景
2012 年から長年運営されてきたサービス
iOS・Android (, Web) の各プラットフォームの古いコードベースが抱える多くの技
術的負債や仕様差異
サーバサイドの負債の解消・仕様変更もクライアントアプリが理由で進めづらい
✅ 目標
Flutter へのリプレイスによる単一コードベース化と技術的負債の返済
リプレイス中も機能追加は止めない
toC アプリとしてあるべき素早く・細かい開発サイクルでの開発・検証を実現する
10
どのようにリプレイスするのか?
11
どのようにリプレイスするのか?
✅ 負債化を繰り返さず、高い開発生産性を維持し続ける
アーキテクチャ、CI、テスト、ドキュメンテーションなどで多くの工夫
✅ Flutter 経験の無いメンバーや新規参画メンバーが早期に活躍できる
Flutter 経験者ゼロ・業務委託中心組織から正社員採用によるチームの拡大へ
✅ リプレイス中も機能追加は止めない
Add-to-App を利用して既存アプリから Flutter をモジュールとして呼び出す
主要なユーザー体験から順に、新しい機能を追加しながら Flutter にリプレイスす
る
12
陥りがちな現実
13
PRレビューで防ぐしかないコード規約違反
ElevatedButton(
onPressed: () async {
await someRepositoryMethod();
},
/** 省略 */
);
UI 層から直接 Repository のメソッドを呼び出すのを禁止している場合
コード規約で縛り、PR レビューで防ぐしか方法がない
見逃されたコードが生き残ると、コード規約が曖昧に
14
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
不自然な依存
import 'package:dio/dio.dart';
ElevatedButton(
onPressed: () async {
try {
await doSomething();
} on DioException catch (e) {
/** 省略 */
},
},
/** 省略 */
),
UI 層が特定の HTTP クライアントパッケージに依存するのは不自然(知るべきでな
い)
16
曖昧な例外ハンドリングの方針
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
依存を誤って負債化
class SomeRepository {
/** 省略 */
Future<Something> fetchSomething({/** 省略 */}) async {
final accessToken = _ref.read(accessTokenProvider);
final response = await _client.request(/** 省略 */);
/** 省略 */
}
}
通信層が、業務知識としての認証情報に依存する
通信層のユニットテストを書くのに、業務知識をモックする必要がある
もしこの依存が許されるなら、あらゆる業務知識に同様に依存し始める危険がある
18
依存を誤って負債化
class PaginatedFetcher<T> {
/** 省略 */
Future<List<T>> fetchNextPage(int page, int perPage) async {
/** 省略 */
final items = await fetch(/** 省略 */);
/** 省略 */
await ref.read(someUseCaseProvider).doSomething();
return items;
}
}
当初の汎用的な基盤実装が、いつの間にか意図しない対象に依存するようになる
汎用性は失われ、責務が曖昧な負債コードに
19
クライアント・サーバサイドアプリの密結合
ElevatedButton(
onPressed: () async {
try {
await doSomething();
} on APIException catch (e) {
await showDialog<void>(
context: context,
builder: (context) => AlertDialog(content: Text(e.message)),
);
},
},
/** 省略 */
),
ユーザーに知らせるエラーメッセージに、サーバサイドからのレスポンスデータが
そのまま利用される
クライアントアプリとサーバサイドアプリの境界が曖昧 20
どのように防ぐ?
21
可能な限り仕組みで防ぎたい!
22
方針
✅ マルチパッケージ構成
レイヤーごとにパッケージを分ける
各パッケージの責務と許可される依存を厳密にする
Flutter の世界と Dart の世界、クライアントアプリとサーバサイドアプリの境界を
明確にする
✅ ユニットテストを十分に継続的に書く
業務ロジック層およびそれより抽象的な層で、ユニットテストがカバレッジ 100%
で記述できる
✅ 実装を可能な限りパターン化する
典型的な実装をパターン化し、新規参画メンバーの早期の活躍を実現する
23
パッケージ構成
24
パッケージ一覧(抜粋)
レイヤーごとにパッケージを分ける
各パッケージに許される直接的な依存は矢印の先のパッケージのみに限定される
他にも Unimplemented なインターフェースのみを定義し、各パッケージに利用さ
せるパッケージなどもある
※ ことば遣いは Flutter 公式の"Architecting Flutter apps" や Android Developers の "Guide to app
architecture" に類似
25
パッケージ一覧(app)
Flutter アプリのエントリポイントとしての実装
ProviderScope.overrides による依存の注入
UI 層としてのユーザー体験を実装
ウィジェットツリーの構築、画面遷移、各画面やコンポーネントの描画とインタ
ラクション
26
パッケージ一覧(base_ui)
テキストスタイル、色などのスタイルガイドの実装
画像、ボタン、AppBar、TextField などの基礎的な UI 部品の実装
27
パッケージ一覧(domain)
クライアントアプリの業務概念を表すエンティティの定義
ユーザー体験として利用する例外型の定義
データの取得や保存などを含む業務ロジックの記述
28
パッケージ一覧(repository)
データソースとのやり取りの実装
接続先のデータソースの情報は表出させない
29
パッケージ一覧(system)
3rd パーティのツールをラップして基礎的な実装を記述
例:HTTP クライアント、Shared Preferences, Firebase Analytics など
3rd パーティのツールとの依存は例外も含めて閉じ込めて外部に表出させない
不要な機能のインターフェースは塞いでシンプルにする
30
パッケージ一覧(Flutterの世界)
app, base_ui パッケージは Flutter に直接依存する
Flutter エンジニアとしてコードを書く
31
パッケージ一覧(Dartの世界)
domain, repository, system パッケージは Flutter には直接依存しない
Dart エンジニアとしてコードを書く
Dart 言語と riverpod や freezed にさえ慣れれば Flutter 経験が浅くても書ける
32
パッケージ一覧(サーバサイドとの境界)
repository パッケージがサーバサイドとの境界として働く
サーバサイドの負債は repository パッケージで吸収し切り、app, domain に流入
させない
33
各パッケージの実装内容
34
base_ui
テキストスタイル、色などのスタイルガイドの実装
画像、ボタン、AppBar、TextField などの基礎的な UI 部品の実装
35
base_uiが依存するパッケージの例
dependencies:
extended_image:
flutter:
sdk: flutter
flutter_svg:
dev_dependencies:
flutter_gen_runner:
Flutter への依存: direct
依存パッケージの例:
extended_image: 画像の基盤実装
flutter_gen: 静的アセットのコード生成
36
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
base_uiの実装例
/// アプリ内で共通で用いられる [ElevatedButton].
class CommonElevatedButton extends StatelessWidget {
/// S サイズの [CommonElevatedButton] を生成する。
const CommonElevatedButton.s({/** 省略 */});
/// S サイズのアイコン付きの [CommonElevatedButton] を生成する。
const CommonElevatedButton.sWithIcon({/** 省略 */});
/** 省略 */
}
ボタン、画像、AppBar、TextField などの基礎的な UI 部品
Figma のコンポーネント定義と一致するインターフェース
38
system
3rd パーティのツールをラップして基礎的な実装を記述
外部パッケージへの依存を閉じ込めて、外部に表出させない
39
systemが依存するパッケージの例
dependencies:
dio:
firebase_analytics:
firebase_remote_config:
shared_preferences:
Flutter への依存: transitive
各種の 3rd パーティのツールに依存する
40
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
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
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
repository
データソースとのやり取りの実装
接続先のデータソースの情報は表出させない
44
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
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
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
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
domain
クライアントアプリの業務概念を表すエンティティの定義
ユーザー体験として利用する例外型の定義
データの取得や保存などを含む業務ロジックの記述
49
domainが依存するパッケージ
dependencies:
freezed_annotation:
repository:
path: ../repository
riverpod:
riverpod_annotation:
dev_dependencies:
freezed:
riverpod_generator:
Flutter への依存: transitive
repository パッケージに依存してデータにアクセスする
freezed で業務概念のエンティティを定義する
riverpod で状態管理ロジックを実装する
50
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
domainの実装例
enum LogInStatus {
active,
recent,
inactive,
;
factory LogInStatus.fromDateTime(DateTime lastAccessedAt) {/** 省略 */}
}
お相手メンバーのログインステータス(直近どのくらいアクティブか)の定義
最終ログイン日時というサーバサイド (DB) 上の事実から、ログインステータスと
いうクライアントアプリにおける業務概念を計算する
52
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
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
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
app
Flutter アプリのエントリポイントとしての実装
ProviderScope.overrides による依存の注入
UI 層としてのユーザー体験を実装
ウィジェットツリーの構築、画面遷移、各画面やコンポーネントの描画、ユーザ
ー操作とのインタラクション
56
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
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
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
appの実装例
CommonFilledButton.sWithIcon(
onPressed: () async {
try {
await _ref.read(sendLikeUseCaseProvider).invoke(/** 省略 */);
} on InsufficientPointsToSendLikeException catch (e) {
await _showErrorDialog(/** 省略 */);
}
/** 省略 */
}
/** 省略 */
)
UI を通じたインタラクションをトリガーに domain 層のロジックを呼び出す
domain 層に定義された例外を実現したいユーザー体験に従って処理する
60
パッケージ構成
repository - system 間
dio パッケージを通じて HTTP リクエストを行う
dio パッケージへの依存は例外も含めて repository に表出しない
Result 型のような HttpResponse 型をインターフェースとすることで、repository
層での switch による成功・失敗のハンドリングが強制、画一化される
61
パッケージ構成
domain - repository 間
repository のメソッドを呼び出してデータを読み書きする
Result 型のような RepositoryResult 型をインターフェースとすることで、
domain 層での switch による成功・失敗のハンドリングが強制、画一化される
domain 層では Dto から業務知識を表すエンティティを生成し、サーバサイドの負
債は持ち込まない
62
パッケージ構成
app - domain 間
domain は業務概念を表すデータと業務ロジックを提供する
app はユーザー体験の実現のために domain のロジックを利用し、業務概念のデー
タを読み書きする
domain は業務知識として取り扱うべき例外をスローする
app はそれを捕捉してユーザー体験に反映する
63
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
Tips:アクセストークンを通信層で利用する
アプリの動作時は app の ProviderScope.overrides で振る舞いを上書きする
domain 層で保持・管理されているアクセストークンを必要なときに取り出す
ProviderScope.overrides(
overrides: [
extractAccessTokenProvider.overrideWith(
(ref) => () async => (await ref.read(authNotifierProvider.future)) .accessToken
),
],
);
65
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
ArchitectingFlutterappsとの比較
67
パッケージを分けることで被る不利益?
🤔 build_runner を各パッケージで実行するのが面倒
仕方なし。エイリアスやタスクランナーなどの工夫を!
🤔 同じ機能に関する実装が Explorer 上で遠く見える
ファイル検索の仕方を工夫すると Explorer 上でファイルを探すことがなくなる
🤔 冗長なコードが増える可能性がある
サーバサイドの概念、命名、テーブル定義、HTTP インターフェース、クライアン
トアプリで取り扱う概念に差がない場合など
🤔 難しそう・うまく運用できるか不安
CI や Melos へのキャッチアップは一定必要だが、単一パッケージの構成に戻すの
は簡単! 68
ここまでのまとめ
69
ここまでのまとめ
パッケージを分けることで...
✅ 依存に関する誤った実装を仕組み上防ぐことができる
✅ 各レイヤーの責務が明確になり、ユニットテストも容易に書ける
✅ Result 相当のインターフェース定義と合わせて、レイヤーを跨ぐ例外ハンドリン
グがパターン化される
✅ 「いま自分は Flutter エンジニアなのか、Dart エンジニアなのか」の意識を明確
にして開発できる
✅ クライアントアプリとサーバサイドアプリとの境界が明確になり、サーバサイド
の負債をクライアントアプリに持ち込まなくなる
70
リプレイスの成功のための更なる取り組み
71
リプレイスの成功のための更なる取り組み
✅ ユニットテスト
domain および、それより抽象的なレイヤーでテストカバレッジ 100%
CI で継続的にカバレッジを計測
✅ ドキュメンテーション
public_member_api_docs を全面的に有効化して doc comment を 100% 記述
✅ 実装の可能な限りのパターン化
マッチングアプリはそんなに難しくない!
生成 AI を活用する試み
72
ユニットテスト
73
ユニットテストの対象
出典:「自動テストの種類の曖昧さが少ない「テストサイズ」という分類」(ログミーTech)
74
ユニットテスト
matrix を組んで各パッケージのユニットテストを CI で継続的に実行する
75
ユニットテスト
業務ロジック層およびそれより抽象的な層でのユニットテストのカバレッジが
100% であることを PR 上で確認する
76
ドキュメンテーションコメント
77
public_member_api_docs
78
ドキュメンテーションコメント
/// 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
doccommentを100%記述
public_member_api_docs の lint ルールを全面的に有効化
パッケージ分割と合わせて、「パッケージを作るようにアプリケーションを作る」
意識を重視
インターフェースが完璧で、ドキュメンテーションが十分で、ユニットテストが十
分に書かれた実装に依存する安心・優れた開発体験
ソースコードが「実行可能な仕様書」としての理想に近づく
80
実装の可能な限りのパターン化
81
実装の可能な限りのパターン化
✅ スニペットの充実
Omiai では合計 20 個のスニペットを定義
各レイヤーで必要になる典型的な実装はすべてスニペット化済み
✅ 堅牢な設計がパターン化を支える
例外ハンドリングもスニペット化
ユニットテストもスニペット化
doc comment もスニペットに含まれている
開発者は、インターフェース設計やあるべき業務概念を考えること、UI 層の作り込
みに集中できる
スニペットでカバーできない実装が見つかれば、不確実性の高いタスクとして認識
可能 82
例:Repositoryの実装
83
例:Repositoryのテスト
84
試み:生成AIの活用
85
VSCodeのスニペット
part '${TM_FILENAME_BASE}.g.dart';
class ${TM_FILENAME_BASE/(.*)_use_case$/${1:/pascalcase}/}UseCase {
/** 省略 */
}
TM_FILENAME_BASE や TM_DIRECTORY のような Variables が利用可能
86
まとめ
87
まとめ
✅ 大規模アプリのリプレイスの成功のための堅牢な設計
Architecting Flutter apps や Riverpod の公式ドキュメントをベースに
更にパッケージ分割で負債を仕組みで未然に防ぐ
定めた対象に対してテストを十分に記述し、継続的にカバレッジなどを把握する
ドキュメンテーションコメントを必須にして「実行可能な仕様書」の理想へ
✅ 堅牢な設計の副次的な恩恵
各レイヤーの責務や依存が明確で、それぞれの実装が単純化・パターン化される
スニペットの充実は開発体験の向上と品質の維持に有効で、生成 AI の活用も期
待される
88

More Related Content

PDF
Twitterのリアルタイム分散処理システム「Storm」入門
PDF
Open vSwitchソースコードの全体像
PDF
コンテナを突き破れ!! ~コンテナセキュリティ入門基礎の基礎~(Kubernetes Novice Tokyo #11 発表資料)
PDF
Application Re-Architecture Technology ~ StrutsからSpring MVCへ ~
PPTX
Wiresharkの解析プラグインを作る ssmjp 201409
PDF
今からはじめる! Linuxコマンド入門
PDF
Cassandra導入事例と現場視点での苦労したポイント cassandra summit2014jpn
PDF
YJTC19 B-1 パスワードレス普及への取り組み/ヤフーのデータ戦略を支えるID連携 #yjtc
Twitterのリアルタイム分散処理システム「Storm」入門
Open vSwitchソースコードの全体像
コンテナを突き破れ!! ~コンテナセキュリティ入門基礎の基礎~(Kubernetes Novice Tokyo #11 発表資料)
Application Re-Architecture Technology ~ StrutsからSpring MVCへ ~
Wiresharkの解析プラグインを作る ssmjp 201409
今からはじめる! Linuxコマンド入門
Cassandra導入事例と現場視点での苦労したポイント cassandra summit2014jpn
YJTC19 B-1 パスワードレス普及への取り組み/ヤフーのデータ戦略を支えるID連携 #yjtc

What's hot (9)

DOC
Le pendule de Foucault, une invitation à « venir voir tourner la Terre »
PPTX
BOMB DETECTION ROBOT BY USING GSM & GPS
PDF
SRv6 Mobile User Plane : Initial POC and Implementation
PDF
IoT Based Solar Water Pump Controller
PPT
RFID Basics
PDF
Real Time Hand Gesture Recognition Based Control of Arduino Robot
PPTX
Rfid based attendance system
PPT
Chapter 7 review jeopardy nomenclature only
PDF
FPGAで作るOpenFlow Switch (FPGAエクストリーム・コンピューティング 第6回) FPGAX#6
Le pendule de Foucault, une invitation à « venir voir tourner la Terre »
BOMB DETECTION ROBOT BY USING GSM & GPS
SRv6 Mobile User Plane : Initial POC and Implementation
IoT Based Solar Water Pump Controller
RFID Basics
Real Time Hand Gesture Recognition Based Control of Arduino Robot
Rfid based attendance system
Chapter 7 review jeopardy nomenclature only
FPGAで作るOpenFlow Switch (FPGAエクストリーム・コンピューティング 第6回) FPGAX#6
Ad

Similar to マッチングアプリ『Omiai』の Flutter へのリプレイスの挑戦 (FlutterKaigi 2024) (20)

PPTX
ひと漕ぎで二度おいしい!? Flutterを使ったモバイルアプリ開発への期待と実態と付き合い方(NTTデータ テクノロジーカンファレンス 2020 発表資料)
PDF
【2018/09/11】PAYでのReact Nativeにおける APIクライアント実装 について
PDF
Flutter_Forward_Extended_Kyoto-Keynote_Summary
PPTX
Flutter (フラッター)
PDF
Flutterアプリ開発におけるモジュール分割戦略
PDF
appengine ja night #24 Google Cloud Endpoints and BigQuery
KEY
Inside frogc in Dart
PDF
Flutterやってみよう
PDF
Flutterで単体テストを行う方法とGitHub Actionsを使った自動化
PDF
iPhoneとAndroidのアプリ開発最新潮流
PDF
Flutter のリアクティブ戦略 set state 〜 redux まで
PDF
オープンソースエコシステム #demodaytokyo
PDF
PDF
関西FirefoxOS勉強会6thGiG「アプリ間通信」
PDF
The Essence of Using Ruby on Rails in Corporations
PDF
超高速でflutterアプリをビルドする
PDF
Google I/O 2021 Flutter 全体報告
PDF
Flutterを体験してみませんか
KEY
スマートフォンアプリケーション開発の最新動向
PDF
Building a Flutter Development Environment with VSCode and Useful Extensions
ひと漕ぎで二度おいしい!? Flutterを使ったモバイルアプリ開発への期待と実態と付き合い方(NTTデータ テクノロジーカンファレンス 2020 発表資料)
【2018/09/11】PAYでのReact Nativeにおける APIクライアント実装 について
Flutter_Forward_Extended_Kyoto-Keynote_Summary
Flutter (フラッター)
Flutterアプリ開発におけるモジュール分割戦略
appengine ja night #24 Google Cloud Endpoints and BigQuery
Inside frogc in Dart
Flutterやってみよう
Flutterで単体テストを行う方法とGitHub Actionsを使った自動化
iPhoneとAndroidのアプリ開発最新潮流
Flutter のリアクティブ戦略 set state 〜 redux まで
オープンソースエコシステム #demodaytokyo
関西FirefoxOS勉強会6thGiG「アプリ間通信」
The Essence of Using Ruby on Rails in Corporations
超高速でflutterアプリをビルドする
Google I/O 2021 Flutter 全体報告
Flutterを体験してみませんか
スマートフォンアプリケーション開発の最新動向
Building a Flutter Development Environment with VSCode and Useful Extensions
Ad

マッチングアプリ『Omiai』の Flutter へのリプレイスの挑戦 (FlutterKaigi 2024)