DEV3 Technologies home

Address autocomplete form

Autocomplete field that queries address suggestions and fills street, city and postcode inputs from the chosen result.

FlutterFormsflutteraddressautocompleteform

Code

import 'dart:async';
import 'package:flutter/material.dart';

class AddressSuggestion {
  const AddressSuggestion({required this.label, required this.street, required this.city, required this.postcode});
  final String label, street, city, postcode;
  @override
  String toString() => label;
}

typedef AddressLookup = Future<List<AddressSuggestion>> Function(String query);

class AddressForm extends StatefulWidget {
  const AddressForm({super.key, required this.lookup});
  final AddressLookup lookup;
  @override
  State<AddressForm> createState() => _AddressFormState();
}

class _AddressFormState extends State<AddressForm> {
  final _street = TextEditingController();
  final _city = TextEditingController();
  final _postcode = TextEditingController();
  Timer? _debounce;
  int _ticket = 0;

  @override
  void dispose() {
    _debounce?.cancel();
    for (final c in [_street, _city, _postcode]) { c.dispose(); }
    super.dispose();
  }

  /// Autocomplete wants a Future per keystroke; wait for a pause and drop stale answers.
  Future<Iterable<AddressSuggestion>> _options(TextEditingValue v) {
    final completer = Completer<Iterable<AddressSuggestion>>();
    _debounce?.cancel();
    final mine = ++_ticket;
    if (v.text.trim().length < 3) return Future.value(const []);
    _debounce = Timer(const Duration(milliseconds: 300), () async {
      try {
        final rows = await widget.lookup(v.text.trim());
        completer.complete(mine == _ticket ? rows : const []);
      } catch (_) {
        completer.complete(const []);
      }
    });
    return completer.future;
  }

  void _fill(AddressSuggestion s) {
    _street.text = s.street;
    _city.text = s.city;
    _postcode.text = s.postcode;
  }

  @override
  Widget build(BuildContext context) {
    return Column(children: [
      Autocomplete<AddressSuggestion>(
        optionsBuilder: (v) async => await _options(v),
        onSelected: _fill,
        fieldViewBuilder: (context, controller, focus, onSubmit) => TextField(
          controller: controller,
          focusNode: focus,
          decoration: const InputDecoration(labelText: 'Search address', prefixIcon: Icon(Icons.search)),
        ),
      ),
      const SizedBox(height: 12),
      TextField(controller: _street, decoration: const InputDecoration(labelText: 'Street')),
      Row(children: [
        Expanded(child: TextField(controller: _city, decoration: const InputDecoration(labelText: 'City'))),
        const SizedBox(width: 12),
        SizedBox(width: 120, child: TextField(controller: _postcode, decoration: const InputDecoration(labelText: 'Postcode'))),
      ]),
    ]);
  }
}

How it works

What it solves

Typing a full address on a phone is slow and error-prone. Suggestions fill most of the form with one tap, while the individual fields stay editable for corrections.

How it works

The widget uses Flutter's built-in Autocomplete, whose optionsBuilder may return a Future. _options wraps the lookup in a short debounce: every call bumps a ticket, cancels the previous timer and only completes the future with results if its ticket is still the newest. Queries shorter than three characters return an empty list immediately.

Choosing a suggestion calls _fill, which writes the street, city and postcode into their controllers. Because they are ordinary text fields, users can edit any part afterwards, and the form can validate them as usual.

Adapting it

Implement AddressLookup against your geocoding provider and map its result into AddressSuggestion. Add country and state fields if you ship internationally, and store coordinates in the suggestion for distance pricing. Style the dropdown with optionsViewBuilder when you need custom rows.

Pitfalls

  • Check your provider's terms about caching and display requirements for suggestions.
  • Never send the query on every keystroke; the debounce above also reduces billing for per-request APIs.
  • Autocomplete's field controller is its own; fill the other fields through their controllers, not the search one.

How to use it

Copy to lib/address/. No packages; supply a lookup function.

AddressForm(lookup: (q) => geocoder.suggest(q))

Read _street, _city and _postcode through your own controllers or a callback when submitting.

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets