one_tap_location 0.1.0
one_tap_location: ^0.1.0 copied to clipboard
Gets the location with one tap on the Android 17 system location button or iOS CLLocationButton, with a fallback on Android 7 to 16. Required by the Google Play location policy.
one_tap_location #

A Flutter widget that gets the user's location with a single tap on the system location button.
The system draws the button and asks for consent, so the app shows no permission dialog of its own. The first tap shows a system confirmation; once the user allows access, later taps grant one-time access right away.
| Platform | Button |
|---|---|
| iOS 15+ | CLLocationButton |
| Android 17+ | The system location button |
| Android 7–16 | A fallback button that asks with the permission dialog |
On other platforms the widget renders nothing and reports no results. In
iPhone and iPad apps on Apple Vision Pro, CLLocationButton does not respond to
taps; with visionOS 26.1 or later, the widget renders nothing there either. Such
apps need their own way to get a location; see Availability.
Each tap reports one position. The widget does not track the location, work in the background or locate without a tap.
Google Play has announced that apps that target Android 17 or later and need precise location only for one-time actions that the user starts, such as filling in an address or searching nearby, will have to use the system location button and restrict precise location to it. See Google Play location policy for the date and the manifest changes.
Usage #
class _AddressFormState extends State<AddressForm> {
final OneTapLocationController _controller = OneTapLocationController();
late final StreamSubscription<OneTapLocationDiagnostic>
_diagnosticSubscription;
String _status = '';
@override
void initState() {
super.initState();
_diagnosticSubscription = _controller.diagnostics.listen((diagnostic) {
setState(() => _status = 'Location access was not granted.');
});
}
@override
void dispose() {
_diagnosticSubscription.cancel();
_controller.dispose();
super.dispose();
}
void _handleResult(OneTapLocationResult result) {
setState(() {
_status = switch (result) {
OneTapLocationGranted(:final position) =>
'${position.latitude}, ${position.longitude}',
OneTapLocationDenied() => 'Location access was denied.',
OneTapLocationFailed(:final exception)
when exception.code ==
OneTapLocationErrorCode.locationServicesDisabled =>
'Turn on location services to share your location.',
OneTapLocationFailed() => 'The location could not be determined.',
};
});
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
OneTapLocationButton(controller: _controller, onResult: _handleResult),
Text(_status),
],
);
}
}
A result is reported only once the outcome is known. While the position is
being determined after access was granted, OneTapLocationController.isLocating
is true. When location services are turned off on the device, the result is
OneTapLocationFailed with OneTapLocationErrorCode.locationServicesDisabled,
so the app can ask the user to turn them on; neither platform shows a prompt
of its own for this case.
position.altitude is the altitude above mean sea level. It is null when
position.isPrecise is false, on Android 13 and earlier, and whenever no valid
altitude was determined for the position.
Availability #
OneTapLocationButton.checkAvailability() tells which button the widget shows
on the device:
| Device | buttonType |
|---|---|
| iOS | system |
| iPhone and iPad apps on Apple Vision Pro with visionOS 26.1 or later | none |
| Android 17 or later, where the system provides the location button | system |
| Android 7–16, and later versions whose system does not provide it | fallback |
| Web and other platforms | none |
Earlier versions of visionOS, and apps built with Xcode earlier than 26.1,
cannot detect that the app runs on Apple Vision Pro, so there buttonType is
system although the button does not respond; apps can turn off their
availability on Apple Vision Pro in App Store Connect.
isPreciseLocationRestrictedToSystemButton is true on Android 17 and later
when the app declares ACCESS_FINE_LOCATION with the onlyForLocationButton
flag. The flag restricts precise location to the system location button, so
the permission dialog that the fallback button shows offers approximate
location only:
final OneTapLocationAvailability availability =
await OneTapLocationButton.checkAvailability();
if (availability.buttonType == OneTapLocationButtonType.fallback &&
availability.isPreciseLocationRestrictedToSystemButton) {
// Precise location is not available on this device.
}
The availability does not change while the app runs, and the widget uses the
same answer to choose its button. It describes the device, not whether the
button can be shown: where the system fails to open the location button, the
widget reports the error to FlutterError.onError and shows nothing.
Size #
On iOS, the widget takes the size that the system requires for its label,
icon and font size. The system ignores taps on a smaller button, so the parent
must allow at least that size in both directions; in debug builds, a parent
that imposes a smaller size, such as a SizedBox 48 pixels tall, fails an
assertion.
On Android, the widget fills the width of its parent and is 48 logical pixels
tall, unless the parent requires another height between 48 and 136. The
system draws its label inside that area and cuts the label off when the area
is too narrow, so leave room for the label in every language the app
supports. With AndroidLocationButtonLabel.none, the button is a 48 × 48
square, unless the parent requires a larger size. The fallback button on
Android 7–16 has the same size.
Placement #
The system accepts a tap only while the button is fully visible.
On iOS, the system ignores the tap, without reporting an error, when the button is:
- partly scrolled out of view or clipped,
- translucent, including during a fade transition,
- scaled down,
- smaller than the size the system requires,
- covered by other content.
When a tap does not grant access within a few seconds, the controller reports
OneTapLocationDiagnostic.tapWithoutAuthorization. If access is granted
later, the result is still reported, unless another button was tapped in the
meantime.
On Android 17 and later, the system draws the button above all Flutter content, and Flutter cannot clip, fade or cover it. The widget therefore hides the button while:
- its route is not the current route, or a route transition runs,
- an ancestor clips it, including a scroll view that has scrolled it partly out of view,
- an ancestor lowers its opacity, as
OpacityandFadeTransitiondo, - an ancestor scales or rotates it, as
Transform,FittedBoxandScaleTransitioncan: the system ignores taps on a button that is not drawn at its own size, - an ancestor applies an image filter to it, as
ImageFiltereddoes, and asTransform,ScaleTransitionandAnimatedScaledo with afilterQuality, - it lies under the status bar, the navigation bar or the keyboard,
- content that keeps pointer events from reaching it covers it, such as a dialog, an open drawer, a bottom sheet, a menu or a snack bar.
Once hidden, the button reappears after it has stayed fully visible for a moment, so that standard dialogs, menus and sheets can finish their closing transitions; a route with a longer closing transition can still be visible when the button reappears. Covering content is found by hit testing points across the button, so content that lets pointer events through, such as a tooltip, or that is narrower than 48 logical pixels can go unnoticed; keep such content away from the button. While a scroll view scrolls, covering content cannot be checked: content that covered the button when scrolling started still hides it, content that moves over a shown button goes unnoticed, and a button that scrolls into view appears once scrolling has ended. Shader masks and color filters, such as those of a list that fades at its edges, neither apply to the system button nor hide it. The system also ignores taps for a moment after the button appears.
When the font size or the display size changes while the app runs, the widget replaces the system button, which is missing for a moment. A change of the display size also ends a request in progress without a result.
When the user closes the system confirmation without answering, the controller
reports OneTapLocationDiagnostic.tapWithoutAuthorization. The plugin tells
the confirmation apart from other screens that pause the app, such as a
permission dialog of another plugin, by the activity that the system opens;
where that activity cannot be identified, such screens report the diagnostic
as well. The diagnostic is not reported while several buttons are shown,
because the tapped one is unknown.
The fallback button on Android 7–16 is an ordinary widget, so none of these rules apply to it.
Android 7–16 #
Where the system location button is not available, on Android 7 through 16
and on devices that lack it, the widget shows a fallback button with the same
size and the same results. By default, the fallback button resembles the
system button: a rounded button with the location icon and the English text of
androidLabel. It uses backgroundColor, foregroundColor, cornerRadius
and the Android properties of the style; colors that are null come from
secondaryContainer and onSecondaryContainer of the app's color scheme. As
on the system button, an outline is drawn only when both androidStrokeColor
and androidStrokeWidth are set.
The system translates the text of its own button, but the fallback button shows its text as given. Apps that support other languages provide the text:
OneTapLocationButton(
style: OneTapLocationButtonStyle(
androidFallbackLabel: AppLocalizations.of(context)!.preciseLocation,
),
onResult: _handleResult,
)
To show a button of your own instead, use fallbackBuilder. The widget it
returns is given the size of the system button, and onPressed is null while
a request is in progress:
OneTapLocationButton(
fallbackBuilder: (BuildContext context, VoidCallback? onPressed) {
return FilledButton.icon(
onPressed: onPressed,
icon: const Icon(Icons.my_location),
label: const Text('Use my location'),
);
},
onResult: _handleResult,
)
A tap on the fallback button shows the permission dialog of the system, unless the app already has precise access, or has approximate access while precise location is restricted to the system button (see Availability):
- Access granted in the dialog lasts as long as the user chooses there, for example while the app is in use. On Android 11 and later, the user can also choose "Only this time": Android revokes that access some time after the app leaves the foreground and ends the app's process, and the next tap shows the dialog again.
- When the user allows only approximate location, the result is
OneTapLocationGrantedwithposition.isPreciseset to false. Android determines approximate positions at most every ten minutes, so the position can be that old;position.timestamptells when it was determined. When Android has no approximate position from the last ten minutes, it determines a new one:isLocatingstays true until it arrives, for up to 30 seconds, and the result isOneTapLocationFailedwithOneTapLocationErrorCode.locationUnavailableif none arrives in that time. The next tap offers to change to precise location, unless precise location is restricted to the system button (see Availability). When the user keeps approximate location there, Android can offer the change once more on the next tap; after that, taps report approximate positions without asking. - When the user denies access, the result is
OneTapLocationDenied. On Android 11 and later, once the user has denied access twice, the system denies later taps without showing the dialog; on Android 7 to 10, it does so once the user chooses not to be asked again. - On Android 11 and later, when the app has no location access, Android
reports a dialog that the user closes without answering, for example with
Back, as a denial, so the result is
OneTapLocationDeniedas well; this does not count as one of the two denials. When the app already has approximate access, closing the dialog keeps that access, and the tap continues as described for approximate location. On Android 7 to 10, Back does not close the dialog. - When the request is interrupted before the user answers, for example
because another permission request is in progress, the controller reports
OneTapLocationDiagnostic.tapWithoutAuthorization.
Testing #
Tests that run on the host, such as widget tests, do not run the platform code
of the plugin, so there the widget shows nothing that can be tapped, and
checkAvailability() cannot determine the availability on iOS and Android.
Install the fake from package:one_tap_location/testing.dart before pumping the
widgets:
late FakeOneTapLocation location;
setUp(() {
location = FakeOneTapLocation.install();
addTearDown(location.uninstall);
});
testWidgets('shows the coordinates after a tap', (WidgetTester tester) async {
location.result = OneTapLocationGranted(
OneTapPosition(
latitude: 41.0082,
longitude: 28.9784,
timestamp: DateTime.utc(2026, 9, 16),
isPrecise: true,
),
);
await tester.pumpWidget(const MaterialApp(home: AddressForm()));
await tester.tap(find.byType(OneTapLocationButton));
await tester.pump();
expect(find.text('41.0082, 28.9784'), findsOneWidget);
});
The fake follows the target platform of the test. On Android it shows the
fallback button, or a stand-in for the system button when
hasAndroidSystemButton is true; on iOS it shows a stand-in for
CLLocationButton with the size iosButtonSize, or nothing when
hasIOSSystemButton is false, as in iPhone and iPad apps on Apple Vision Pro
with visionOS 26.1 or later. The size does not follow the style: it defaults to
the size of the system button with the default style, so set it when the app
uses another label, icon or font size. Stand-ins resemble the fallback button,
so golden files show the layout of the app, not the look of the system buttons.
With a result, a tap reports it once the tap has been handled. Without one,
each tap adds a request to location.requests, oldest first, and the request
stays pending until the test answers it with complete or dismiss. Calling
grantAccess before complete lets the test check the app while the position
is determined. The button receives each of these calls right away; pump a frame
to see its effect.
Android setup #
The plugin requires Android 7 (API level 24) or later. An app whose minSdk
is flutter.minSdkVersion gets 24 from Flutter 3.44; a lower minSdk fails
the build when the manifests are merged.
The system location button is available on Android 17 (API level 37) and
later. The plugin compiles against API level 36, so an app can keep
compileSdk 36 unless it declares the onlyForLocationButton flag described
in Google Play location policy.
Declare the location permissions in android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
The plugin adds two entries to the manifest of every app that depends on it, including apps that show the widget only on iOS:
android.permission.USE_LOCATION_BUTTON, which the system button requires. Android grants this normal permission at install time without asking the user. None of the Google Play pages listed in Google Play location policy names it.- A
<queries>entry for the actionandroid.app.permissionui.action.REQUEST_LOCATION_BUTTON_PERMISSIONS, which lets the plugin recognize the system confirmation of the location button.
Removing these entries with tools:node="remove" is not supported.
Without ACCESS_FINE_LOCATION, the system button is not shown. The fallback
button needs both permissions: when either one is not declared, a tap reports a
PlatformException with the code missingPermission to
FlutterError.onError, and onResult is not called.
Access granted through the system button is one-time. About a minute after the app leaves the foreground, the system revokes it and ends the app's process, so save any state the app needs to restore. After the app is updated, the first tap can show the confirmation again even if the user allowed access before. Once the user has denied access twice, the system denies later taps without showing its confirmation, and the user has to allow location access for the app in the system settings.
Google Play location policy #
Google Play has announced location rules that take effect on January 27, 2027. This section summarizes the pages listed below as they read on September 16, 2026. The pages, not this summary, define the policy, and the date has moved before: in early August 2026, the preview gave October 28, 2026.
For apps that target Android 17 (API level 37) or later:
- An app that needs precise location only for one-time actions that the user
starts, such as searching nearby, sharing the location once, tagging content
with a location or filling in an address, must use the system location
button and declare
ACCESS_FINE_LOCATIONwith theonlyForLocationButtonflag. Updates of apps that do not comply may be rejected. ACCESS_FINE_LOCATIONwithout the flag is allowed only for a core, ongoing feature, such as turn-by-turn navigation, that neither the location button nor approximate location can serve.
The button rules apply once an app targets API level 37. Google Play has not
announced when it will require that target level. Apps created from the Flutter
template take targetSdk from the Flutter version that builds them, so
upgrading Flutter can raise it.
Two rules apply at any target API level. The policy allows
ACCESS_FINE_LOCATION only for features that approximate location cannot
serve; features that work with approximate location use
ACCESS_COARSE_LOCATION only. The help article also requires a Play Console
declaration, available from November 2026, from apps that request
ACCESS_FINE_LOCATION. The policy adds that apps requesting location
permissions, including the location button, go through a declaration process
and review. Neither page says whether an app that declares
ACCESS_FINE_LOCATION only with the flag has to complete the declaration.
An app in the first case declares in android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION"
android:usesPermissionFlags="onlyForLocationButton" />
It also sets compileSdk to 37 or later: the Android 16 SDK platforms do not
define the flag, and the build fails with an AAPT error. The Android Gradle
plugin supports API level 37 from version 9.1.1; the version 9.0.1 of the
Flutter 3.44 template builds but prints a warning. The help article shows
android:onlyForLocationButton="true" instead, but Android defines no attribute
of that name; the flag is a value of android:usesPermissionFlags.
With the flag, the system location button still grants precise location,
while the permission dialog offers approximate location only, whatever the
target API level of the app; Android 16 and earlier ignore the flag. On a
device with Android 17 or later whose system does not provide the location
button, the fallback button can therefore obtain approximate location at most;
isPreciseLocationRestrictedToSystemButton tells the app when the flag
applies.
Sources:
- Minimum Scope: Foreground Location Access and the Location Button
- Preview: Permissions and APIs that Access Sensitive Information, and its copy of August 2, 2026
- Target API level requirements for Google Play apps
- Request session-based location access with the location button
usesPermissionFlags
iOS setup #
The plugin requires iOS 15 or later. Set the iOS deployment target of the
Runner target to 15.0 and, when using CocoaPods, the platform in
ios/Podfile:
platform :ios, '15.0'
Add a location usage description to ios/Runner/Info.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>Your location is used to fill in your address.</string>
License #
BSD 3-Clause. See LICENSE.
