Custom Place Picker with callback. Eliminate the extra button press.

Hi there,

I wanted a Place Picker that would allow the user to navigate immediately to the selected location rather than having to tap another button to zoom/pan to location. So I created a custom widget using flutter_google_places with a callback function.


EDIT: since first publishing this post, I hit dependency issues. Please replace "flutter_google_places" flutter package with "flutter_google_places_hoc081098"

Here's the code:

// Automatic FlutterFlow imports
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 '/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 '/backend/backend.dart';
//import '/backend/schema/structs/index.dart';
//import '/actions/actions.dart' as action_blocks;
//import '/custom_code/actions/index.dart'; // Imports custom actions

import 'package:flutter_google_places_hoc081098/flutter_google_places_hoc081098.dart';
//import 'package:flutter_google_places/flutter_google_places.dart';
import 'package:google_api_headers/google_api_headers.dart';
import 'package:google_maps_webservice/places.dart';
import 'package:flutter/foundation.dart';
import 'package:collection/collection.dart';
import '/flutter_flow/lat_lng.dart' as ffLatLng;

class LocationSearch extends StatefulWidget {
  const LocationSearch({
    super.key,
    this.width,
    this.height,
    required this.showSelection,
    this.onLocationSelected,
    required this.iOSGoogleMapsApiKey,
    required this.androidGoogleMapsApiKey,
    required this.webGoogleMapsApiKey,
  });

  final double? width;
  final double? height;
  final bool showSelection;
  //declare callback function onLocationSelected
  final Future Function(String? selectedLocation, LatLng? selectedLatLng)?
      onLocationSelected;
  final String iOSGoogleMapsApiKey;
  final String androidGoogleMapsApiKey;
  final String webGoogleMapsApiKey;

  @override
  State<LocationSearch> createState() => _LocationSearchState();
}

class _LocationSearchState extends State<LocationSearch> {
  String? _selectedLocation;

  @override
  String get googleMapsApiKey {
    switch (defaultTargetPlatform) {
      case TargetPlatform.macOS:
      case TargetPlatform.windows:
        return widget.webGoogleMapsApiKey;
      case TargetPlatform.iOS:
        return widget.iOSGoogleMapsApiKey;
      case TargetPlatform.android:
        return widget.androidGoogleMapsApiKey;
      default:
        return widget.webGoogleMapsApiKey;
    }
  }

  @override
  Widget build(BuildContext context) {
    String? languageCode = Localizations.localeOf(context).languageCode;

    // button styles set here. You could make them parameters
    final buttonBackgroundColour = Color(0xFFFF5A5F);
    final double buttonBorderRadius = 20;
    final double buttonElevation = 2;
    final buttonFontColour = Colors.white;
    final double buttonFontSize = 16;
    final buttonFontWeight = FontWeight.normal;
    final buttonInitialText = "Select Location";

    return (ElevatedButton(
      child: Text(_selectedLocation ?? buttonInitialText),
      //style the button
      style: ElevatedButton.styleFrom(
        shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(buttonBorderRadius)),
        backgroundColor: buttonBackgroundColour,
        textStyle: TextStyle(
          fontSize: buttonFontSize,
          color: buttonFontColour,
          fontWeight: buttonFontWeight,
        ),
        elevation: buttonElevation,
      ),
      onPressed: () async {
        final p = await PlacesAutocomplete.show(
          context: context,
          apiKey: googleMapsApiKey,
          onError: (response) =>
              print('SEARCH: Error occured when getting places response:'
                  '\n${response.errorMessage}'),
          mode: Mode.overlay,
          types: [],
          components: [new Component(Component.country, "Au")],
          strictbounds: false,
          language: languageCode,
        );
//        debugPrint('SEARCH: tapped place picker button');
        await displayPrediction(p, languageCode);
      },
    ));
  }

  Future displayPrediction(Prediction? p, String? languageCode) async {
    if (p != null && p.placeId != null) {
      GoogleMapsPlaces _places = GoogleMapsPlaces(
        apiKey: googleMapsApiKey,
        apiHeaders: await GoogleApiHeaders().getHeaders(),
      );
      PlacesDetailsResponse detail =
          await _places.getDetailsByPlaceId(p.placeId!);
      if (detail != null &&
          detail.result != null &&
          detail.result.geometry != null) {
        final lat = detail.result.geometry?.location?.lat;
        final lng = detail.result.geometry?.location?.lng;
        debugPrint('SEARCH: gotlatlng: $lat  $lng');
        if (lat != null && lng != null) {
          //call the custom action to reload markers
          final locationLatLng = ffLatLng.LatLng(lat, lng);
          await widget.onLocationSelected!
              .call(detail.result.name, locationLatLng);
//          debugPrint('SEARCH: Completed callback');
          if (widget.showSelection) {
            setState(() {
              _selectedLocation = detail.result.name;
            });
//            debugPrint('saved to state for button label: $_selectedLocation');
          }
        }
      }
    }
  }
}

and here's the configuration in FF;

10
20 replies