0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

既存WebのAPIをFlutterで叩いてiOS/Androidアプリを最小コストで作った

0
Last updated at Posted at 2026-05-20

はじめに

以前、AWS Lambda + API Gateway でビジネスメール添削 API を作りました。

この API はブラウザから叩くことを前提にしていましたが、Flutter でそのまま叩けば iOS/Android アプリが作れると気づき、実際に試してみました。バックエンドは一切触らず、UI だけを新たに実装するアプローチです。

GitHub: https://github.com/kojiman55/mail-checker-app

スクリーンショット

iOS 入力画面 iOS 結果画面
iOS 入力 iOS 結果
Web 版 入力画面 Web 版 結果画面
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.darthome_screen.dart の両方から参照できます。

関連記事

まとめ

バックエンドを共有することで、追加開発は UI 実装だけに集中できました。

観点 Web 版 Flutter 版
CORS 設定 必要(苦労した) 不要
レイアウト CSS メディアクエリ LayoutBuilder
文字数制限 5,000 文字 500 文字(フロントのみ)
ノッチ対応 不要 SafeArea 必須

既存の API を持っている方には、Flutter でのモバイル版追加は非常に低コストでおすすめです。CORSがない分、Web版より接続まわりはむしろシンプルでした。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?