Skip to content

API利用法

FlutterとRailsのAPI連携について、両方の観点から実践的に解説します。

FlutterとRailsは、それぞれ独立した役割を担います。

  • 🚂 Rails(バックエンド): APIサーバーとして機能します。データベースとのやり取り、ビジネスロジックの実行、そしてJSON形式でのデータ提供を担当します。
  • 📱 Flutter(フロントエンド): UI(ユーザーインターフェース)を担当します。ユーザーの操作に応じてRailsのAPIにHTTPリクエストを送信し、返ってきたJSONデータを画面に表示します。

この構成では、両者はRESTful APIを介して通信するのが一般的です。

まず、RailsでAPIエンドポイントを準備します。Railsは--apiオプションを使ってAPI専用プロジェクトを簡単に作成できます。

Terminal window
rails new your_project_name --api

次に、データを扱うためのリソース(例:posts)を生成します。

Terminal window
rails g resource post title:string body:string

これにより、app/models/post.rbapp/controllers/posts_controller.rb、ルーティング設定が自動で生成されます。

posts_controller.rbで、index(一覧取得)やshow(個別取得)などのアクションを定義します。

app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
@posts = Post.all
render json: @posts # JSON形式でデータを返却
end
def show
@post = Post.find(params[:id])
render json: @post
end
end

rails sコマンドでサーバーを起動すると、http://localhost:3000/postsにアクセスすることで、JSONデータが返されるようになります。

FlutterアプリからRails APIにアクセスするためには、httpパッケージを使用します。特に、非同期処理を扱うFutureasync/await構文が重要です。

http.getメソッドを使って、Railsからデータを取得します。

import 'dart:convert';
import 'package:http/http.dart' as http;
Future<List<Post>> fetchPosts() async {
// 開発中のローカル環境では、PCのIPアドレスを指定
final response = await http.get(Uri.parse('http://10.0.2.2:3000/posts'));
if (response.statusCode == 200) {
// JSON文字列をデコードし、List<Map>に変換
final List<dynamic> jsonList = json.decode(response.body);
// Mapをカスタムモデルのリストに変換
return jsonList.map((json) => Post.fromJson(json)).toList();
} else {
throw Exception('Failed to load posts');
}
}
// レスポンスのJSONを扱うためのモデルクラス
class Post {
final int id;
final String title;
final String body;
Post({required this.id, required this.title, required this.body});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id'],
title: json['title'],
body: json['body'],
);
}
}

ポイント:

  • IPアドレス: Androidエミュレータは、localhostではなく10.0.2.2でホストマシンにアクセスします。iOSシミュレータの場合はlocalhostで問題ありません。
  • モデルクラス: JSONデータを直接扱うのではなく、モデルクラスに変換することで、コードの安全性が高まります。

http.postメソッドを使って、新しいデータをRailsに送信します。

Future<Post> createPost(String title, String body) async {
final response = await http.post(
Uri.parse('http://10.0.2.2:3000/posts'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{
'title': title,
'body': body,
}),
);
if (response.statusCode == 201) {
// 成功(201 Created)
return Post.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to create post');
}
}

ポイント:

  • headers: JSONデータを送ることをRailsに伝えるため、Content-Typeを設定します。
  • body: DartのMapjsonEncodeでJSON文字列に変換してから送信します。

4. 実際のFlutterアプリでの表示 🖼️

Section titled “4. 実際のFlutterアプリでの表示 🖼️”

上記で作成した関数を、FutureBuilderウィジェットと組み合わせて使用することで、非同期処理のローディング状態やエラーを簡単にUIに反映できます。

class PostListPage extends StatefulWidget {
const PostListPage({super.key});
@override
State<PostListPage> createState() => _PostListPageState();
}
class _PostListPageState extends State<PostListPage> {
late Future<List<Post>> futurePosts;
@override
void initState() {
super.initState();
futurePosts = fetchPosts();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Rails × Flutter')),
body: FutureBuilder<List<Post>>(
future: futurePosts,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('エラー: ${snapshot.error}'));
} else if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
final post = snapshot.data![index];
return ListTile(title: Text(post.title));
},
);
} else {
return const Center(child: Text('データがありません'));
}
},
),
);
}
}

この例は、FlutterがRailsのAPIから取得したデータを動的に表示する、一般的なパターンを示しています。

API疎通におけるCORSの扱い 🛡️

Section titled “API疎通におけるCORSの扱い 🛡️”

CORSは、異なるオリジン(ドメイン、プロトコル、ポートの組み合わせ)間でリソースを共有するための仕組みです。ブラウザはセキュリティ上の理由から、CORSのルールに従ってリクエストをブロックします。

結論から言うと、通常のFlutter開発において、CORSの設定は基本的に不要です。

その理由は、CORSがWebブラウザのセキュリティ機能だからです。モバイルアプリ(iOS/Android)はブラウザのセキュリティモデルに縛られないため、異なるオリジンへのAPIリクエストを自由に送信できます。

ただし、以下のプラットフォームではCORS設定が必要になる場合があります。

  • Web: FlutterアプリをWebで動かす場合、Webブラウザ上で動作するため、CORSの制約を受けます。この場合、Rails側にCORS設定を追加する必要があります。
  • macOS / Windows / Linux: デスクトップアプリとして動作する場合、ブラウザの制約を受けないため、CORS設定は不要です。

RailsでCORSを有効にするには、rack-cors gemを使用するのが一般的です。

Gemfileに以下の行を追加し、bundle installを実行します。

gem 'rack-cors'

config/initializers/cors.rbファイルで設定を行います。

Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
# Flutter Webアプリのオリジンを指定
origins 'http://localhost:XXXX' # 例: localhost:5500など
# 複数指定する場合は 'http://localhost:3000', 'https://example.com' のようにカンマ区切りで記述
# 許可するHTTPメソッド
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end

originsには、Flutter Webアプリが動作するオリジン(ドメインとポート)を指定します。ワイルドカード*を使うことも可能ですが、セキュリティ上の理由から、本番環境では特定のドメインを指定することが推奨されます。

1. 開発環境でのIPアドレスの注意点

Section titled “1. 開発環境でのIPアドレスの注意点”

前回の回答で触れたように、Androidエミュレータは10.0.2.2を使用しますが、これは開発環境特有の注意点です。

  • iOSシミュレータ: localhostまたは127.0.0.1でホストマシンにアクセスできます。
  • 実機テスト: PCと同じWi-Fiネットワークに接続し、PCのローカルIPアドレス(例: 192.168.1.5)を使用する必要があります。

本番環境では、APIキーや認証トークンを使って通信を保護することが不可欠です。

  • Rails側: posts_controller.rbで、before_action :authenticate_user!などを設定し、認証されていないリクエストを拒否します。
  • Flutter側: httpリクエストのヘッダーに認証トークンを含めて送信します。
// ヘッダーに認証トークンを追加
final token = 'YOUR_AUTH_TOKEN';
final response = await http.get(
Uri.parse('http://your-api-url.com/posts'),
headers: {
'Authorization': 'Bearer $token',
},
);

これらのポイントを踏まえることで、FlutterとRailsの連携をより安全かつスムーズに進めることができます。

APIクライアントの抽象化と管理 🧩

Section titled “APIクライアントの抽象化と管理 🧩”

問題点: APIリクエストをウィジェットの内部に直接記述すると、コードが複雑になり、テストやメンテナンスが困難になります。特に、複数のAPIエンドポイントやリクエストヘッダーの共通設定がある場合、コードの重複が発生しやすくなります。

解決策: APIリクエストを専門に扱うAPIクライアントクラスを作成し、抽象化します。

lib/services/api_service.dart
import 'package:http/http.dart' as http;
class ApiService {
final String _baseUrl = 'http://10.0.2.2:3000';
final Map<String, String> _headers = {
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer YOUR_AUTH_TOKEN', // 認証トークン
};
Future<http.Response> getPosts() {
final uri = Uri.parse('$_baseUrl/posts');
return http.get(uri, headers: _headers);
}
Future<http.Response> createPost(Map<String, dynamic> data) {
final uri = Uri.parse('$_baseUrl/posts');
return http.post(uri, headers: _headers, body: jsonEncode(data));
}
}

実践的な使い方: ApiServiceのようなクラスをシングルトンパターンや依存性注入(例: Riverpod)で管理し、アプリケーション全体で再利用します。これにより、APIのエンドポイントやヘッダーの変更が必要になった場合でも、このクラスを1箇所修正するだけで済みます。

エラーハンドリングの強化 ⚠️

Section titled “エラーハンドリングの強化 ⚠️”

問題点: 前回のコード例では、ステータスコードが200または201でない場合に一律でExceptionを投げていました。しかし、APIから返されるエラーには、入力値の検証エラー(422 Unprocessable Entity)や認証エラー(401 Unauthorized)など、様々な種類があります。

解決策: ステータスコードに応じて、より具体的なエラー処理を行います。

Future<List<Post>> fetchPosts() async {
final response = await http.get(Uri.parse('$_baseUrl/posts'));
if (response.statusCode == 200) {
// 成功
// ...
} else if (response.statusCode == 401) {
// 認証エラー
throw Exception('認証に失敗しました。再度ログインしてください。');
} else if (response.statusCode >= 400 && response.statusCode < 500) {
// クライアントエラー
throw Exception('不正なリクエストです。');
} else {
// その他のエラー
throw Exception('サーバーエラーが発生しました。');
}
}

実践的な使い方: APIレスポンスのステータスコードを詳細にチェックし、ユーザーに分かりやすいエラーメッセージを表示したり、ログを記録したりします。これにより、ユーザー体験が向上し、デバッグも容易になります。

より高度なAPIクライアント: Dio パッケージ 🌐

Section titled “より高度なAPIクライアント: Dio パッケージ 🌐”

問題点: httpパッケージはシンプルで使いやすいですが、インターセプター、リクエストのキャンセル、グローバルな設定など、高度な機能がありません。

解決策: より高機能なHTTPクライアントライブラリであるDioを使用することを検討します。

// Dioのインストール
// flutter pub add dio
import 'package:dio/dio.dart';
final dio = Dio();
Future<void> fetchDataWithDio() async {
try {
// ベースURLやヘッダーをグローバルに設定
dio.options.baseUrl = 'http://10.0.2.2:3000';
dio.options.headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_AUTH_TOKEN',
};
final response = await dio.get('/posts');
if (response.statusCode == 200) {
print('Data: ${response.data}');
}
} on DioException catch (e) {
// ネットワークエラーやAPIエラーを詳細にハンドリング
print('エラー: ${e.response?.statusCode}');
print('エラーメッセージ: ${e.response?.data}');
}
}

実践的な使い方:

  • インターセプター: すべてのリクエストに認証トークンを自動で追加するなどの共通処理を実装できます。
  • エラーハンドリング: DioExceptionクラスを使って、ネットワークエラー、タイムアウト、ステータスコードごとのエラーをより細かく捕捉できます。

これらの留意点を考慮することで、より堅牢で保守性の高いFlutterアプリケーションを開発できます。

データシリアライゼーションの自動化 📦

Section titled “データシリアライゼーションの自動化 📦”

APIから受け取ったJSONデータをモデルクラスに変換する作業(シリアライゼーション)は、手動で行うと非常に手間がかかり、ミスも起こりやすいです。特に、JSONの構造が複雑だったり、APIの仕様変更があったりする場合、手動での対応は現実的ではありません。

解決策: json_serializablebuild_runnerというパッケージを使って、シリアライゼーションのコードを自動生成します。

pubspec.yamlに以下のパッケージを追加します。

dependencies:
json_annotation: ^4.8.1
dev_dependencies:
build_runner: ^2.4.6
json_serializable: ^6.7.1

自動生成を有効にするために、モデルクラスを@JsonSerializable()アノテーションで装飾し、factoryコンストラクタとtoJsonメソッドを追加します。

import 'package:json_annotation/json_annotation.dart';
part 'post.g.dart';
@JsonSerializable()
class Post {
final int id;
final String title;
final String body;
Post({required this.id, required this.title, required this.body});
factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);
Map<String, dynamic> toJson() => _$PostToJson(this);
}

ターミナルで以下のコマンドを実行します。

Terminal window
flutter pub run build_runner build

これにより、post.g.dartというファイルが生成され、_$PostFromJson_$PostToJsonというシリアライゼーションのロジックが自動的に作成されます。

実践的な使い方: APIの仕様が変更された場合でも、モデルクラスを修正してコマンドを再実行するだけで、シリアライゼーションのコードが最新の状態に保たれます。これにより、開発効率が大幅に向上し、ヒューマンエラーを減らせます。

APIリクエストは非同期処理であり、その状態(ローディング中、成功、エラー)をUIに適切に反映する必要があります。FutureBuilderは単一のウィジェットで非同期処理を扱うには便利ですが、複数のウィジェットで同じAPIデータを共有したり、状態をより複雑に管理したりする場合には不十分です。

解決策: Riverpodのような状態管理ライブラリとAPIクライアントを組み合わせることで、API連携のロジックをUIから完全に分離できます。

APIクライアントとプロバイダの連携

Section titled “APIクライアントとプロバイダの連携”

先ほど作成したApiServiceRiverpodProviderで管理します。

lib/providers/providers.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../services/api_service.dart';
import '../models/post.dart';
// ApiServiceのインスタンスをシングルトンとして提供
final apiServiceProvider = Provider<ApiService>((ref) => ApiService());
// APIから投稿一覧を取得するFutureProvider
final postsProvider = FutureProvider<List<Post>>((ref) async {
final apiService = ref.watch(apiServiceProvider);
final response = await apiService.getPosts();
if (response.statusCode == 200) {
final List<dynamic> jsonList = json.decode(response.body);
return jsonList.map((json) => Post.fromJson(json)).toList();
}
throw Exception('投稿の取得に失敗しました');
});

実践的な使い方:

  • UIからAPIロジックを分離: postsProviderを定義することで、ウィジェットは直接APIリクエストを呼び出す必要がなくなります。
  • 状態の自動管理: FutureProviderが自動的にローディング、データ、エラーの状態を管理してくれるため、FutureBuilderを繰り返し書く手間が省けます。
  • UIでの利用: UI側ではref.watch(postsProvider)を呼び出すだけで、APIの状態に応じてUIを切り替えられます。

これらの高度なテクニックを導入することで、大規模で複雑なアプリケーションでも、API連携を効率的かつ堅牢に実装できます。

パフォーマンス最適化とスケーラビリティ 🚀

Section titled “パフォーマンス最適化とスケーラビリティ 🚀”

頻繁にアクセスするが、あまり更新されないデータ(例: アプリの起動時に取得する設定情報など)がある場合、毎回APIを叩くのは非効率です。

解決策: 取得したデータをローカルにキャッシュ(保存)することで、APIリクエストの回数を減らし、アプリのパフォーマンスを向上させます。

  • 簡易的なキャッシュ: shared_preferencesパッケージを使って、キーと値のペアでデータを保存できます。
  • 複雑なデータのキャッシュ: データベースライブラリ(例: sqfliteHive)を使って、より構造化されたデータを保存・管理できます。

Hiveを用いた例: Hiveは、NoSQLの軽量なデータベースで、手軽にデータを保存できます。

// Hiveの初期化
await Hive.initFlutter();
await Hive.openBox('postBox');
// データの保存
final box = Hive.box('postBox');
box.put('posts', posts.map((post) => post.toJson()).toList());
// データの読み込み
final cachedPosts = box.get('posts');

バックグラウンドでのデータ処理

Section titled “バックグラウンドでのデータ処理”

大量のJSONデータを解析するような重い処理は、メインスレッド(UIスレッド)で実行すると、アプリの動作が一時的に固まる(UIジャック)原因になります。

解決策: compute関数を使って、重い処理をバックグラウンドのスレッド(アイソレート)で実行します。これにより、UIの応答性を維持できます。

import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
// バックグラウンドで実行する関数
List<Post> parsePosts(String responseBody) {
final parsed = jsonDecode(responseBody) as List<dynamic>;
return parsed.map((json) => Post.fromJson(json)).toList();
}
Future<List<Post>> fetchPosts() async {
final response = await http.get(Uri.parse('...'));
if (response.statusCode == 200) {
// compute関数でバックグラウンドに処理をオフロード
return compute(parsePosts, response.body);
} else {
throw Exception('Failed to load posts');
}
}

APIキーや認証情報など、機密性の高い情報はソースコードに直接書き込むべきではありません。GitHubなどの公開リポジトリにアップロードしてしまうと、情報が漏洩するリスクがあります。

解決策: flutter_dotenv--dart-defineフラグなどを使って、環境変数として管理します。

  • flutter_dotenv: .envファイルにAPIキーを記述し、.gitignoreに追加することで、バージョン管理から除外できます。
  • —dart-define: ビルド時にコマンドラインから変数を渡す方法で、特にCI/CD環境での利用に適しています。
Terminal window
// ビルドコマンドの例
flutter run --dart-define=API_KEY=your_api_key_here

const apiKey = String.fromEnvironment('API_KEY');のようにコードからアクセスできます。

これらの技術は、単にAPIを疎通させるだけでなく、より本格的なアプリケーション開発において、パフォーマンス、セキュリティ、そして保守性を向上させるために不可欠です。