はじめに
以前、AWS Lambda + API Gateway でビジネスメール添削 API を作りました。
この API はブラウザから叩くことを前提にしていましたが、Flutter でそのまま叩けば iOS/Android アプリが作れると気づき、実際に試してみました。バックエンドは一切触らず、UI だけを新たに実装するアプローチです。
GitHub: https://github.com/kojiman55/mail-checker-app
スクリーンショット
| iOS 入力画面 | iOS 結果画面 |
|---|---|
![]() |
![]() |
| Web 版 入力画面 | Web 版 結果画面 |
|---|---|
![]() |
![]() |
アプリの主な機能
- メール本文を入力して AI 添削
- 指摘を「誤り / 改善提案 / 参考情報」の 3 分類で表示
- 各指摘に元の表現・修正案・理由を表示
- AI 総評の生成
- 添削後の全文コピー機能
- 日本語・英語対応
技術スタック
| 項目 | 内容 |
|---|---|
| フレームワーク | Flutter |
| 言語 | Dart |
| HTTP 通信 |
http パッケージ |
| バックエンド | API Gateway + Lambda(Web 版と共通) |
バックエンドには一切手を加えていません。
Web 版との違い・実装で学んだこと
CORS 設定が不要
Web 版の開発では API Gateway の CORS 設定で多くの時間を使いました。オリジンの許可・プリフライトリクエストへの対応・Lambda のレスポンスヘッダーの設定など、細かいところで何度もつまずきました。
Flutter からの API 呼び出しでは、CORS は一切関係ありません。
CORS はブラウザの仕様であり、ネイティブアプリには存在しない概念です。Flutter の http パッケージはそのまま HTTP リクエストを投げるだけで、オリジンの検証は何もありません。
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<ReviewResult> review(String text, String language) async {
final res = await http.post(
Uri.parse('$_apiBase/review'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'text': text, 'language': language}),
);
if (res.statusCode != 200) throw Exception('API error: ${res.statusCode}');
return ReviewResult.fromJson(jsonDecode(res.body));
}
Web 版で費やした CORS 設定が Flutter では完全に不要です。API Gateway の CORS 設定は Web のためにあり、Flutter には無関係と割り切れます。
SafeArea 対応:iOS のノッチ・Dynamic Island
ヘッダーを実装して実機で確認したところ、iOS でタイトル文字がノッチ(Dynamic Island)のエリアに重なっていました。
SafeArea ウィジェットで囲むだけで解消します。下部は bottom: false を指定してフッターが不必要に押し上げられないようにしています。
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
bottom: false,
child: Column(
children: [
_buildHeader(),
Expanded(child: _buildContent()),
],
),
),
);
}
Web の感覚でいると存在を忘れがちなので注意が必要です。
レスポンシブレイアウト:スマートフォン / タブレット対応
スマートフォンは縦 1 カラム、タブレット・iPad は横 2 カラムにするため LayoutBuilder で画面幅を判定しました。
LayoutBuilder(builder: (context, constraints) {
final isWide = constraints.maxWidth >= 768;
if (isWide) {
// タブレット・iPad: 左右に並べる
return Row(
children: [
Expanded(child: _buildInputPanel()),
Expanded(child: _buildResultPanel()),
],
);
}
// スマートフォン: 入力と結果を切り替え表示
return _response != null
? _buildResultPanel()
: _buildInputPanel();
})
モバイルの文字数制限は Web より短くした
Web 版のバックエンドは 5,000 文字まで受け付けています。スマートフォンでそのまま 5,000 文字入力させると使い勝手が悪いため、フロントエンドのみで制限を 500 文字に絞りました。
static const _maxLength = 500;
TextField(
maxLength: _maxLength,
maxLines: 8,
decoration: const InputDecoration(
hintText: 'メール本文を入力(500文字以内)',
),
)
バックエンドには何も変更を加えず、フロントエンドだけで制御しています。
ファイル構成
500 行を超えたタイミングで責務ごとに分割しました。
lib/
main.dart # エントリーポイント・アプリ設定
home_screen.dart # メイン画面 UI
api.dart # API クライアント・サンプルテキスト
models.dart # データモデル(ReviewResult, ReviewIssue)
models.dart に型定義を集約することで、api.dart と home_screen.dart の両方から参照できます。
関連記事
まとめ
バックエンドを共有することで、追加開発は UI 実装だけに集中できました。
| 観点 | Web 版 | Flutter 版 |
|---|---|---|
| CORS 設定 | 必要(苦労した) | 不要 |
| レイアウト | CSS メディアクエリ | LayoutBuilder |
| 文字数制限 | 5,000 文字 | 500 文字(フロントのみ) |
| ノッチ対応 | 不要 |
SafeArea 必須 |
既存の API を持っている方には、Flutter でのモバイル版追加は非常に低コストでおすすめです。CORSがない分、Web版より接続まわりはむしろシンプルでした。



