Skip to content

Commit c79219b

Browse files
authored
feat(geolocation): split get() into onSuccess/onError callbacks (#24270)
Geolocation.get() now takes a (onSuccess, onError) pair, mirroring GeolocationTracker.addPositionListener and the W3C getCurrentPosition(success, error) signature. The optional GeolocationOptions argument moves to the trailing position so that additional parameters can be added at the end without rearranging the common case. GeolocationOutcome is retained as the SPI sum-type returned by GeolocationClient#get; only the public facade changes shape.
1 parent d9290f8 commit c79219b

9 files changed

Lines changed: 131 additions & 115 deletions

File tree

‎flow-server/src/main/java/com/vaadin/flow/component/geolocation/Geolocation.java‎

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.vaadin.flow.component.geolocation;
1717

1818
import java.io.Serializable;
19+
import java.util.Objects;
1920

2021
import org.jspecify.annotations.Nullable;
2122
import org.slf4j.Logger;
@@ -41,11 +42,15 @@
4142
* <p>
4243
* <b>Two usage modes:</b>
4344
* <ul>
44-
* <li>{@link #get(SerializableConsumer)} — one-shot position request. Use this
45-
* when the application only needs to know the user's location at a single
46-
* moment (e.g. on a button click). The callback receives a
47-
* {@link GeolocationOutcome} — either a {@link GeolocationPosition} or a
48-
* {@link GeolocationError}.</li>
45+
* <li>{@link #get(SerializableConsumer, SerializableConsumer)} — one-shot
46+
* position request. Use this when the application only needs to know the user's
47+
* location at a single moment (e.g. on a button click). Takes a pair of
48+
* callbacks — one for a successful {@link GeolocationPosition}, one for a
49+
* {@link GeolocationError} — mirroring the W3C
50+
* {@code getCurrentPosition(success, error)} pair and matching
51+
* {@link GeolocationTracker#addPositionListener
52+
* GeolocationTracker.addPositionListener}. An overload accepts a trailing
53+
* {@link GeolocationOptions} for accuracy / timeout / cache-age tuning.</li>
4954
* <li>{@link #track(Component)} — continuous tracking that keeps the server
5055
* updated as the user moves. Returns a {@link GeolocationTracker} whose
5156
* {@link GeolocationTracker#valueSignal() valueSignal()} is a reactive signal
@@ -75,13 +80,10 @@
7580
* <pre>
7681
* Button locate = new Button("Use my location");
7782
* locate.addClickListener(
78-
* e -&gt; UI.getCurrent().getGeolocation().get(outcome -&gt; {
79-
* switch (outcome) {
80-
* case GeolocationPosition pos -&gt; showNearest(
81-
* pos.coords().latitude(), pos.coords().longitude());
82-
* case GeolocationError err -&gt; showManualEntry();
83-
* }
84-
* }));
83+
* e -&gt; e.getUI().getGeolocation()
84+
* .get(pos -&gt; showNearest(pos.coords().latitude(),
85+
* pos.coords().longitude()),
86+
* err -&gt; showManualEntry()));
8587
* </pre>
8688
*
8789
* <p>
@@ -176,21 +178,29 @@ private static GeolocationClient resolveClient(UI ui) {
176178
}
177179

178180
/**
179-
* Requests the user's current position once. The callback receives a
180-
* {@link GeolocationOutcome} — either a {@link GeolocationPosition} or a
181-
* {@link GeolocationError}. Use {@code switch} pattern matching on the
182-
* outcome; no dead "pending" arm is needed because one-shot requests never
183-
* produce that value.
181+
* Requests the user's current position once. On a successful reading
182+
* {@code onSuccess} is invoked with the {@link GeolocationPosition}; if the
183+
* browser reports an error instead {@code onError} is invoked with the
184+
* {@link GeolocationError}. The pair mirrors the W3C
185+
* {@code getCurrentPosition(success, error)} signature and matches
186+
* {@link GeolocationTracker#addPositionListener
187+
* GeolocationTracker.addPositionListener}, so callers can share the same
188+
* handler shape between one-shot and watch APIs.
184189
* <p>
185190
* The call returns immediately. The browser may show a permission dialog on
186-
* the first call; after the user responds, the callback is invoked on the
187-
* UI thread.
191+
* the first call; after the user responds, exactly one of the callbacks is
192+
* invoked on the UI thread.
188193
*
189-
* @param callback
190-
* invoked with the outcome once the browser reports it
194+
* @param onSuccess
195+
* invoked with the position on a successful reading; not
196+
* {@code null}
197+
* @param onError
198+
* invoked with the error if the browser reports one; not
199+
* {@code null}
191200
*/
192-
public void get(SerializableConsumer<GeolocationOutcome> callback) {
193-
get(null, callback);
201+
public void get(SerializableConsumer<GeolocationPosition> onSuccess,
202+
SerializableConsumer<GeolocationError> onError) {
203+
get(onSuccess, onError, null);
194204
}
195205

196206
/**
@@ -199,25 +209,35 @@ public void get(SerializableConsumer<GeolocationOutcome> callback) {
199209
* See {@link GeolocationOptions} for the available settings.
200210
* <p>
201211
* The call returns immediately. The browser may show a permission dialog on
202-
* the first call; after the user responds, the callback is invoked on the
203-
* UI thread.
212+
* the first call; after the user responds, exactly one of the callbacks is
213+
* invoked on the UI thread.
204214
*
215+
* @param onSuccess
216+
* invoked with the position on a successful reading; not
217+
* {@code null}
218+
* @param onError
219+
* invoked with the error if the browser reports one; not
220+
* {@code null}
205221
* @param options
206222
* accuracy / timeout / cache-age tuning, or {@code null} to use
207223
* the browser defaults
208-
* @param callback
209-
* invoked with the outcome once the browser reports it
210224
*/
211-
public void get(@Nullable GeolocationOptions options,
212-
SerializableConsumer<GeolocationOutcome> callback) {
225+
public void get(SerializableConsumer<GeolocationPosition> onSuccess,
226+
SerializableConsumer<GeolocationError> onError,
227+
@Nullable GeolocationOptions options) {
228+
Objects.requireNonNull(onSuccess, "onSuccess callback cannot be null");
229+
Objects.requireNonNull(onError, "onError callback cannot be null");
213230
client.get(options).whenComplete((outcome, error) -> {
214231
if (error != null) {
215232
LOGGER.debug("Geolocation get() failed", error);
216-
callback.accept(new GeolocationError(
233+
onError.accept(new GeolocationError(
217234
GeolocationErrorCode.UNKNOWN.code(),
218235
"Client-side geolocation bridge failure"));
219-
} else {
220-
callback.accept(outcome);
236+
return;
237+
}
238+
switch (outcome) {
239+
case GeolocationPosition position -> onSuccess.accept(position);
240+
case GeolocationError outcomeError -> onError.accept(outcomeError);
221241
}
222242
});
223243
}

‎flow-server/src/main/java/com/vaadin/flow/component/geolocation/GeolocationError.java‎

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,20 @@
2020
* {@link GeolocationPosition}.
2121
* <p>
2222
* This is one of the three possible values of a
23-
* {@link GeolocationTracker#valueSignal()} signal, and one of the two values a
24-
* {@link Geolocation#get} callback can receive. Typical application code
25-
* switches on {@link #errorCode()} to react to the specific reason:
23+
* {@link GeolocationTracker#valueSignal()} signal, and the value passed to the
24+
* error callback of {@link Geolocation#get Geolocation.get}. Typical
25+
* application code switches on {@link #errorCode()} to react to the specific
26+
* reason:
2627
*
2728
* <pre>
28-
* ui.getGeolocation().get(result -&gt; {
29-
* if (result instanceof GeolocationError err) {
30-
* switch (err.errorCode()) {
31-
* case PERMISSION_DENIED -&gt;
32-
* showExplanation("Location is blocked for this site.");
33-
* case POSITION_UNAVAILABLE -&gt;
34-
* showRetry("Could not determine your location.");
35-
* case TIMEOUT -&gt; showRetry("Location request took too long.");
36-
* case UNKNOWN -&gt; showGenericError(err.message());
37-
* }
29+
* ui.getGeolocation().get(pos -&gt; showNearest(pos), err -&gt; {
30+
* switch (err.errorCode()) {
31+
* case PERMISSION_DENIED -&gt;
32+
* showExplanation("Location is blocked for this site.");
33+
* case POSITION_UNAVAILABLE -&gt;
34+
* showRetry("Could not determine your location.");
35+
* case TIMEOUT -&gt; showRetry("Location request took too long.");
36+
* case UNKNOWN -&gt; showGenericError("Could not read your location.");
3837
* }
3938
* });
4039
* </pre>

‎flow-server/src/main/java/com/vaadin/flow/component/geolocation/GeolocationOutcome.java‎

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,14 @@
1818
/**
1919
* The actual answer to a geolocation request — either a successful reading or
2020
* an error. Narrower than {@link GeolocationResult}: the "waiting for first
21-
* reading" {@link GeolocationPending} state is excluded because one-shot
22-
* {@link Geolocation#get} never produces it.
21+
* reading" {@link GeolocationPending} state is excluded because a one-shot
22+
* request never produces it.
2323
* <p>
24-
* Returned to the callback of {@link Geolocation#get}. Use this instead of
25-
* {@link GeolocationResult} when you only need to handle the Position / Error
26-
* branches and want the {@code switch} to stay exhaustive without a dead
27-
* Pending arm.
28-
*
29-
* <pre>
30-
* ui.getGeolocation().get(outcome -&gt; {
31-
* switch (outcome) {
32-
* case GeolocationPosition pos -&gt; showNearest(pos);
33-
* case GeolocationError err -&gt; showManualEntry();
34-
* }
35-
* });
36-
* </pre>
24+
* Used as the result type of the internal {@link GeolocationClient#get} future,
25+
* where the sum-type encoding keeps Pending out of the contract. Application
26+
* code rarely references this type directly: {@link Geolocation#get
27+
* Geolocation.get} delivers the position or the error through separate
28+
* callbacks.
3729
*/
3830
public sealed interface GeolocationOutcome extends GeolocationResult
3931
permits GeolocationPosition, GeolocationError {

‎flow-server/src/main/java/com/vaadin/flow/component/geolocation/GeolocationPending.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
/**
1919
* The initial state of a newly started tracking session, held by
2020
* {@link GeolocationTracker#valueSignal()} until the browser reports its first
21-
* position or error. One-shot {@link Geolocation#get} callbacks never receive
22-
* this value.
21+
* position or error. One-shot {@link Geolocation#get} requests never produce
22+
* this value — they deliver a position or an error through separate callbacks.
2323
*/
2424
public record GeolocationPending() implements GeolocationResult {
2525
}

‎flow-server/src/main/java/com/vaadin/flow/component/geolocation/GeolocationPosition.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
* moment in time they were taken.
2323
* <p>
2424
* This is one of the three possible values of a
25-
* {@link GeolocationTracker#valueSignal()} signal, and one of the two values a
26-
* {@link Geolocation#get} callback can receive.
25+
* {@link GeolocationTracker#valueSignal()} signal, and the value passed to the
26+
* success callback of {@link Geolocation#get Geolocation.get}.
2727
*
2828
* @param coords
2929
* the latitude/longitude and related fields; see

‎flow-server/src/main/java/com/vaadin/flow/component/geolocation/GeolocationResult.java‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@
2929
* <li>{@link GeolocationPosition} — a successful reading.</li>
3030
* <li>{@link GeolocationError} — the browser reported an error.</li>
3131
* </ul>
32-
* For the one-shot {@link Geolocation#get} callback use the narrower
33-
* {@link GeolocationOutcome}, which excludes {@link GeolocationPending}
34-
* (one-shot requests never produce that value).
32+
* One-shot {@link Geolocation#get} requests never produce
33+
* {@link GeolocationPending}; they deliver the position and the error through
34+
* separate callbacks instead.
3535
* <p>
3636
* The sealed hierarchy is designed for exhaustive pattern matching. A
3737
* {@code switch} covering the three permitted variants is guaranteed complete

‎flow-server/src/test/java/com/vaadin/flow/component/geolocation/GeolocationClientSeamTest.java‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636

3737
import static org.junit.jupiter.api.Assertions.assertEquals;
3838
import static org.junit.jupiter.api.Assertions.assertFalse;
39-
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
4039
import static org.junit.jupiter.api.Assertions.assertNotNull;
4140
import static org.junit.jupiter.api.Assertions.assertNull;
4241
import static org.junit.jupiter.api.Assertions.assertSame;
@@ -71,7 +70,8 @@ void lookupFactory_resolvedAtConstruction_clientReceivesGetCalls() {
7170
.thenReturn(unused -> fake);
7271

7372
UI freshUi = new MockUI();
74-
freshUi.getGeolocation().get(outcome -> {
73+
freshUi.getGeolocation().get(pos -> {
74+
}, err -> {
7575
});
7676

7777
assertEquals(1, fake.getCalls.size(),
@@ -83,7 +83,8 @@ void setClient_routesGetThroughInstalledClient() {
8383
FakeClient fake = new FakeClient();
8484
ui.getGeolocation().setClient(fake);
8585

86-
ui.getGeolocation().get(outcome -> {
86+
ui.getGeolocation().get(pos -> {
87+
}, err -> {
8788
});
8889

8990
assertEquals(1, fake.getCalls.size(),
@@ -131,21 +132,21 @@ void track_handleIsNullAfterStop() {
131132
}
132133

133134
@Test
134-
void get_callbackReceivesUnknownErrorWhenClientFutureFailsExceptionally() {
135+
void get_onErrorReceivesUnknownErrorWhenClientFutureFailsExceptionally() {
135136
FakeClient fake = new FakeClient();
136137
fake.nextGetResult = CompletableFuture
137138
.failedFuture(new RuntimeException(
138139
"Client-side geolocation.get failed: boom"));
139140
ui.getGeolocation().setClient(fake);
140141

141-
AtomicReference<@Nullable GeolocationOutcome> received = new AtomicReference<>();
142-
ui.getGeolocation().get(received::set);
142+
AtomicReference<@Nullable GeolocationPosition> position = new AtomicReference<>();
143+
AtomicReference<@Nullable GeolocationError> error = new AtomicReference<>();
144+
ui.getGeolocation().get(position::set, error::set);
143145

144-
GeolocationOutcome outcome = received.get();
145-
assertNotNull(outcome,
146-
"callback must fire even when the JS bridge fails");
147-
GeolocationError err = assertInstanceOf(GeolocationError.class, outcome,
148-
"infra failure should surface as a GeolocationError");
146+
GeolocationError err = error.get();
147+
assertNotNull(err, "onError must fire even when the JS bridge fails");
148+
assertNull(position.get(),
149+
"onSuccess must stay silent when the bridge fails");
149150
assertEquals(GeolocationErrorCode.UNKNOWN, err.errorCode(),
150151
"error code should be UNKNOWN for client-bridge failures");
151152
assertFalse(err.message().contains("boom"),

‎flow-server/src/test/java/com/vaadin/flow/component/geolocation/GeolocationTest.java‎

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ void get_executesPromiseJs() {
156156
TestComponent component = new TestComponent();
157157
ui.add(component);
158158

159-
ui.getGeolocation().get(result -> {
159+
ui.getGeolocation().get(pos -> {
160+
}, err -> {
160161
});
161162

162163
List<PendingJavaScriptInvocation> invocations = ui
@@ -166,43 +167,47 @@ void get_executesPromiseJs() {
166167
}
167168

168169
@Test
169-
void get_callbackReceivesPosition() {
170+
void get_onSuccessReceivesPositionAndOnErrorIsSilent() {
170171
TestComponent component = new TestComponent();
171172
ui.add(component);
172173

173-
List<GeolocationOutcome> received = new ArrayList<>();
174-
ui.getGeolocation().get(received::add);
174+
List<GeolocationPosition> positions = new ArrayList<>();
175+
List<GeolocationError> errors = new ArrayList<>();
176+
ui.getGeolocation().get(positions::add, errors::add);
175177

176178
resolvePromise(ui,
177179
resultJson(position(60.1699, 24.9384, 10.0), null, "GRANTED"));
178180

179-
assertEquals(1, received.size());
180-
assertInstanceOf(GeolocationPosition.class, received.get(0));
181-
assertEquals(60.1699,
182-
((GeolocationPosition) received.get(0)).coords().latitude());
181+
assertEquals(1, positions.size());
182+
assertEquals(60.1699, positions.get(0).coords().latitude());
183+
assertTrue(errors.isEmpty(),
184+
"onError must not fire for a successful reading");
183185
}
184186

185187
@Test
186-
void get_callbackReceivesError() {
188+
void get_onErrorReceivesErrorAndOnSuccessIsSilent() {
187189
TestComponent component = new TestComponent();
188190
ui.add(component);
189191

190-
List<GeolocationOutcome> received = new ArrayList<>();
191-
ui.getGeolocation().get(received::add);
192+
List<GeolocationPosition> positions = new ArrayList<>();
193+
List<GeolocationError> errors = new ArrayList<>();
194+
ui.getGeolocation().get(positions::add, errors::add);
192195

193196
resolvePromise(ui, resultJson(null, error(1, "denied"), "DENIED"));
194197

195-
assertEquals(1, received.size());
196-
assertInstanceOf(GeolocationError.class, received.get(0));
197-
assertEquals(1, ((GeolocationError) received.get(0)).code());
198+
assertEquals(1, errors.size());
199+
assertEquals(1, errors.get(0).code());
200+
assertTrue(positions.isEmpty(),
201+
"onSuccess must not fire when the browser reports an error");
198202
}
199203

200204
@Test
201205
void get_updatesAvailabilityFromResponse() {
202206
TestComponent component = new TestComponent();
203207
ui.add(component);
204208

205-
ui.getGeolocation().get(result -> {
209+
ui.getGeolocation().get(pos -> {
210+
}, err -> {
206211
});
207212
resolvePromise(ui,
208213
resultJson(position(60.0, 25.0, 10.0), null, "GRANTED"));

0 commit comments

Comments
 (0)