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?

FlutterでパブリックIPを取得する — APIキー不要、レート制限なし

0
Posted at

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.yamlhttpを追加:

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検出に別のアプローチを使っていますか?コメントで共有してください。

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?