lejard-h / google_maps_webservice

BSD 3-Clause "New" or "Revised" License
168 stars 225 forks source link

How to add and manage session tokens in flutter project #76

Open jeneena-jose opened 4 years ago

jeneena-jose commented 4 years ago

Hello, Thanks for sharing such a useful package for integrating various Google APIs under one roof.

I am glad using it. But I am facing issues with pricing because in Google Autocomplete requests, there are a lot of queries generated without maintaining session tokens.

The APIs I use :

  1. Google Autocomplete API (multiple times)
  2. Google Place Details (single time as per user selected location)
  3. Google Geocoding (single time as per user inputs any location))

Please guide how to handle session tokens in flutter app to reduce pricing from Google Maps !

minoesteban commented 4 years ago

Hi, I'll stick around, I'm struggling with the same topic

flikkr commented 4 years ago

If you're still struggling with this, you can manage session tokens by creating a TokenGenerator class that issues a new token whenever a autocomplete query is completed or expired. Using my implementation, you call TokenGenerator.token whenever you want a token and it will return that same token until it expires. Once you are done with the search (i.e. the user selects a place), you can call TokenGenerator.done() for the generator to clear the current token. The token is generated using UUID package.

import 'package:uuid/uuid.dart';

class TokenGenerator {
  // Session token validity duration, according to Google
  static const Duration _validity = Duration(seconds: 180);
  static String _token;
  static DateTime _lastFetched;

  static String get _generate {
    _lastFetched = DateTime.now();
    _token = Uuid().v4();
    return _token;
  }

  static String get token {
    if (_lastFetched == null ||
        _token == null ||
        _lastFetched.add(_validity).isBefore(DateTime.now())) return _generate;
    return _token;
  }

  static void done() {
    _lastFetched = null;
    _token = null;
  }
}
jeneena-jose commented 4 years ago

Thanks @flikkr ! I will integrate UUID package and check.

minoesteban commented 4 years ago

Thanks @flikkr ! But actually, if i'm not mistaken the places autocomplete API returns a session token in the first response, and you can use that session token as many times as you want for autocomplete calls until the first call you make to the places details api (once a place was selected). What would be great is a way to handle these session tokens internally (capture it within the PlacesResponse object and reuse it with further autocomplete calls en place details calls)

lejard-h commented 3 years ago

I am not very familiar with this topic. But I don't see any session token return by the API

And Google seems to recommend to generate your own session token - https://developers.google.com/maps/documentation/places/web-service/session-tokens

tspoke commented 2 years ago

I created this handy class to wrap the autocomplete() of this library :

Features :

import 'dart:async';
import 'package:google_maps_webservice/places.dart';
import 'package:uuid/uuid.dart';

class AutoCompleteSession {
  static const Uuid uuid = Uuid();
  static final _logger = AppLogger.getLogger(); // TODO provide your own logger or just use print()

  final StreamController<List<Prediction>> _controller = StreamController();
  final GoogleMapsPlaces _places;
  final int _debounceDuration;
  Timer? _debounce;
  late String _sessionToken;

  var _previousSearch = "";

  AutoCompleteSession(this._places, this._debounceDuration) {
    _resetToken();
  }

  /// Search for address
  ///
  /// @remarks:
  ///  The session begins when the user starts typing a query,
  ///  and concludes when they select a place and a call to Place Details is made.
  ///
  ///  Each session can have multiple queries, followed by one place selection.
  ///  The API key(s) used for each request within a session must belong to the same Google Cloud Console project.
  ///  Once a session has concluded, the token is no longer valid; your app must generate a fresh token for each session.
  ///  If the sessiontoken parameter is omitted, or if you reuse a session token, the session is charged as if no session
  ///  token was provided (each request is billed separately).
  ///  @return bool   TRUE if the autocomplete will search for value
  bool search(final String input) {
    if (_previousSearch == input.trim()) {
      _logger.d("Skip autocomplete searching - same input detected");
      return false;
    }
    _previousSearch = input.trim();

    if (_debounce?.isActive ?? false) {
      _debounce?.cancel();
    }

    if (_previousSearch == "") { // skip empty requests
      _logger.d("Skip autocomplete searching - empty input");
      return false;
    }

    _debounce = Timer(Duration(milliseconds: _debounceDuration), () async {
      _places.autocomplete(input, sessionToken: _sessionToken, components: [Component(Component.country, "fr")], types: ["address"]).then((result) {
        if (result.isOkay) {
          _controller.add(result.predictions);
        } else if(result.hasNoResults) {
          _controller.add([]);
        } else {
        _logger.d(result.errorMessage);
        _controller.addError(Exception(result.errorMessage));
        }
      }).catchError((err) {
        _logger.d(err.errorMessage);
        _controller.addError(Exception(err.errorMessage));
      });
    });

    return true;
  }

  /// Get place details
  ///
  /// @fields: Basic fields (cheap): address_component, adr_address, business_status, formatted_address, geometry, icon, icon_mask_base_uri, icon_background_color, name,
  /// permanently_closed, photo, type, url, utc_offset, or vicinity
  Future<PlacesDetailsResponse?> getDetails(final Prediction prediction) async {
    final String? placeId = prediction.placeId;
    if (placeId != null) {
      if (_debounce?.isActive ?? false) {
        _debounce?.cancel();
      }

      final result = await _places.getDetailsByPlaceId(placeId, sessionToken: _sessionToken, fields: ["place_id", "geometry", "formatted_address", "name", "type"]);
      _resetToken();
      return result;
    }
    return null;
  }

  /// Get stream
  Stream<List<Prediction>> stream() {
    return _controller.stream;
  }

  /// Close the session
  void close() {
    if (_debounce?.isActive ?? false) {
      _debounce?.cancel();
    }

    if (!_controller.isClosed) {
      _controller.close();
    }
  }

  void _resetToken() {
    _sessionToken = uuid.v4().toString();
    _previousSearch = "";
    _logger.d("Autocomplete session - resetting token !");
  }
}

You can use it like this in your code :

var places = GoogleMapsPlaces(apiKey: "GOOGLE  PLACES API KEY");

var autocompleteSession = AutoCompleteSession(_places, 330);

// observe results in a stream an update your UI with it
autocompleteSession.stream().listen(
      (results) => /* update your ui or store here */,
      onError: (err) => print("err in mapAddController"),
      onDone: () => print("DONE"),
    );

// to search for string input
autoCompleteSession.search(input);

// then when the user click on a predictions 
autoCompleteSession.getPlaceDetails(prediction); // will reset token and cleanup :)

// don't forget to close the session when UI is disposed
autoCompleteSession.close();