FlutterでパブリックIPを取得する — APIキー不要、レート制限なし
FlutterアプリでユーザーのパブリックIPアドレスを取得する必要がある場合、よくある経験があると思います:無料サービスを選ぶ、APIキーを追加する、レート制限に引っかかる、別のサービスを探す、また繰り返す。
この記事では、IPPubblico.org を使ったよりシンプルなアプローチを紹介します。APIキー不要の完全無料API、HTTPS対応、CORS有効、合理的な使用では制限なし。シンプルな1行から国検出付きのジオロケーションまで、4つのユースケースを実際のコードで解説します。
なぜIPPubblico?
コードに入る前に、試す価値がある理由をまとめます:
- APIキー不要 — ゼロセットアップ、即座に動作
- HTTPSのみ — mixed contentの問題なし
- CORS有効 — Webでも動作
- プレーンテキストエンドポイント — シンプルなケースではJSONパース不要
- 完全なJSONエンドポイント — 都市、地域、国、ISP、ASN、タイムゾーン取得可能
- 43言語対応 —
?lang=jaで日本語レスポンス - 合理的な使用では無料 — 隠れた制限なし
セットアップ
pubspec.yamlにhttpを追加:
dependencies:
flutter:
sdk: flutter
http: ^1.2.0
AndroidManifest.xmlにインターネット権限を追加:
<uses-permission android:name="android.permission.INTERNET" />
ユースケース1 — IPアドレスのみ(最もシンプル)
生のIPv4アドレスだけが必要な場合は、プレーンテキストエンドポイントを使用します:
import 'package:http/http.dart' as http;
Future<String?> getPublicIP() async {
try {
final response = await http
.get(Uri.parse('https://ipv4.ippubblico.org/'))
.timeout(const Duration(seconds: 5));
if (response.statusCode == 200) {
return response.body.trim();
}
} catch (e) {
debugPrint('IP取得失敗: $e');
}
return null;
}
使用例:
final ip = await getPublicIP();
print(ip); // 203.0.113.42
これだけです。パース不要、モデルクラス不要、認証不要。エンドポイントはIPアドレスの1行だけを返します。
ユースケース2 — IPv4とIPv6の両方
両方のプロトコルを検出する必要がある場合は、メインドメインの?text=1エンドポイントを使用します:
Future<Map<String, String?>> getBothIPs() async {
try {
final response = await http
.get(Uri.parse('https://ippubblico.org/?text=1'))
.timeout(const Duration(seconds: 5));
if (response.statusCode == 200) {
final lines = response.body.trim().split('\n');
String? ipv4;
String? ipv6;
for (final line in lines) {
if (line.startsWith('IPv4: ')) {
final value = line.substring(6).trim();
if (value != 'NONE') ipv4 = value;
} else if (line.startsWith('IPv6: ')) {
final value = line.substring(6).trim();
if (value != 'NONE') ipv6 = value;
}
}
return {'ipv4': ipv4, 'ipv6': ipv6};
}
} catch (e) {
debugPrint('IP取得失敗: $e');
}
return {'ipv4': null, 'ipv6': null};
}
使用例:
final ips = await getBothIPs();
print('IPv4: ${ips['ipv4']}'); // IPv4: 203.0.113.42
print('IPv6: ${ips['ipv6']}'); // IPv6: 2001:db8::1 または null
ユースケース3 — 完全なジオロケーションデータ
国、都市、ISP、タイムゾーンが必要な場合はJSONエンドポイントを使用します。?lang=jaを追加すると日本語でレスポンスが返ってきます:
import 'dart:convert';
import 'package:http/http.dart' as http;
class IPInfo {
final String ip;
final String? country;
final String? countryCode;
final String? city;
final String? region;
final String? isp;
final String? timezone;
final double? lat;
final double? lon;
IPInfo({
required this.ip,
this.country,
this.countryCode,
this.city,
this.region,
this.isp,
this.timezone,
this.lat,
this.lon,
});
factory IPInfo.fromJson(Map<String, dynamic> json) {
final geo = json['geo'] as Map<String, dynamic>? ?? {};
return IPInfo(
ip: json['ip'] ?? '',
country: geo['country'],
countryCode: geo['country_code'],
city: geo['city'],
region: geo['region'],
isp: json['isp'],
timezone: json['timezone'],
lat: (geo['lat'] as num?)?.toDouble(),
lon: (geo['lon'] as num?)?.toDouble(),
);
}
}
Future<IPInfo?> getIPInfo({String lang = 'en'}) async {
try {
final uri = Uri.parse('https://ippubblico.org/?api=1&lang=$lang');
final response = await http
.get(uri)
.timeout(const Duration(seconds: 5));
if (response.statusCode == 200) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
return IPInfo.fromJson(json);
}
} catch (e) {
debugPrint('IP情報取得失敗: $e');
}
return null;
}
使用例(日本語レスポンス):
// ?lang=ja で都市名・国名が日本語で返ってくる
final info = await getIPInfo(lang: 'ja');
if (info != null) {
print('IP: ${info.ip}');
print('国: ${info.country}'); // 日本
print('都市: ${info.city}'); // 東京
print('地域: ${info.region}'); // 東京都
print('ISP: ${info.isp}');
print('タイムゾーン: ${info.timezone}');
}
ユースケース4 — ロケール・コンテンツのための国検出
よくある実際のユースケース:最初の起動時にユーザーの国を検出してデフォルト言語を設定したり、地域固有のコンテンツを表示したりする。
class CountryDetector {
static String? _cachedCountryCode;
static Future<String?> getCountryCode() async {
if (_cachedCountryCode != null) return _cachedCountryCode;
try {
final response = await http
.get(Uri.parse('https://ippubblico.org/?api=1'))
.timeout(const Duration(seconds: 5));
if (response.statusCode == 200) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
final geo = json['geo'] as Map<String, dynamic>? ?? {};
_cachedCountryCode = geo['country_code'] as String?;
return _cachedCountryCode;
}
} catch (e) {
debugPrint('国検出失敗: $e');
}
return null;
}
static void clearCache() {
_cachedCountryCode = null;
}
}
main.dartでの使用例:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final countryCode = await CountryDetector.getCountryCode();
final locale = _localeFromCountry(countryCode);
runApp(MyApp(initialLocale: locale));
}
Locale _localeFromCountry(String? code) {
switch (code) {
case 'JP': return const Locale('ja'); // 日本
case 'CN': return const Locale('zh'); // 中国
case 'KR': return const Locale('ko'); // 韓国
case 'DE': return const Locale('de'); // ドイツ
case 'FR': return const Locale('fr'); // フランス
default: return const Locale('en'); // その他は英語
}
}
エラーの適切な処理
本番アプリには常にフォールバックが必要です。複数の戦略を試す本番対応ラッパー:
Future<String?> getPublicIPWithFallback() async {
// まずプレーンテキストエンドポイントを試す(最速)
try {
final response = await http
.get(Uri.parse('https://ipv4.ippubblico.org/'))
.timeout(const Duration(seconds: 3));
if (response.statusCode == 200) {
return response.body.trim();
}
} catch (_) {}
// JSONエンドポイントにフォールバック
try {
final response = await http
.get(Uri.parse('https://ippubblico.org/?api=1'))
.timeout(const Duration(seconds: 5));
if (response.statusCode == 200) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
return json['ip'] as String?;
}
} catch (_) {}
return null;
}
レート制限の尊重
IPPubblicは無料でハードな制限はありませんが、乱用防止のためのソフトレート制限があります。ガイドライン:
- 継続的にポーリングしない — アプリ起動時に1回確認してキャッシュする
- 結果をキャッシュする — IPアドレスはめったに変わらない
-
429を処理する —
Too Many Requestsレスポンスを受け取った場合はRetry-Afterヘッダーを読んで待機する:
Future<String?> getIPRespectingRateLimit() async {
final response = await http
.get(Uri.parse('https://ipv4.ippubblico.org/'))
.timeout(const Duration(seconds: 5));
if (response.statusCode == 200) {
return response.body.trim();
}
if (response.statusCode == 429) {
final retryAfter = int.tryParse(
response.headers['retry-after'] ?? '30'
) ?? 30;
debugPrint('レート制限中。${retryAfter}秒後にリトライします。');
await Future.delayed(Duration(seconds: retryAfter));
// 1回リトライ
final retry = await http
.get(Uri.parse('https://ipv4.ippubblico.org/'))
.timeout(const Duration(seconds: 5));
if (retry.statusCode == 200) return retry.body.trim();
}
return null;
}
クイックリファレンス
| 用途 | エンドポイント | レスポンス |
|---|---|---|
| IPv4のみ | https://ipv4.ippubblico.org/ |
203.0.113.42 |
| IPv6のみ | https://ipv6.ippubblico.org/ |
2001:db8::1 または NONE
|
| 両プロトコル | https://ippubblico.org/?text=1 |
IPv4: x\nIPv6: x |
| 完全なジオロケーション | https://ippubblico.org/?api=1 |
都市、国、ISP付きJSON |
| 日本語レスポンス | https://ippubblico.org/?api=1&lang=ja |
都市・国名が日本語 |
完全なAPIドキュメント(日本語対応):ippubblico.org/docs.html
まとめ
IPPubblicはシンプルな「IPアドレスを返すだけ」から、43言語対応の完全なジオロケーションまで、1つのAPIで対応します。登録不要、キー管理不要、料金の心配なし。日本語レスポンス(?lang=ja)は比較した他のサービスには存在しない機能です。
Retry-Afterヘッダーの処理は、制限に引っかかることがなくても実装する価値があります — 良い実践であり、アプリを設計上から堅牢にします。
FlutterでのIP検出に別のアプローチを使っていますか?コメントで共有してください。