google_places_sdk_plus 0.5.1
google_places_sdk_plus: ^0.5.1 copied to clipboard
A Flutter plugin for google places sdk that uses the native libraries on each platform
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_places_sdk_plus/google_places_sdk_plus.dart';
import 'package:google_places_sdk_plus_example/constants.dart';
import 'package:google_places_sdk_plus_example/google_places_img.dart'
if (dart.library.html) 'package:google_places_sdk_plus_example/google_places_img_web.dart'
as gpi;
import 'package:google_places_sdk_plus_example/settings_page.dart';
/// Title
const title = 'Flutter Google Places SDK Example';
void main() {
runApp(MyApp());
}
/// Main app
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: title,
theme: ThemeData(primaryColor: Colors.blueAccent),
home: MyHomePage(),
);
}
}
/// Main home page
class MyHomePage extends StatefulWidget {
/// Construct the HomePage
const MyHomePage({super.key, this.initOnStart = true});
/// When true, the relevant classes (e.g. [FlutterGooglePlacesSdk]) will be
/// initialized as part of the lifecycle (e.g. initState).
/// When false, user will need to click the "Init" button to initialize it.
final bool initOnStart;
@override
State<StatefulWidget> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
FlutterGooglePlacesSdk? _placesVar;
FlutterGooglePlacesSdk get _places => _placesVar!;
//
String? _predictLastText;
List<String> _placeTypesFilter = ['establishment'];
bool _locationBiasEnabled = true;
LatLngBounds _locationBias = LatLngBounds(
southwest: LatLng(lat: 32.0810305, lng: 34.785707),
northeast: LatLng(lat: 32.0935937, lng: 34.8013896),
);
bool _locationRestrictionEnabled = false;
LatLngBounds _locationRestriction = LatLngBounds(
southwest: LatLng(lat: 32.0583974, lng: 34.7633473),
northeast: LatLng(lat: 32.0876885, lng: 34.8040563),
);
List<String> _countries = ['il'];
bool _countriesEnabled = true;
bool _predicting = false;
dynamic _predictErr;
List<AutocompletePrediction>? _predictions;
//
final TextEditingController _fetchPlaceIdController = TextEditingController();
List<PlaceField> _placeFields = PlaceField.values;
// List<PlaceField> _placeFields = [
// PlaceField.Address,
// PlaceField.AddressComponents,
// PlaceField.BusinessStatus,
// PlaceField.Id,
// PlaceField.Location,
// PlaceField.DisplayName,
// PlaceField.OpeningHours,
// PlaceField.NationalPhoneNumber,
// PlaceField.InternationalPhoneNumber,
// PlaceField.Photos,
// PlaceField.PlusCode,
// PlaceField.PriceLevel,
// PlaceField.Rating,
// PlaceField.Types,
// PlaceField.UserRatingCount,
// PlaceField.UtcOffset,
// PlaceField.Viewport,
// PlaceField.WebsiteUri,
// ];
bool _fetchingPlace = false;
dynamic _fetchingPlaceErr;
bool _fetchingPlacePhoto = false;
dynamic _fetchingPlacePhotoErr;
Place? _place;
FetchPlacePhotoResponse? _placePhoto;
PhotoMetadata? _placePhotoMetadata;
// -- Search by Text
String? _searchByTextLastQuery;
bool _searchingByText = false;
dynamic _searchByTextErr;
List<Place>? _searchByTextResults;
String? _searchByTextIncludedType;
int? _searchByTextMaxResults;
double? _searchByTextMinRating;
bool _searchByTextOpenNow = false;
bool _searchByTextStrictTypeFiltering = false;
TextSearchRankPreference? _searchByTextRankPreference;
bool _searchByTextLocationBiasEnabled = false;
LatLngBounds _searchByTextLocationBias = LatLngBounds(
southwest: LatLng(lat: 32.0810305, lng: 34.785707),
northeast: LatLng(lat: 32.0935937, lng: 34.8013896),
);
// -- Search Nearby
bool _searchingNearby = false;
dynamic _searchNearbyErr;
List<Place>? _searchNearbyResults;
String? _searchNearbyIncludedTypes;
int? _searchNearbyMaxResults;
NearbySearchRankPreference? _searchNearbyRankPreference;
LatLng _searchNearbyCenter = LatLng(lat: 32.0853, lng: 34.7818);
double _searchNearbyRadius = 500.0;
@override
void initState() {
super.initState();
if (widget.initOnStart) {
_doInit();
}
}
void _doInit() {
if (_placesVar != null) {
debugPrint('Warning: Places init called after already initialized!');
return;
}
_placesVar = FlutterGooglePlacesSdk(
INITIAL_API_KEY,
locale: INITIAL_LOCALE,
);
_places.isInitialized().then((value) {
debugPrint('Places Initialized: $value');
// Update the state to reflect initialized state
setState(() {});
});
}
@override
Widget build(BuildContext context) {
final initWidgets = _buildInitWidgets();
final predictionsWidgets = _buildPredictionWidgets();
final fetchPlaceWidgets = _buildFetchPlaceWidgets();
final fetchPlacePhotoWidgets = _buildFetchPlacePhotoWidgets();
final searchByTextWidgets = _buildSearchByTextWidgets();
final searchNearbyWidgets = _buildSearchNearbyWidgets();
return Scaffold(
appBar: AppBar(
title: const Text(title),
actions: [
new IconButton(
onPressed: _openSettingsModal,
icon: const Icon(Icons.settings),
),
],
),
body: Padding(
padding: EdgeInsets.all(30),
child: ListView(
children:
initWidgets +
[SizedBox(height: 16)] +
predictionsWidgets +
[SizedBox(height: 16)] +
fetchPlaceWidgets +
[SizedBox(height: 16)] +
fetchPlacePhotoWidgets +
[SizedBox(height: 16)] +
searchByTextWidgets +
[SizedBox(height: 16)] +
searchNearbyWidgets,
),
),
);
}
void _onPlaceTypeFilterChanged(String? value) {
if (value != null) {
setState(() {
_placeTypesFilter = [value];
});
}
}
String? _countriesValidator(String? input) {
if (input == null || input.length == 0) {
return null; // valid
}
return input
.split(",")
.map((part) => part.trim())
.map((part) {
if (part.length != 2) {
return "Country part '${part}' must be 2 characters";
}
return null;
})
.where((item) => item != null)
.firstOrNull;
}
void _onCountriesTextChanged(String countries) {
_countries = (countries == "")
? []
: countries
.split(",")
.map((item) => item.trim())
.toList(growable: false);
}
void _onPredictTextChanged(String value) {
_predictLastText = value;
}
void _fetchPlace() async {
if (_fetchingPlace) {
return;
}
final text = _fetchPlaceIdController.text;
final hasContent = text.isNotEmpty;
setState(() {
_fetchingPlace = hasContent;
_fetchingPlaceErr = null;
});
if (!hasContent) {
return;
}
try {
final result = await _places.fetchPlace(
_fetchPlaceIdController.text,
fields: _placeFields,
);
setState(() {
_place = result.place;
_fetchingPlace = false;
});
} catch (err) {
setState(() {
_fetchingPlaceErr = err;
_fetchingPlace = false;
});
}
}
void _predict() async {
if (_predicting) {
return;
}
final hasContent = _predictLastText?.isNotEmpty ?? false;
setState(() {
_predicting = hasContent;
_predictErr = null;
});
if (!hasContent) {
return;
}
try {
final result = await _places.findAutocompletePredictions(
_predictLastText!,
countries: _countriesEnabled ? _countries : null,
placeTypesFilter: _placeTypesFilter,
newSessionToken: false,
origin: LatLng(lat: 43.12, lng: 95.20),
locationBias: _locationBiasEnabled ? _locationBias : null,
locationRestriction: _locationRestrictionEnabled
? _locationRestriction
: null,
);
setState(() {
_predictions = result.predictions;
_predicting = false;
});
} catch (err) {
setState(() {
_predictErr = err;
_predicting = false;
});
}
}
void _onItemClicked(AutocompletePrediction item) {
_fetchPlaceIdController.text = item.placeId ?? '';
}
Widget _buildPredictionItem(AutocompletePrediction item) {
return InkWell(
onTap: () => _onItemClicked(item),
child: Column(
children: [
Text(item.fullText ?? ''),
Text('${item.primaryText ?? ''} - ${item.secondaryText ?? ''}'),
const Divider(thickness: 2),
],
),
);
}
Widget _buildErrorWidget(dynamic err) {
final theme = Theme.of(context);
final errorText = err == null ? '' : err.toString();
return Text(
errorText,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.error,
),
);
}
List<Widget> _buildFetchPlacePhotoWidgets() {
return [
// --
// TextFormField(controller: _fetchPlaceIdController),
ElevatedButton(
onPressed: (_fetchingPlacePhoto == true || _place == null)
? null
: _fetchPlacePhoto,
child: const Text('Fetch Place Photo'),
),
// -- Error widget + Result
_buildErrorWidget(_fetchingPlacePhotoErr),
_buildPhotoWidget(_placePhoto),
];
}
void _fetchPlacePhoto() async {
final place = _place;
if (_fetchingPlacePhoto || place == null) {
return;
}
if ((place.photoMetadatas?.length ?? 0) == 0) {
setState(() {
_fetchingPlacePhoto = false;
_fetchingPlacePhotoErr = "No photos for place";
});
return;
}
setState(() {
_fetchingPlacePhoto = true;
_fetchingPlacePhotoErr = null;
});
try {
final metadata = place.photoMetadatas![0];
final result = await _places.fetchPlacePhoto(metadata);
setState(() {
_placePhoto = result;
_placePhotoMetadata = metadata;
_fetchingPlacePhoto = false;
});
} catch (err) {
setState(() {
_fetchingPlacePhotoErr = err;
_fetchingPlacePhoto = false;
});
}
}
List<Widget> _buildFetchPlaceWidgets() {
return [
// --
TextFormField(controller: _fetchPlaceIdController),
ElevatedButton(
onPressed: _fetchingPlace == true ? null : _fetchPlace,
child: const Text('Fetch Place'),
),
// -- Error widget + Result
_buildErrorWidget(_fetchingPlaceErr),
WebSelectableText('Result: ' + (_place?.toString() ?? 'N/A')),
];
}
Widget _buildEnabledOption(
bool value,
void Function(bool) callback,
Widget child,
) {
return Row(
children: [
Checkbox(
value: value,
onChanged: (value) {
setState(() {
callback(value ?? false);
});
},
),
Flexible(child: child),
],
);
}
List<Widget> _buildInitWidgets() {
final isInit = _placesVar != null;
return [
Row(
children: [
isInit
? Icon(Icons.check, color: Colors.green)
: Icon(Icons.close, color: Colors.red),
Text('Initialized: ' + (isInit ? 'true' : 'false')),
],
),
ElevatedButton(
onPressed: isInit ? null : _doInit,
child: Text('Initialize!'),
),
];
}
List<Widget> _buildPredictionWidgets() {
return [
// --
TextFormField(
onChanged: _onPredictTextChanged,
decoration: InputDecoration(label: Text("Query")),
),
// -- Countries
_buildEnabledOption(
_countriesEnabled,
(value) => _countriesEnabled = value,
TextFormField(
enabled: _countriesEnabled,
onChanged: _onCountriesTextChanged,
decoration: InputDecoration(label: Text("Countries")),
validator: _countriesValidator,
autovalidateMode: AutovalidateMode.onUserInteraction,
initialValue: _countries.join(","),
),
),
// -- Place Types
DropdownButton<String>(
items: const ['address', 'establishment', 'geocode']
.map(
(item) =>
DropdownMenuItem<String>(child: Text(item), value: item),
)
.toList(growable: false),
value: _placeTypesFilter.isEmpty ? null : _placeTypesFilter[0],
onChanged: _onPlaceTypeFilterChanged,
),
// -- Location Bias
_buildEnabledOption(
_locationBiasEnabled,
(value) => _locationBiasEnabled = value,
LocationField(
label: "Location Bias",
enabled: _locationBiasEnabled,
value: _locationBias,
onChanged: (bounds) {
setState(() {
_locationBias = bounds;
});
},
),
),
// -- Location Restrictions
_buildEnabledOption(
_locationRestrictionEnabled,
(value) => _locationRestrictionEnabled = value,
LocationField(
label: "Location Restriction",
enabled: _locationRestrictionEnabled,
value: _locationRestriction,
onChanged: (bounds) {
setState(() {
_locationRestriction = bounds;
});
},
),
),
// -- Predict
ElevatedButton(
onPressed: _predicting == true ? null : _predict,
child: const Text('Predict'),
),
// -- Error widget + Result
_buildErrorWidget(_predictErr),
Column(
mainAxisSize: MainAxisSize.min,
children: (_predictions ?? [])
.map(_buildPredictionItem)
.toList(growable: false),
),
Image(image: FlutterGooglePlacesSdk.assetPoweredByGoogleOnWhite),
];
}
// ===== Search by Text =====
void _searchByText() async {
if (_searchingByText) return;
final hasContent = _searchByTextLastQuery?.isNotEmpty ?? false;
setState(() {
_searchingByText = hasContent;
_searchByTextErr = null;
});
if (!hasContent) return;
try {
final result = await _places.searchByText(
_searchByTextLastQuery!,
fields: PlaceField.values,
includedType: _searchByTextIncludedType,
maxResultCount: _searchByTextMaxResults,
minRating: _searchByTextMinRating,
openNow: _searchByTextOpenNow ? true : null,
strictTypeFiltering: _searchByTextStrictTypeFiltering ? true : null,
rankPreference: _searchByTextRankPreference,
locationBias: _searchByTextLocationBiasEnabled
? _searchByTextLocationBias
: null,
);
setState(() {
_searchByTextResults = result.places;
_searchingByText = false;
});
} catch (err) {
setState(() {
_searchByTextErr = err;
_searchingByText = false;
});
}
}
List<Widget> _buildSearchByTextWidgets() {
return [
Text('Search by Text', style: Theme.of(context).textTheme.titleMedium),
// -- Text query
TextFormField(
onChanged: (value) => _searchByTextLastQuery = value,
decoration: InputDecoration(label: Text("Text Query")),
),
// -- Included Type
TextFormField(
onChanged: (value) =>
_searchByTextIncludedType = value.isEmpty ? null : value,
decoration: InputDecoration(
label: Text("Included Type"),
hintText: "e.g. restaurant",
),
),
// -- Max Result Count
TextFormField(
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (value) => _searchByTextMaxResults = value.isEmpty
? null
: int.tryParse(value),
decoration: InputDecoration(label: Text("Max Results")),
),
// -- Min Rating
TextFormField(
keyboardType: TextInputType.numberWithOptions(decimal: true),
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[\d.]'))],
onChanged: (value) => _searchByTextMinRating = value.isEmpty
? null
: double.tryParse(value),
decoration: InputDecoration(label: Text("Min Rating (0-5)")),
),
// -- Open Now
Row(
children: [
Checkbox(
value: _searchByTextOpenNow,
onChanged: (value) {
setState(() => _searchByTextOpenNow = value ?? false);
},
),
Text('Open Now'),
],
),
// -- Strict Type Filtering
Row(
children: [
Checkbox(
value: _searchByTextStrictTypeFiltering,
onChanged: (value) {
setState(() => _searchByTextStrictTypeFiltering = value ?? false);
},
),
Text('Strict Type Filtering'),
],
),
// -- Rank Preference
DropdownButton<TextSearchRankPreference?>(
hint: Text("Rank Preference"),
items: [
DropdownMenuItem(value: null, child: Text('None')),
...TextSearchRankPreference.values.map(
(item) => DropdownMenuItem(value: item, child: Text(item.name)),
),
],
value: _searchByTextRankPreference,
onChanged: (value) {
setState(() => _searchByTextRankPreference = value);
},
),
// -- Location Bias
_buildEnabledOption(
_searchByTextLocationBiasEnabled,
(value) => _searchByTextLocationBiasEnabled = value,
LocationField(
label: "Location Bias",
enabled: _searchByTextLocationBiasEnabled,
value: _searchByTextLocationBias,
onChanged: (bounds) {
setState(() => _searchByTextLocationBias = bounds);
},
),
),
// -- Search button
ElevatedButton(
onPressed: _searchingByText ? null : _searchByText,
child: const Text('Search by Text'),
),
// -- Error + Results
_buildErrorWidget(_searchByTextErr),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: (_searchByTextResults ?? [])
.map(
(place) => Padding(
padding: EdgeInsets.symmetric(vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
place.displayName?.text ?? place.name ?? 'N/A',
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(place.address ?? ''),
if (place.rating != null) Text('Rating: ${place.rating}'),
const Divider(thickness: 2),
],
),
),
)
.toList(growable: false),
),
];
}
// ===== Search Nearby =====
void _searchNearby() async {
if (_searchingNearby) return;
setState(() {
_searchingNearby = true;
_searchNearbyErr = null;
});
try {
final result = await _places.searchNearby(
fields: PlaceField.values,
locationRestriction: CircularBounds(
center: _searchNearbyCenter,
radius: _searchNearbyRadius,
),
includedTypes: _searchNearbyIncludedTypes?.isNotEmpty == true
? _searchNearbyIncludedTypes!
.split(',')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toList()
: null,
maxResultCount: _searchNearbyMaxResults,
rankPreference: _searchNearbyRankPreference,
);
setState(() {
_searchNearbyResults = result.places;
_searchingNearby = false;
});
} catch (err) {
setState(() {
_searchNearbyErr = err;
_searchingNearby = false;
});
}
}
List<Widget> _buildSearchNearbyWidgets() {
return [
Text('Search Nearby', style: Theme.of(context).textTheme.titleMedium),
// -- Center Lat
Row(
children: [
Flexible(
child: TextFormField(
keyboardType: TextInputType.numberWithOptions(
signed: true,
decimal: true,
),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[\d.\-]')),
],
initialValue: _searchNearbyCenter.lat.toString(),
onChanged: (value) {
final lat = double.tryParse(value);
if (lat != null) {
_searchNearbyCenter = LatLng(
lat: lat,
lng: _searchNearbyCenter.lng,
);
}
},
decoration: InputDecoration(label: Text("Center Lat")),
),
),
SizedBox(width: 8),
Flexible(
child: TextFormField(
keyboardType: TextInputType.numberWithOptions(
signed: true,
decimal: true,
),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[\d.\-]')),
],
initialValue: _searchNearbyCenter.lng.toString(),
onChanged: (value) {
final lng = double.tryParse(value);
if (lng != null) {
_searchNearbyCenter = LatLng(
lat: _searchNearbyCenter.lat,
lng: lng,
);
}
},
decoration: InputDecoration(label: Text("Center Lng")),
),
),
],
),
// -- Radius
TextFormField(
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[\d.]'))],
initialValue: _searchNearbyRadius.toString(),
onChanged: (value) {
final radius = double.tryParse(value);
if (radius != null) _searchNearbyRadius = radius;
},
decoration: InputDecoration(label: Text("Radius (meters)")),
),
// -- Included Types
TextFormField(
onChanged: (value) =>
_searchNearbyIncludedTypes = value.isEmpty ? null : value,
decoration: InputDecoration(
label: Text("Included Types"),
hintText: "e.g. restaurant,cafe",
),
),
// -- Max Result Count
TextFormField(
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (value) => _searchNearbyMaxResults = value.isEmpty
? null
: int.tryParse(value),
decoration: InputDecoration(label: Text("Max Results")),
),
// -- Rank Preference
DropdownButton<NearbySearchRankPreference?>(
hint: Text("Rank Preference"),
items: [
DropdownMenuItem(value: null, child: Text('None')),
...NearbySearchRankPreference.values.map(
(item) => DropdownMenuItem(value: item, child: Text(item.name)),
),
],
value: _searchNearbyRankPreference,
onChanged: (value) {
setState(() => _searchNearbyRankPreference = value);
},
),
// -- Search button
ElevatedButton(
onPressed: _searchingNearby ? null : _searchNearby,
child: const Text('Search Nearby'),
),
// -- Error + Results
_buildErrorWidget(_searchNearbyErr),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: (_searchNearbyResults ?? [])
.map(
(place) => Padding(
padding: EdgeInsets.symmetric(vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
place.displayName?.text ?? place.name ?? 'N/A',
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(place.address ?? ''),
if (place.rating != null) Text('Rating: ${place.rating}'),
if (place.types != null)
Text(
'Types: ${place.types!.map((t) => t.name).join(", ")}',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
const Divider(thickness: 2),
],
),
),
)
.toList(growable: false),
),
];
}
Widget _buildPhotoWidget(FetchPlacePhotoResponse? placePhoto) {
if (placePhoto == null) {
return Container();
}
return gpi.GooglePlacesImg(
photoMetadata: _placePhotoMetadata!,
placePhotoResponse: placePhoto,
);
}
void _openSettingsModal() {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SettingsPage(_places)),
);
}
}
/// Callback function with LatLngBounds
typedef void ActionWithBounds(LatLngBounds);
/// Location widget used to display and edit a LatLngBounds type
class LocationField extends StatefulWidget {
/// Label associated with this field
final String label;
/// If true the field is enabled. If false it is disabled and user can not interact with it
/// Value is retained even when the field is disabled
final bool enabled;
/// The current value in the field
final LatLngBounds value;
/// Callback for when the value has changed by the user.
final ActionWithBounds onChanged;
/// Create a LocationField
const LocationField({
Key? key,
required this.label,
required this.enabled,
required this.value,
required this.onChanged,
}) : super(key: key);
@override
State<StatefulWidget> createState() => _LocationFieldState();
}
class _LocationFieldState extends State<LocationField> {
late TextEditingController _ctrlNeLat;
late TextEditingController _ctrlNeLng;
late TextEditingController _ctrlSwLat;
late TextEditingController _ctrlSwLng;
@override
void initState() {
super.initState();
_ctrlNeLat = TextEditingController.fromValue(
TextEditingValue(text: widget.value.northeast.lat.toString()),
);
_ctrlNeLng = TextEditingController.fromValue(
TextEditingValue(text: widget.value.northeast.lng.toString()),
);
_ctrlSwLat = TextEditingController.fromValue(
TextEditingValue(text: widget.value.southwest.lat.toString()),
);
_ctrlSwLng = TextEditingController.fromValue(
TextEditingValue(text: widget.value.southwest.lng.toString()),
);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(5.0),
child: InputDecorator(
decoration: InputDecoration(
enabled: widget.enabled,
labelText: widget.label,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10.0)),
),
child: Row(
children: [
_buildField("NE/Lat", _ctrlNeLat),
_buildField("NE/Lng", _ctrlNeLng),
_buildField("SW/Lat", _ctrlSwLat),
_buildField("SW/Lng", _ctrlSwLng),
],
),
),
);
}
Widget _buildField(String label, TextEditingController controller) {
return Flexible(
child: TextFormField(
enabled: widget.enabled,
keyboardType: TextInputType.numberWithOptions(
signed: true,
decimal: true,
),
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[\d.]'))],
onChanged: (value) => _onValueChanged(controller, value),
decoration: InputDecoration(label: Text(label)),
// validator: _boundsValidator,
// autovalidateMode: AutovalidateMode.onUserInteraction,
controller: controller,
),
);
}
void _onValueChanged(TextEditingController ctrlNELat, String value) {
final neLat = double.parse(ctrlNELat.value.text);
LatLngBounds bounds = LatLngBounds(
southwest: LatLng(lat: 0.0, lng: 0.0),
northeast: LatLng(lat: neLat, lng: 0.0),
);
widget.onChanged(bounds);
}
}
/// Creates a web-selectable text widget.
///
/// If the platform is web, the widget created is [SelectableText].
/// Otherwise, it's a [Text].
class WebSelectableText extends StatelessWidget {
/// The text to display.
///
/// This will be null if a [textSpan] is provided instead.
final String data;
/// Creates a web-selectable text widget.
///
/// If the platform is web, the widget created is [SelectableText].
/// Otherwise, it's a [Text].
const WebSelectableText(this.data, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
if (kIsWeb) {
return SelectableText(data);
}
return Text(data);
}
}