FlutterDartMobileGeolocationTutorial

Flutter & Dart IP Geolocation: Detect Country from IP in Mobile Apps

ApogeoAPI5 min read

Important: don't embed API keys in Flutter apps

Flutter app bundles are publicly inspectable — never put your API key directly in the Flutter/Dart code. The recommended pattern is: Flutter → your own backend endpoint → ApogeoAPI. Your backend holds the key and the Flutter app calls your endpoint.

Backend endpoint (Dart shelf / any server)

// server/geo_handler.dart  (runs on your server, not in the app)
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shelf/shelf.dart';

final _cache = <String, Map<String, dynamic>>{};

Response geoHandler(Request request) async {
  final ip = request.headers['x-forwarded-for']?.split(',').first.trim()
      ?? request.context['shelf.io.connection_info'].remoteAddress.address;

  final subnet = ip.split('.').take(3).join('.');
  if (_cache.containsKey(subnet)) return Response.ok(json.encode(_cache[subnet]!));

  final res = await http.get(
    Uri.parse('https://api.apogeoapi.com/v1/ip/$ip'),
    headers: {'Authorization': 'Bearer ${const String.fromEnvironment("APOGEOAPI_KEY")}'},
  );

  final data = json.decode(res.body) as Map<String, dynamic>;
  _cache[subnet] = data;
  return Response.ok(json.encode(data), headers: {'Content-Type': 'application/json'});
}

Flutter service calling your backend

// lib/services/geo_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';

class GeoData {
  final String countryCode;
  final String? city;
  final String? currencyCode;
  final String? timezone;

  const GeoData({
    required this.countryCode,
    this.city,
    this.currencyCode,
    this.timezone,
  });

  factory GeoData.fromJson(Map<String, dynamic> j) => GeoData(
        countryCode: j['country_code'] as String? ?? 'US',
        city:         j['city']          as String?,
        currencyCode: j['currency_code'] as String?,
        timezone:     j['timezone']      as String?,
      );
}

class GeoService {
  static const _cacheKey = 'geo_country';
  static const _cacheTtlKey = 'geo_ts';
  static const _cacheTtl = Duration(hours: 1);
  final String backendUrl;

  const GeoService({required this.backendUrl});

  Future<GeoData> detect() async {
    // Check SharedPreferences cache first
    final prefs = await SharedPreferences.getInstance();
    final ts = prefs.getInt(_cacheTtlKey) ?? 0;
    final cachedJson = prefs.getString(_cacheKey);

    if (cachedJson != null &&
        DateTime.now().millisecondsSinceEpoch - ts < _cacheTtl.inMilliseconds) {
      return GeoData.fromJson(json.decode(cachedJson) as Map<String, dynamic>);
    }

    try {
      final res = await http
          .get(Uri.parse('$backendUrl/api/geo'))
          .timeout(const Duration(seconds: 3));

      if (res.statusCode == 200) {
        final data = json.decode(res.body) as Map<String, dynamic>;
        await prefs.setString(_cacheKey, json.encode(data));
        await prefs.setInt(_cacheTtlKey, DateTime.now().millisecondsSinceEpoch);
        return GeoData.fromJson(data);
      }
    } catch (_) {
      // Network error — return default
    }
    return const GeoData(countryCode: 'US');
  }
}

Using GeoData in a Flutter widget

// lib/screens/pricing_screen.dart
import 'package:flutter/material.dart';
import '../services/geo_service.dart';

class PricingScreen extends StatefulWidget {
  const PricingScreen({super.key});
  @override
  State<PricingScreen> createState() => _PricingScreenState();
}

class _PricingScreenState extends State<PricingScreen> {
  final _geo = const GeoService(backendUrl: 'https://api.yourapp.com');
  GeoData? _geoData;

  @override
  void initState() {
    super.initState();
    _geo.detect().then((d) => setState(() => _geoData = d));
  }

  @override
  Widget build(BuildContext context) {
    final country = _geoData?.countryCode ?? 'US';
    final currency = _geoData?.currencyCode ?? 'USD';

    final (symbol, amount) = switch (currency) {
      'USD' => ('$', 29),
      'GBP' => ('£', 25),
      'EUR' => ('€', 27),
      'AUD' => ('A$', 45),
      'BRL' => ('R$', 149),
      _ => ('$', 29),
    };

    return Scaffold(
      appBar: AppBar(title: const Text('Pricing')),
      body: Center(
        child: Column(children: [
          Text('Your region: $country', style: Theme.of(context).textTheme.headlineSmall),
          Text('$symbol$amount / month', style: Theme.of(context).textTheme.displaySmall),
        ]),
      ),
    );
  }
}

pubspec.yaml dependencies

dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.0
  shared_preferences: ^2.2.2

Resources

Try ApogeoAPI free

1,000 requests/month forever. 14-day full-access trial. No credit card.

Get your free API key