Custom autocomplete address Brazil

Hello guys,

Due to the high costs of Google's auto-complete, I created a custom code as an alternative to easily retrieve addresses for the user. Only for addresses in Brazil

Here's the code:

// Automatic FlutterFlow imports
import '/backend/backend.dart';
import '/backend/schema/structs/index.dart';
import '/backend/supabase/supabase.dart';
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
import '/custom_code/widgets/index.dart'; // Imports other custom widgets
import '/custom_code/actions/index.dart'; // Imports custom actions
import '/flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom widget code
// DO NOT REMOVE OR MODIFY THE CODE ABOVE!

import 'dart:convert';

import 'package:http/http.dart' as http;

class Endereco extends StatefulWidget {
  const Endereco({
    Key? key,
    this.width,
    this.height,
    required this.rapidApiKey,
    required this.hinttext,
    required this.radius,
    required this.corBorda,
    required this.corBordaFoco,
  }) : super(key: key);

  final double? width;
  final double? height;
  final String rapidApiKey;
  final String hinttext;
  final double radius;
  final Color corBorda;
  final Color corBordaFoco;

  @override
  _EnderecoState createState() => _EnderecoState();
}

class _EnderecoState extends State<Endereco> {
  double radius = 8.0;
  Color corBorda = Colors.black;
  Color corBordaFoco = Colors.blue;
  String hinttext = '';
  String rapidApiKey = '';
  final searchTextController = TextEditingController();
  List<SearchItem> searchItems = [];
  bool isSearching = false;
  bool showSuggestions = false;
  String? selectedLat;
  String? selectedLng;
  String lastSearch = '';

  @override
  void initState() {
    super.initState();
    searchTextController.addListener(_onSearchTextChanged);
  }

  void _onSearchTextChanged() {
    final query = searchTextController.text;
    if (query.isNotEmpty && query != lastSearch && !isSearching) {
      setState(() {
        showSuggestions = true;
        isSearching = true;
      });
      searchLocation(query);
    } else {
      setState(() {
        showSuggestions = false;
      });
    }
  }

  Future<void> searchLocation(String query) async {
    try {
      final response = await http.get(
        Uri.parse(
          'https://autocomplete-address-brazil.p.rapidapi.com/search.php/$query?addressdetails=1',
        ),
        headers: {
          'X-RapidAPI-Host': 'autocomplete-address-brazil.p.rapidapi.com',
          'X-RapidAPI-Key': widget.rapidApiKey,
        },
      );

      if (response.statusCode == 200) {
        final data = response.body;
        print(data);

        if (data.isNotEmpty) {
          final searchData = List<SearchItem>.from(
            json.decode(data).map((x) => SearchItem.fromJson(x)),
          );

          setState(() {
            searchItems = searchData;
            isSearching = false;
          });
        } else {
          print('Resposta vazia');
        }
      } else {
        print('Erro na requisição HTTP: ${response.reasonPhrase}');
      }
    } catch (e) {
      print('Erro durante a solicitação HTTP: $e');
    }
  }

  void updateLocation(SearchItem data) {
    setState(() {
      searchTextController.text = data.displayName;
      selectedLat = data.lat.toString();
      selectedLng = data.lon.toString();
      searchItems = [];
      lastSearch = data.displayName;
      showSuggestions = false;

      // Criar AddressStruct
      AddressStruct endereco = AddressStruct(
        place: data.address['place'] ?? '',
        number: data.address['house_number'] ?? '',
        road: data.address['road'] ?? '',
        suburb: data.address['suburb'] ?? '',
        city: data.address['city'] ?? '',
        state: data.address['state'] ?? '',
        postcode: data.address['postcode'] ?? '',
        lat: data.lat.toDouble(),
        lon: data.lon.toDouble(),
      );

      // Atribuir à variável FFAppState().Endereco
      FFAppState().Endereco = endereco;
    }); // Certifique-se de que esta chave fecha o setState
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          controller: searchTextController,
          decoration: InputDecoration(
            hintText: widget.hinttext,
            border: OutlineInputBorder(
              borderRadius: BorderRadius.circular(widget.radius),
            ),
            focusedBorder: OutlineInputBorder(
              borderRadius: BorderRadius.circular(widget.radius),
              borderSide: BorderSide(color: widget.corBordaFoco),
            ),
            enabledBorder: OutlineInputBorder(
              borderRadius: BorderRadius.circular(widget.radius),
              borderSide: BorderSide(color: widget.corBorda),
            ),
          ),
          onTap: () => _onSearchTextChanged(),
          onChanged: (value) {
            if (value.isEmpty) {
              setState(() {
                searchItems = [];
                showSuggestions = false;
              });
            }
          },
        ),
        if (showSuggestions)
          SizedBox(
            height: 300,
            child: ListView.builder(
              itemCount: searchItems.length,
              itemBuilder: (context, index) {
                final item = searchItems[index];
                return ListTile(
                  leading: Icon(Icons.place),
                  onTap: () => updateLocation(item),
                  title: Text(
                    item.displayName,
                    overflow: TextOverflow.ellipsis,
                    maxLines: 1,
                  ),
                );
              },
            ),
          ),
      ],
    );
  }

  @override
  void dispose() {
    searchTextController.removeListener(_onSearchTextChanged);
    searchTextController.dispose();
    super.dispose();
  }
}

class SearchItem {
  final String displayName;
  final double lat;
  final double lon;
  final Map<String, dynamic> address;

  SearchItem(
      {required this.displayName,
      required this.lat,
      required this.lon,
      required this.address});

  factory SearchItem.fromJson(Map<String, dynamic> json) {
    return SearchItem(
      displayName: json['display_name'] ?? '',
      lat: double.tryParse(json['lat'] ?? '0.0') ?? 0.0,
      lon: double.tryParse(json['lon'] ?? '0.0') ?? 0.0,
      address: json['address'] ?? {},
    );
  }
}

In this widget, you need to pass your API key acquired from RapidAPI: https://rapidapi.com/helinhoehmau/api/autocomplete-address-brazil

You can customize the radius, border, focused border, and hint text.

When the user selects an address, it will fill the variable "Endereco," which is an app state with the data type "Address." The data type to be created should be as follows:

When the user selects the address, you can retrieve the following information:

The API also has reverse geocoding and Mapbox capabilities; however, I did not address them in this custom widget.

I hope you like it. If you have any questions, feel free to send me a private message.

7
2 replies