google_places_autocomplete 1.0.0 copy "google_places_autocomplete: ^1.0.0" to clipboard
google_places_autocomplete: ^1.0.0 copied to clipboard

Seamlessly integrate Google Places API into your Flutter app with this package. Get location autocomplete, detailed place info, and flexible UI implementation.

example/lib/main.dart

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

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Google Places Autocomplete Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.blue,
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      home: const PlacesAutocompleteScreen(),
    );
  }
}

class PlacesAutocompleteScreen extends StatefulWidget {
  const PlacesAutocompleteScreen({super.key});

  @override
  State<PlacesAutocompleteScreen> createState() =>
      _PlacesAutocompleteScreenState();
}

class _PlacesAutocompleteScreenState extends State<PlacesAutocompleteScreen> {
  final _searchController = TextEditingController();
  late GooglePlacesAutocomplete _placesService;

  List<Prediction> _predictions = [];
  bool _isLoading = false;
  PlaceDetails? _selectedPlace;
  bool _isInitialized = false;

  // Example user location (San Francisco)
  // In production, get this from Geolocator or location services
  static const double _userLat = 37.7749;
  static const double _userLng = -122.4194;

  @override
  void initState() {
    super.initState();
    // Initialize the places service
    // Note: Initialization is async and connects to the native platform
    _initPlacesService();
  }

  Future<void> _initPlacesService() async {
    _placesService = GooglePlacesAutocomplete(
      // 1. API Key:
      // The package will automatically read 'com.google.android.geo.API_KEY' from AndroidManifest
      // and 'GOOGLE_PLACES_API_KEY' from Info.plist.
      // You can also pass it explicitly here:
      // apiKey: 'YOUR_API_KEY',

      // 2. Distance:
      // Provide user location to get distance metrics in predictions
      originLat: _userLat,
      originLng: _userLng,

      // 3. Filters:
      // Optional: Filter by country (ISO 3166-1 Alpha-2)
      countries: ['us'],
      // Optional: Filter by place types
      // placeTypes: ['restaurant'],

      // 4. UI Updates:
      debounceTime: 500, // Smoother typing experience
      predictionsListener: (predictions) {
        if (mounted) {
          setState(() => _predictions = predictions);
        }
      },
      loadingListener: (isLoading) {
        if (mounted) {
          setState(() => _isLoading = isLoading);
        }
      },
      // 5. Error Handling:
      // Handle errors gracefully with the onError callback
      onError: (error) {
        debugPrint('Places API Error: ${error.code} - ${error.message}');
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text('Error: ${error.message}'),
              backgroundColor: Colors.red,
            ),
          );
        }
      },
    );

    try {
      await _placesService.initialize();
      if (mounted) {
        setState(() => _isInitialized = true);
      }
    } catch (e) {
      debugPrint("Failed to initialize Places Service: $e");
    }
  }

  @override
  void dispose() {
    _searchController.dispose();
    _placesService.dispose(); // Important: dispose the places service
    super.dispose();
  }

  Future<void> _onPredictionTap(Prediction prediction) async {
    if (prediction.placeId == null) return;

    final details = await _placesService.getPlaceDetails(prediction.placeId!);

    if (mounted && details != null) {
      setState(() {
        _selectedPlace = details;
        _predictions = [];
        _searchController.text = prediction.title ?? '';
      });
    }
  }

  /// Format distance in a human-readable way
  String _formatDistance(int meters) {
    if (meters >= 1000) {
      final km = meters / 1000;
      return km >= 10 ? '${km.round()} km' : '${km.toStringAsFixed(1)} km';
    }
    return '$meters m';
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Places Autocomplete'),
        centerTitle: true,
      ),
      body: !_isInitialized
          ? const Center(child: CircularProgressIndicator())
          : Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                children: [
                  // Search Field
                  TextField(
                    controller: _searchController,
                    decoration: InputDecoration(
                      hintText: 'Search for a place...',
                      prefixIcon: const Icon(Icons.search),
                      suffixIcon: _isLoading
                          ? const Padding(
                              padding: EdgeInsets.all(12),
                              child: SizedBox(
                                width: 20,
                                height: 20,
                                child:
                                    CircularProgressIndicator(strokeWidth: 2),
                              ),
                            )
                          : _searchController.text.isNotEmpty
                              ? IconButton(
                                  icon: const Icon(Icons.clear),
                                  onPressed: () {
                                    _searchController.clear();
                                    setState(() => _predictions = []);
                                  },
                                )
                              : null,
                      border: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(12),
                      ),
                    ),
                    onChanged: (value) {
                      if (value.isNotEmpty) {
                        _placesService.getPredictions(value);
                      } else {
                        setState(() => _predictions = []);
                      }
                    },
                  ),

                  const SizedBox(height: 16),

                  // Predictions List
                  Expanded(
                    child: _predictions.isEmpty
                        ? _buildEmptyState()
                        : ListView.separated(
                            itemCount: _predictions.length,
                            separatorBuilder: (_, __) => const Divider(),
                            itemBuilder: (context, index) {
                              final prediction = _predictions[index];
                              return _buildPredictionTile(prediction);
                            },
                          ),
                  ),

                  // Selected Place Details
                  if (_selectedPlace != null) ...[
                    const Divider(height: 32),
                    _buildPlaceDetails(_selectedPlace!, theme),
                  ],
                ],
              ),
            ),
    );
  }

  Widget _buildEmptyState() {
    return Center(
      child: Text(
        _searchController.text.isEmpty
            ? 'Start typing to search'
            : 'No results found',
        style: TextStyle(color: Colors.grey[600]),
      ),
    );
  }

  Widget _buildPredictionTile(Prediction prediction) {
    return ListTile(
      leading: const CircleAvatar(
        child: Icon(Icons.location_on, size: 20),
      ),
      title: Text(
        prediction.title ?? '',
        maxLines: 1,
        overflow: TextOverflow.ellipsis,
      ),
      subtitle: Text(
        prediction.description ?? '',
        maxLines: 1,
        overflow: TextOverflow.ellipsis,
        style: TextStyle(color: Colors.grey[600]),
      ),
      // NEW: Distance badge
      trailing: prediction.distanceMeters != null
          ? Container(
              padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
              decoration: BoxDecoration(
                color: Theme.of(context).colorScheme.primaryContainer,
                borderRadius: BorderRadius.circular(12),
              ),
              child: Text(
                _formatDistance(prediction.distanceMeters!),
                style: TextStyle(
                  color: Theme.of(context).colorScheme.onPrimaryContainer,
                  fontSize: 12,
                  fontWeight: FontWeight.w500,
                ),
              ),
            )
          : null,
      onTap: () => _onPredictionTap(prediction),
    );
  }

  Widget _buildPlaceDetails(PlaceDetails details, ThemeData theme) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              details.name ?? 'Selected Place',
              style: theme.textTheme.titleMedium?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 8),
            if (details.formattedAddress != null)
              Row(
                children: [
                  const Icon(Icons.location_on, size: 16, color: Colors.red),
                  const SizedBox(width: 8),
                  Expanded(child: Text(details.formattedAddress!)),
                ],
              ),
            if (details.location != null) ...[
              const SizedBox(height: 8),
              Row(
                children: [
                  const Icon(Icons.pin_drop, size: 16, color: Colors.purple),
                  const SizedBox(width: 8),
                  Text(
                    '${details.location!.lat.toStringAsFixed(4)}, ${details.location!.lng.toStringAsFixed(4)}',
                    style: const TextStyle(fontFamily: 'monospace'),
                  ),
                ],
              ),
            ],
          ],
        ),
      ),
    );
  }
}
7
likes
150
points
1.59k
downloads

Publisher

verified publishercuboidinc.com

Weekly Downloads

Seamlessly integrate Google Places API into your Flutter app with this package. Get location autocomplete, detailed place info, and flexible UI implementation.

Repository (GitHub)
Contributing

Topics

#network #http #google-places #flexible-ui #map

Documentation

API reference

License

MIT (license)

Dependencies

flutter, plugin_platform_interface, rxdart

More

Packages that depend on google_places_autocomplete

Packages that implement google_places_autocomplete