From fc41bec906464242245b1dc8395884a85a27fece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Ali=20Ustao=C4=9Flu?= Date: Sat, 24 Oct 2026 21:08:08 +0200 Subject: [PATCH 1/2] geolocator_watchos: upstream's example, the Baseflow demo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The example was a hand-written demo, though the README called it the upstream Baseflow demo "ported verbatim". AUTHORING.md §3 asks for upstream's own example, so it is now geolocator's example app, ported with `flutter-watchos plugin port --include-example` (#24). Its code is unchanged; two listed watchOS deviations: - example/pubspec.yaml adds url_launcher_watchos, so the template's links open on the watch. - The runner's Info.plist sets FlutterWatchOSContentScale to 0.6. The demo is laid out for a phone and overflowed a 46 mm watch screen by 106 pixels; the scale fits it without touching the Dart code. The package's LICENSE is Baseflow's MIT licence, which covers the demo. geolocator ships no integration test. On the watchOS 27.0 Simulator, built with flutter.inspector.structuredErrors=false so framework errors reach the log (spec 0006 criterion 8a), the example starts with no exception in its first 15 seconds and no overflow, and reports the location service enabled. 0.2.0 is not published yet (#16), so this joins its CHANGELOG entry. --- packages/geolocator_watchos/CHANGELOG.md | 7 + packages/geolocator_watchos/PORTING_REPORT.md | 12 +- packages/geolocator_watchos/README.md | 20 +- .../geolocator_watchos/example/lib/main.dart | 447 ++++++++++++++++-- .../geolocator_watchos/example/pubspec.yaml | 5 + .../example/watchos/Runner/Info.plist | 2 + 6 files changed, 447 insertions(+), 46 deletions(-) diff --git a/packages/geolocator_watchos/CHANGELOG.md b/packages/geolocator_watchos/CHANGELOG.md index 22d9875..38d87ea 100644 --- a/packages/geolocator_watchos/CHANGELOG.md +++ b/packages/geolocator_watchos/CHANGELOG.md @@ -51,6 +51,13 @@ filter, as geolocator_apple's does: a reduced-accuracy fix can legitimately be old. +**Example** + +* `geolocator`'s own example app, the Baseflow demo, replaces a hand-written + demo (`AUTHORING.md` §3). Its code is unchanged; the example adds + `url_launcher_watchos`, so the template's links open on the watch, and + scales its content by 0.6 so the phone layout fits the watch screen. + **Docs** * README and PORTING_REPORT: the three members move out of "Not supported on diff --git a/packages/geolocator_watchos/PORTING_REPORT.md b/packages/geolocator_watchos/PORTING_REPORT.md index 33f1c61..7571f30 100644 --- a/packages/geolocator_watchos/PORTING_REPORT.md +++ b/packages/geolocator_watchos/PORTING_REPORT.md @@ -7,7 +7,7 @@ | Implementation | ✅ Working (CoreLocation FFI) | | watchOS capability | Full — query, permission, accuracy, service status, and position stream. Accuracy, service status and the `AppleSettings` fields are checked by host tests; the temporary full-accuracy prompt and background updates are not yet checked on a physical watch | | Host unit tests (`flutter-watchos test`) | ✅ pass | -| Upstream integration test | ○ none upstream (Baseflow manual demo) — verified by building + running the example on-sim | +| Upstream integration test | ○ none upstream (Baseflow manual demo) — verified by building + running the example on-sim (watchOS 27.0 Simulator, 2026-10-24) | | Internal unified demo | ✅ included | Marking: ✅ full / passes · ◐ partial — reason given · ○ not applicable (no upstream test) · ✗ unsupported on watchOS. @@ -21,6 +21,16 @@ Source: `geolocator_apple` 2.3.14 Base platform: ios (Objective-C) Output: `./geolocator_watchos` +## Example + +`geolocator`'s own example app, the Baseflow demo, ported with +`flutter-watchos plugin port --include-example`. It replaces a hand-written +demo (`AUTHORING.md` §3). Its code is unchanged; the README lists its two +watchOS deviations: `url_launcher_watchos` in the example's pubspec, so the +template's links open on the watch, and `FlutterWatchOSContentScale` 0.6 in +the runner's `Info.plist`, so the phone layout fits the watch screen. The +package's `LICENSE` is Baseflow's MIT licence, which covers the demo. + ## This is an FFI scaffold Method-channel plugins are not supported on watchOS — a `pluginClass:`-only implementation builds, but its channel calls throw `MissingPluginException`. The supported plugin model is **dart:ffi**, so this package is an FFI scaffold, not a copy of the source plugin's native code: diff --git a/packages/geolocator_watchos/README.md b/packages/geolocator_watchos/README.md index 3efd347..fec4655 100644 --- a/packages/geolocator_watchos/README.md +++ b/packages/geolocator_watchos/README.md @@ -97,10 +97,22 @@ updates with the wrist down. | Apple Watch (`watchos`) | yes | | Watch simulator (`watchsimulator`) | query methods verified; live fixes need a simulated location | -The example is geolocator's own upstream Baseflow demo, ported verbatim. That -demo ships no `integration_test/`, so this package is verified by building and -running the example on the watch simulator; the interactive permission prompt -and live position updates are verified on a physical Apple Watch. +The interactive permission prompt and live position updates are verified on +a physical Apple Watch. + +## Example + +The example is `geolocator`'s own example app, the Baseflow demo, ported with +`flutter-watchos plugin port --include-example`. Its code is unchanged; these +are its watchOS deviations: + +| Where | Change | Why | +|---|---|---| +| `example/pubspec.yaml` | adds `url_launcher_watchos: ^0.1.1` | the Baseflow template opens its links with `url_launcher`, which needs it to open them on the watch | +| `example/watchos/Runner/Info.plist` | sets `FlutterWatchOSContentScale` to `0.6` | the demo is designed for a phone and overflowed the watch screen by 106 pixels on a 46 mm Simulator; the scale lays it out in a logical space 1.67 times the screen's size and draws it smaller, without touching the Dart code | + +The demo ships no `integration_test/`, so this package is verified by building +and running the example on the watch simulator. ## Not supported on watchOS diff --git a/packages/geolocator_watchos/example/lib/main.dart b/packages/geolocator_watchos/example/lib/main.dart index 3cbf194..5f4ec44 100644 --- a/packages/geolocator_watchos/example/lib/main.dart +++ b/packages/geolocator_watchos/example/lib/main.dart @@ -1,64 +1,429 @@ -// Copyright 2026 The FlutterWatch Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +import 'dart:async'; +import 'dart:io' show Platform; +import 'package:baseflow_plugin_template/baseflow_plugin_template.dart'; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; -void main() => runApp(const ExampleApp()); +/// Defines the main theme color. +final MaterialColor themeMaterialColor = + BaseflowPluginExample.createMaterialColor( + const Color.fromRGBO(48, 49, 60, 1)); -class ExampleApp extends StatelessWidget { - const ExampleApp({super.key}); +void main() { + runApp(const GeolocatorWidget()); +} - @override - Widget build(BuildContext context) { - return const MaterialApp(home: GeolocatorDemo()); +/// Example [Widget] showing the functionalities of the geolocator plugin +class GeolocatorWidget extends StatefulWidget { + /// Creates a new GeolocatorWidget. + const GeolocatorWidget({super.key}); + + /// Utility method to create a page with the Baseflow templating. + static ExamplePage createPage() { + return ExamplePage( + Icons.location_on, (context) => const GeolocatorWidget()); } + + @override + State createState() => _GeolocatorWidgetState(); } -class GeolocatorDemo extends StatefulWidget { - const GeolocatorDemo({super.key}); +class _GeolocatorWidgetState extends State { + static const String _kLocationServicesDisabledMessage = + 'Location services are disabled.'; + static const String _kPermissionDeniedMessage = 'Permission denied.'; + static const String _kPermissionDeniedForeverMessage = + 'Permission denied forever.'; + static const String _kPermissionGrantedMessage = 'Permission granted.'; + + final GeolocatorPlatform _geolocatorPlatform = GeolocatorPlatform.instance; + final List<_PositionItem> _positionItems = <_PositionItem>[]; + StreamSubscription? _positionStreamSubscription; + StreamSubscription? _serviceStatusStreamSubscription; + bool positionStreamStarted = false; @override - State createState() => _GeolocatorDemoState(); -} + void initState() { + super.initState(); + _toggleServiceStatusStream(); + } + + PopupMenuButton _createActions() { + return PopupMenuButton( + elevation: 40, + onSelected: (value) async { + switch (value) { + case 1: + _getLocationAccuracy(); + break; + case 2: + _requestTemporaryFullAccuracy(); + break; + case 3: + _openAppSettings(); + break; + case 4: + _openLocationSettings(); + break; + case 5: + setState(_positionItems.clear); + break; + default: + break; + } + }, + itemBuilder: (context) => [ + if (Platform.isIOS) + const PopupMenuItem( + value: 1, + child: Text("Get Location Accuracy"), + ), + if (Platform.isIOS) + const PopupMenuItem( + value: 2, + child: Text("Request Temporary Full Accuracy"), + ), + const PopupMenuItem( + value: 3, + child: Text("Open App Settings"), + ), + if (Platform.isAndroid || Platform.isWindows) + const PopupMenuItem( + value: 4, + child: Text("Open Location Settings"), + ), + const PopupMenuItem( + value: 5, + child: Text("Clear"), + ), + ], + ); + } -class _GeolocatorDemoState extends State { - String _status = 'tap to locate'; + @override + Widget build(BuildContext context) { + const sizedBox = SizedBox( + height: 10, + ); - Future _run() async { - setState(() => _status = 'locating…'); - try { - LocationPermission permission = await Geolocator.checkPermission(); + return BaseflowPluginExample( + pluginName: 'Geolocator', + githubURL: 'https://github.com/Baseflow/flutter-geolocator', + pubDevURL: 'https://pub.dev/packages/geolocator', + appBarActions: [ + _createActions() + ], + pages: [ + ExamplePage( + Icons.location_on, + (context) => Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + body: ListView.builder( + itemCount: _positionItems.length, + itemBuilder: (context, index) { + final positionItem = _positionItems[index]; + + if (positionItem.type == _PositionItemType.log) { + return ListTile( + title: Text(positionItem.displayValue, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + )), + ); + } else { + return Card( + child: ListTile( + tileColor: themeMaterialColor, + title: Text( + positionItem.displayValue, + style: const TextStyle(color: Colors.white), + ), + ), + ); + } + }, + ), + floatingActionButton: Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FloatingActionButton( + onPressed: () { + positionStreamStarted = !positionStreamStarted; + _toggleListening(); + }, + tooltip: (_positionStreamSubscription == null) + ? 'Start position updates' + : _positionStreamSubscription!.isPaused + ? 'Resume' + : 'Pause', + backgroundColor: _determineButtonColor(), + child: (_positionStreamSubscription == null || + _positionStreamSubscription!.isPaused) + ? const Icon(Icons.play_arrow) + : const Icon(Icons.pause), + ), + sizedBox, + FloatingActionButton( + onPressed: _getCurrentPosition, + child: const Icon(Icons.my_location), + ), + sizedBox, + FloatingActionButton( + onPressed: _getLastKnownPosition, + child: const Icon(Icons.bookmark), + ), + ], + ), + ), + ) + ]); + } + + Future _getCurrentPosition() async { + final hasPermission = await _handlePermission(); + + if (!hasPermission) { + return; + } + + final position = await _geolocatorPlatform.getCurrentPosition(); + _updatePositionList( + _PositionItemType.position, + position.toString(), + ); + } + + Future _handlePermission() async { + bool serviceEnabled; + LocationPermission permission; + + // Test if location services are enabled. + serviceEnabled = await _geolocatorPlatform.isLocationServiceEnabled(); + if (!serviceEnabled) { + // Location services are not enabled don't continue + // accessing the position and request users of the + // App to enable the location services. + _updatePositionList( + _PositionItemType.log, + _kLocationServicesDisabledMessage, + ); + + return false; + } + + permission = await _geolocatorPlatform.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await _geolocatorPlatform.requestPermission(); if (permission == LocationPermission.denied) { - permission = await Geolocator.requestPermission(); + // Permissions are denied, next time you could try + // requesting permissions again (this is also where + // Android's shouldShowRequestPermissionRationale + // returned true. According to Android guidelines + // your App should show an explanatory UI now. + _updatePositionList( + _PositionItemType.log, + _kPermissionDeniedMessage, + ); + + return false; } - if (permission == LocationPermission.denied || - permission == LocationPermission.deniedForever) { - setState(() => _status = 'permission: $permission'); + } + + if (permission == LocationPermission.deniedForever) { + // Permissions are denied forever, handle appropriately. + _updatePositionList( + _PositionItemType.log, + _kPermissionDeniedForeverMessage, + ); + + return false; + } + + // When we reach here, permissions are granted and we can + // continue accessing the position of the device. + _updatePositionList( + _PositionItemType.log, + _kPermissionGrantedMessage, + ); + return true; + } + + void _updatePositionList(_PositionItemType type, String displayValue) { + _positionItems.add(_PositionItem(type, displayValue)); + setState(() {}); + } + + bool _isListening() => !(_positionStreamSubscription == null || + _positionStreamSubscription!.isPaused); + + Color _determineButtonColor() { + return _isListening() ? Colors.green : Colors.red; + } + + void _toggleServiceStatusStream() { + if (_serviceStatusStreamSubscription == null) { + final serviceStatusStream = _geolocatorPlatform.getServiceStatusStream(); + _serviceStatusStreamSubscription = + serviceStatusStream.handleError((error) { + _serviceStatusStreamSubscription?.cancel(); + _serviceStatusStreamSubscription = null; + }).listen((serviceStatus) { + String serviceStatusValue; + if (serviceStatus == ServiceStatus.enabled) { + if (positionStreamStarted) { + _toggleListening(); + } + serviceStatusValue = 'enabled'; + } else { + if (_positionStreamSubscription != null) { + setState(() { + _positionStreamSubscription?.cancel(); + _positionStreamSubscription = null; + _updatePositionList( + _PositionItemType.log, 'Position Stream has been canceled'); + }); + } + serviceStatusValue = 'disabled'; + } + _updatePositionList( + _PositionItemType.log, + 'Location service has been $serviceStatusValue', + ); + }); + } + } + + void _toggleListening() { + if (_positionStreamSubscription == null) { + final positionStream = _geolocatorPlatform.getPositionStream(); + _positionStreamSubscription = positionStream.handleError((error) { + _positionStreamSubscription?.cancel(); + _positionStreamSubscription = null; + }).listen((position) => _updatePositionList( + _PositionItemType.position, + position.toString(), + )); + _positionStreamSubscription?.pause(); + } + + setState(() { + if (_positionStreamSubscription == null) { return; } - final Position pos = await Geolocator.getCurrentPosition(); - setState(() => _status = '${pos.latitude.toStringAsFixed(4)}, ' - '${pos.longitude.toStringAsFixed(4)}'); - } catch (e) { - setState(() => _status = 'error: $e'); - } + + String statusDisplayValue; + if (_positionStreamSubscription!.isPaused) { + _positionStreamSubscription!.resume(); + statusDisplayValue = 'resumed'; + } else { + _positionStreamSubscription!.pause(); + statusDisplayValue = 'paused'; + } + + _updatePositionList( + _PositionItemType.log, + 'Listening for position updates $statusDisplayValue', + ); + }); } @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(_status, textAlign: TextAlign.center), - const SizedBox(height: 8), - ElevatedButton(onPressed: _run, child: const Text('Locate')), - ], - ), - ), + void dispose() { + if (_positionStreamSubscription != null) { + _positionStreamSubscription!.cancel(); + _positionStreamSubscription = null; + } + + super.dispose(); + } + + void _getLastKnownPosition() async { + final position = await _geolocatorPlatform.getLastKnownPosition(); + if (position != null) { + _updatePositionList( + _PositionItemType.position, + position.toString(), + ); + } else { + _updatePositionList( + _PositionItemType.log, + 'No last known position available', + ); + } + } + + void _getLocationAccuracy() async { + final status = await _geolocatorPlatform.getLocationAccuracy(); + _handleLocationAccuracyStatus(status); + } + + void _requestTemporaryFullAccuracy() async { + final status = await _geolocatorPlatform.requestTemporaryFullAccuracy( + purposeKey: "TemporaryPreciseAccuracy", ); + _handleLocationAccuracyStatus(status); } + + void _handleLocationAccuracyStatus(LocationAccuracyStatus status) { + String locationAccuracyStatusValue; + if (status == LocationAccuracyStatus.precise) { + locationAccuracyStatusValue = 'Precise'; + } else if (status == LocationAccuracyStatus.reduced) { + locationAccuracyStatusValue = 'Reduced'; + } else { + locationAccuracyStatusValue = 'Unknown'; + } + _updatePositionList( + _PositionItemType.log, + '$locationAccuracyStatusValue location accuracy granted.', + ); + } + + void _openAppSettings() async { + final opened = await _geolocatorPlatform.openAppSettings(); + String displayValue; + + if (opened) { + displayValue = 'Opened Application Settings.'; + } else { + displayValue = 'Error opening Application Settings.'; + } + + _updatePositionList( + _PositionItemType.log, + displayValue, + ); + } + + void _openLocationSettings() async { + final opened = await _geolocatorPlatform.openLocationSettings(); + String displayValue; + + if (opened) { + displayValue = 'Opened Location Settings'; + } else { + displayValue = 'Error opening Location Settings'; + } + + _updatePositionList( + _PositionItemType.log, + displayValue, + ); + } +} + +enum _PositionItemType { + log, + position, +} + +class _PositionItem { + _PositionItem(this.type, this.displayValue); + + final _PositionItemType type; + final String displayValue; } diff --git a/packages/geolocator_watchos/example/pubspec.yaml b/packages/geolocator_watchos/example/pubspec.yaml index 1096b87..6331644 100644 --- a/packages/geolocator_watchos/example/pubspec.yaml +++ b/packages/geolocator_watchos/example/pubspec.yaml @@ -12,6 +12,11 @@ dependencies: geolocator: ^14.1.1 geolocator_watchos: path: ../ + # Upstream's example is built on the Baseflow template. + baseflow_plugin_template: ^2.1.2 + # The template's links open with url_launcher; this lets them open on the + # watch (a listed deviation, see the README). + url_launcher_watchos: ^0.1.1 dev_dependencies: flutter_test: diff --git a/packages/geolocator_watchos/example/watchos/Runner/Info.plist b/packages/geolocator_watchos/example/watchos/Runner/Info.plist index 1c0d002..139d364 100644 --- a/packages/geolocator_watchos/example/watchos/Runner/Info.plist +++ b/packages/geolocator_watchos/example/watchos/Runner/Info.plist @@ -20,6 +20,8 @@ $(FLUTTER_BUILD_NAME) CFBundleVersion $(FLUTTER_BUILD_NUMBER) + FlutterWatchOSContentScale + 0.6 NSLocationWhenInUseUsageDescription This example reads your location to demonstrate geolocator on the watch. WKApplication From 3413dbcd1b0722914a0336d3b0ffb0f3c07c9cd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Ali=20Ustao=C4=9Flu?= Date: Sat, 24 Oct 2026 22:15:02 +0200 Subject: [PATCH 2/2] geolocator_watchos: README says which version ran on a physical watch The permission prompt and live updates were verified on a physical Apple Watch with 0.1.x; 0.2.0 has run only on the Simulator so far (#16). The README is published with 0.2.0, so it says so. --- packages/geolocator_watchos/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/geolocator_watchos/README.md b/packages/geolocator_watchos/README.md index fec4655..96105f2 100644 --- a/packages/geolocator_watchos/README.md +++ b/packages/geolocator_watchos/README.md @@ -97,8 +97,9 @@ updates with the wrist down. | Apple Watch (`watchos`) | yes | | Watch simulator (`watchsimulator`) | query methods verified; live fixes need a simulated location | -The interactive permission prompt and live position updates are verified on -a physical Apple Watch. +The interactive permission prompt and live position updates were verified on +a physical Apple Watch with 0.1.x. 0.2.0 has run only on the Simulator so far +([#16](https://github.com/flutterwatch/plugins/issues/16)). ## Example