Flutter city autocomplete tutorial

A self-contained Flutter widget that debounces keystrokes and queries Geomelon's free, no-key prefix-search endpoint. Add the http package (flutter pub add http) and drop this widget in.

Live demo (web preview)

The behavior below is what the widget implements — typing a Mexican city:

Type to search Mexico cities…

The widget

import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

class City {
  final String id;
  final String name;
  final int? population;
  final String? emoji;

  City({required this.id, required this.name, this.population, this.emoji});

  factory City.fromJson(Map<String, dynamic> json) => City(
        id: json['id'] as String,
        name: json['name'] as String,
        population: json['population'] as int?,
        emoji: json['emoji'] as String?,
      );
}

class CityAutocomplete extends StatefulWidget {
  final String countryIso;
  final String lang;
  final void Function(City) onSelected;

  const CityAutocomplete({
    super.key,
    required this.countryIso,
    required this.lang,
    required this.onSelected,
  });

  @override
  State<CityAutocomplete> createState() => _CityAutocompleteState();
}

class _CityAutocompleteState extends State<CityAutocomplete> {
  final _controller = TextEditingController();
  List<City> _results = [];
  Timer? _debounce;

  Future<void> _search(String prefix) async {
    if (prefix.trim().isEmpty) {
      setState(() => _results = []);
      return;
    }
    final url = Uri.parse(
      'https://oneshot.geomelon.dev/${widget.countryIso}/${widget.lang}/${Uri.encodeComponent(prefix.toLowerCase())}',
    );
    final res = await http.get(url);
    if (res.statusCode == 200) {
      final list = jsonDecode(res.body) as List;
      setState(() => _results = list.map((e) => City.fromJson(e)).toList());
    } else {
      setState(() => _results = []);
    }
  }

  void _onChanged(String value) {
    _debounce?.cancel();
    _debounce = Timer(const Duration(milliseconds: 150), () => _search(value));
  }

  @override
  void dispose() {
    _debounce?.cancel();
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        TextField(
          controller: _controller,
          onChanged: _onChanged,
          decoration: const InputDecoration(hintText: 'Start typing a city...'),
        ),
        ..._results.map(
          (city) => ListTile(
            leading: Text(city.emoji ?? ''),
            title: Text(city.name),
            onTap: () {
              _controller.text = city.name;
              widget.onSelected(city);
              setState(() => _results = []);
            },
          ),
        ),
      ],
    );
  }
}

For search across countries, sorting, or resolving a picked city to full details (coordinates, region, translations), call GET /cities/search or GET /cities/:id on the main API instead — same http.get pattern, with the two RapidAPI headers added to the request.

Try the full API free on RapidAPI