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.
The behavior below is what the widget implements — typing a Mexican city:
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.