Migrating to v1
Step numbering matches the iOS guide, so a team migrating both platforms can read them side by side. Three headings differ because the platform mechanism differs — noted where they do.
Overview
v1 replaces four things across every module:
- Static singletons → per-instance sessions.
WemapCoreSDK.instanceandWemapMapSDK.instanceare gone; aCoreSessionorMapSessionloads the map data and is shared by the views and location sources of one screen. - Global mutable constants → immutable configs.
CoreConstants,MapConstants,ARConstants,VPSControllerConstantsandStateManagerConstantsare replaced bydata classconfigs, fixed at creation time. - Listeners →
Flow. Every*Listener/*Observerinterface on a manager or a location source is removed in favour of a read-onlyFlowproperty. - Kotlin idiom throughout. Acronyms are no longer all-caps in symbol names, every public
Floatscalar is aDouble, time-valued members arekotlin.time.Duration, and errors are sealed class hierarchies.
Work through the steps in order. Each step leaves the project compiling, so you can stop and test between them. Steps 1–7 apply to every consumer; 8–12 only if you use that feature.
- Effort: a few hours for a map-only app; closer to a day for an app with VPS positioning or a custom location source.
- Toolchain: Kotlin 2.2, JVM target 11 and
compileSdk37 are now the minimum.
If you are migrating an iOS app in parallel, the iOS guide uses the same step numbering.
1 · Update the dependency
Rule: move to the latest 1.x release and raise your toolchain floor.
Take the version itself — and the transitive dependency versions that come with it — from the releases page. This guide deliberately does not repeat them, so that it stays correct as 1.x moves on.
Three floors changed in v1, and they block the build before anything else does:
| v0.x | v1.0 | |
|---|---|---|
| Kotlin | 1.9 — no pin, so whatever the 2.0.20 compiler emitted | 2.2 — set by the kotlinx-serialization :core re-exposes |
| JVM target | 1.8 | 11 — 17 for :geo-ar and :geo-ar-compose |
| compileSdk | 36 | 37 |
minSdk is unchanged at 23 for most modules. Four require 24, each because of a hardware-gated
dependency: :geo-ar and :geo-ar-compose (ARCore/SceneView, as in v0.x), and — new in v1.0 —
:positioning:wemap-vps-arcore (ARCore 1.56.0) and :positioning:wemap-vps-local (LiteRT). Nothing that can
run ARCore or on-device inference is below API 24, so this is a formality for those four.
Two artifacts are new in v1 and are opt-in — nothing else changes if you do not take them:
implementation("com.getwemap.sdk:map-compose:<version>") // Jetpack Compose for the Map SDK
implementation("com.getwemap.sdk:geo-ar-compose:<version>") // Jetpack Compose for the GeoAR SDK
Watch out: five dependencies :core used to expose transitively are now implementation — Retrofit,
Retrofit's SerializationConverter, Coroutines Android, OkHttp's LoggingInterceptor and (from
:positioning:android-fused-adaptive) PlayServices Base. If your app compiled against any of them through the
SDK, declare it yourself.
2 · Create a session
Rule: wherever you reached a singleton or passed a MapData, create one session per screen and pass it in.
// before
val mapData = WemapMapSDK.instance.mapData(id = 19158, token = "TOKEN")
mapView.mapData = mapData
mapView.onCreate(savedInstanceState)
mapView.getMapViewAsync { view, map, style, data -> /* … */ }
// after
val session = MapSession.create(context, mapId = 19158, token = "TOKEN", config = SessionConfig())
mapView.configure(session, MapViewConfig())
lifecycleScope.launch {
runCatching { mapView.awaitLoaded() }
.onSuccess { /* managers are usable */ }
.onFailure { error -> println("Failed to load mapView with error - $error") }
}
Share the same session across a screen's WemapMapView, GeoARView and location sources — that is what
keeps navigation, POI selection and user location consistent. A second map needs its own session. Call
session.deinit() when the screen is done.
CoreSession.create and MapSession.create are suspend; offline maps use
MapSession.create(context, offlineZip, config) (see step 9).
Removed: WemapCoreSDK.instance, WemapCoreSDK.setEnvironment, WemapCoreSDK.setItinerariesEnvironment,
WemapMapSDK.instance, WemapMapSDK.mapData(id, token), ServiceFactory, DependencyManager.
Constructor mapping:
| v0.x | v1.0 |
|---|---|
WemapMapView.mapData = … | mapView.configure(session, config) |
GeoARView.mapData = … | geoARView.configure(session, config) |
WemapVPSARCoreLocationSource(context, mapData) / (context, token) | VpsARCoreLocationSource(context, session, vpsConfig) |
GPSLocationSource(context, mapData) | GpsLocationSource(context, session) |
GmsFusedLocationSource(context) / (context, mapData) | GmsFusedLocationSource(context, session) |
AndroidFusedAdaptiveLocationSource(context, mapData) | AndroidFusedAdaptiveLocationSource(context, session) |
SimulatorLocationSource(mapData, options) | SimulatorLocationSource(session, options) |
Watch out: MapData is no longer for public use. Read map metadata from the session instead: CoreSession
exposes exactly mapId, mapCenter, isVpsEnabled, config, itineraryProvider, pointOfInterestService,
navigationInfoHandler and deinit(); MapSession adds the offline factories. There is no public graph id — the graph is resolved by the session and reached through
session.itineraryProvider.
Watch out: creating a session is a suspending network call, so it can no longer happen inline in
onViewCreated. Hoist it into a ViewModel so a configuration change does not re-fetch the map — that is what
the sample apps do, and it is also what WemapMap (Compose) expects, since it rebuilds the view when session
changes.
3 · Pass configs instead of setting globals
Rule: every value you used to assign to a *Constants static is now a val property of a config, built in
one constructor call and passed at creation time. There is no global mutable configuration left.
// before
CoreConstants.ITINERARY_RECALCULATION_ENABLED = false
CoreConstants.ENVIRONMENT = Environment.Dev()
MapConstants.STALE_TIMEOUT_MILLISECONDS = 10_000
// after
val session = MapSession.create(
context, mapId = 19158, token = "TOKEN",
config = SessionConfig(itineraryRecalculationEnabled = false, environment = Environment.DEV)
)
mapView.configure(session, MapViewConfig(staleStateTimeout = 10.seconds))
| v0.x | v1.0 | Passed to |
|---|---|---|
CoreConstants statics | SessionConfig (com.getwemap.sdk.core.configs) | CoreSession.create / MapSession.create |
WemapCoreSDK.setEnvironment(…) / CoreConstants.ENVIRONMENT | SessionConfig.environment | CoreSession.create / MapSession.create |
WemapCoreSDK.setItinerariesEnvironment(…) | SessionConfig.directionsEnvironment | CoreSession.create / MapSession.create |
MapConstants statics | MapViewConfig (com.getwemap.sdk.map.configs) | mapView.configure(session, config) |
ARConstants + ARConstants.DirectionalArrow | GeoARViewConfig + GeoARViewConfig.DirectionalArrow | geoARView.configure(session, config) |
VPSControllerConstants, StateManagerConstants | VpsConfig — see step 8 | VpsARCoreLocationSource(…) |
Property-by-property:
| v0.x | v1.0 |
|---|---|
CoreConstants.ITINERARY_RECALCULATION_ENABLED | SessionConfig.itineraryRecalculationEnabled |
CoreConstants.USER_LOCATION_PROJECTION_ON_ITINERARY_ENABLED | SessionConfig.userLocationProjectionOnItineraryEnabled |
CoreConstants.USER_LOCATION_PROJECTION_ON_GRAPH_ENABLED | SessionConfig.userLocationProjectionOnGraphEnabled |
MapConstants.SWITCH_LEVELS_AUTOMATICALLY_ON_USER_MOVEMENTS | MapViewConfig.switchLevelsAutomaticallyOnUserMovements |
MapConstants.STALE_TIMEOUT_MILLISECONDS (Long, ms) | MapViewConfig.staleStateTimeout (Duration) |
ARConstants.NAVIGATION_VISIBILITY_DISTANCE | GeoARViewConfig.navigationVisibilityDistance — now nullable |
ARConstants.STEP_INSTRUCTION_ALTITUDE | GeoARViewConfig.stepInstructionAltitude |
ARConstants.DirectionalArrow.TARGET_DISTANCE (and siblings) | GeoARViewConfig.DirectionalArrow.targetDistance (and siblings) |
Three knobs that were public vars are no longer public API:
CoreConstants.INCLUDE_PROJECTION_DISTANCE_INTO_ACCURACY, MapConstants.VISUAL_DEBUGGER_CONFIG and
ARConstants.VISUAL_DEBUGGER_ENABLED. They have no replacement in the public surface.
Other shape changes on the same lines:
MapConstantsmoved frommap.helperstomap.configsand keeps onlyWEMAP_BLUE.ARConstantsis internal.- The
Environmentsealed class andIEnvironmentwere replaced by thecom.getwemap.sdk.core.configs.Environmentenum —Environment.DEV/Environment.PROD.Environment.Prod()no longer compiles. CoreConstants.ITINERARIES_HOST/ITINERARIES_BASE_URLwere stored and settable;SessionConfig.directionsBaseUrlis computed fromdirectionsEnvironment, so the two can no longer disagree.
Watch out: every config property is a val. Build the config you want in one constructor call — there is
nothing to assign to afterwards, by design. copy() works for deriving a variant.
Watch out (Java callers): SessionConfig and MapViewConfig carry a Duration property, so Kotlin mangles their
generated copy() and componentN() into names like copy-45ZY6uE. Build these configs from Kotlin.
4 · Update your Coordinate and level usage
Rule: Coordinate is an immutable value type built around a 2D GeoJSON Point — it no longer wraps a
Location — and levels are a Levels value rather than a List<Float>.
// before
val coordinate = Coordinate(someLocation, listOf(0f, 1f))
val accuracy = coordinate.accuracy // Float?
val location = coordinate.location // Location
val time = coordinate.time
// after
val coordinate = Coordinate(someLocation, Levels.Range(0f..1f))
val accuracy = coordinate.horizontalAccuracy // Double, 0.0 or negative means "no accuracy"
val location = coordinate.toLocation()
val time = coordinate.timestamp
| v0.x | v1.0 |
|---|---|
levels: List<Float> (empty meant outdoor) | levels: Levels (Levels.Outdoor by default) |
Coordinate(location, levels, heightFromFloor, heightFromGround) | same shape, levels is a Levels and the heights are Double? |
Coordinate(latitude, longitude, levels: List<Float>) | Coordinate(latitude, longitude, levels: Levels, altitude, bearing, horizontalAccuracy, timestamp, heightFromFloor, heightFromGround) |
| — | Coordinate(latitude, longitude, level: Float, altitude) and (latitude, longitude, range: ClosedRange<Float>, altitude) convenience constructors |
accuracy: Float? | horizontalAccuracy: Double — non-optional |
time: Long | timestamp: Long |
location: Location | toLocation() |
copy(location, levels, …) | copy(point, …) — takes a Point, not a Location |
Levels is a sealed class with Levels.Outdoor, Levels.Single(level) and Levels.Range(range) (a
ClosedRange<Float>), plus fromArray(List<Float>), toList(), union, contains, intersects,
intersection, diff and the isOutdoor / isSingle / isRange predicates. Segment.levels and the graph
Edge.levels are Levels too. fromArray throws Levels.InitializationError, itself a sealed hierarchy of
InvalidLevelsCount, WrongRangeBoundsOrder and RangeBoundsEqual — match with is.
No longer public API: the Coordinate.ecef and cartesian properties, ecefToEusRot, eusToEcefRot and
the fromECEF / fromCartesian / fromPoint / equals(lhs, rhs, eps, epsAlt) factories, plus the
LocationFactory object. ecefToEnuRot and enuToEcefRot are gone outright, with no internal equivalent.
Level moved from :core to :map — com.getwemap.sdk.core.model.entities.Level is now
com.getwemap.sdk.map.buildings.Level. Its copy() was removed (its constructor was already internal in v0.x),
so a Level can only come from a Building.
Watch out: toLocation() allocates a fresh Location on every call — hold the result rather than calling it
in a loop.
5 · Replace listeners with Flows
iOS calls this step "Replace delegates with
AsyncStream". Same change, different mechanism.
Rule: delete the listener registration and collect the Flow of the same name, scoped to the view's lifecycle.
// before
pointOfInterestManager.addListener(object : PointOfInterestManagerListener {
override fun onPointOfInterestSelected(poi: PointOfInterest) { /* … */ }
override fun onPointOfInterestUnselected(poi: PointOfInterest) { /* … */ }
})
// after
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
pointOfInterestManager.selectionUpdates.collect { update ->
if (update.selected.isNotEmpty()) { /* … */ }
}
}
}
| v0.x listener | v1.0 flows |
|---|---|
LocationSource.listener (LocationSourceListener) | coordinates / attitudes / errors |
INavigationManager.addListener / removeListener | navigationEvents (sealed NavigationEvent — Started / Stopped / Arrived / Recalculated) / navigationInfoUpdates / errors |
IPointOfInterestManager.addListener / removeListener | selectionUpdates (PointOfInterestSelectionUpdate) / touchedPois |
BuildingManager.addListener / removeListener | focusedBuildings / activeLevelChanges / errors |
UserLocationManager and ARLocationManager listeners | coordinates / attitudes / errors |
UserLocationManager.coordinateFlow | coordinates |
WemapVPSARCoreLocationSourceListener / …Observer | see step 8 |
| view loading callbacks | see step 7 |
UserLocationManager and ARLocationManager implement the new read-only UserLocationProvider
(coordinates / attitudes / errors / lastCoordinate).
Watch out: in a Fragment, collect from viewLifecycleOwner.lifecycleScope, not lifecycleScope. The
Fragment outlives its view, so a collector on the Fragment's own scope keeps running against a torn-down view and
holds the managers alive with it. repeatOnLifecycle(Lifecycle.State.STARTED) is what makes the collector stop at
ON_STOP and restart at ON_START; without it a plain launch { … collect { } } keeps collecting in the
background. Every screen in the
sample apps uses exactly this shape.
Watch out: focusedBuildings, loadPhases, states, scanStatuses and backgroundScanStatuses are
StateFlows and replay their current value to a new collector; the rest are event flows and do not. If you were
relying on a listener being called back with the current state at registration time, read the matching
non-flow property (focusedBuilding, loadPhase, state, scanStatus) instead.
6 · Update the renamed calls and types
iOS calls this step "Await instead of subscribing". Android's asynchronous calls were already
suspendin v0.x, so nothing changes there — but the same call sites are hit by four renaming themes.
Rule: the whole public surface was normalized to Kotlin idiom in one release. Most of it is mechanical.
Acronyms are no longer all-caps
POI → Poi, VPS → Vps, GPS → Gps, URL → Url, ID → Id, ECEF → Ecef.
// before
pointOfInterestManager.addPOI(poi)
pointOfInterestManager.hideAllPOIs()
val selected = pointOfInterestManager.getSelectedPOI()
// after
pointOfInterestManager.addPoi(poi)
pointOfInterestManager.hideAllPois()
val selected = pointOfInterestManager.getSelectedPoi()
| v0.x | v1.0 |
|---|---|
getPOIs, addPOI, addPOIs, removePOI, removePOIs | getPois, addPoi, addPois, removePoi, removePois |
showPOI, hidePOI, showPOIs, hidePOIs, showAllPOIs, hideAllPOIs | showPoi, hidePoi, showPois, hidePois, showAllPois, hideAllPois |
getSelectedPOI, getSelectedPOIs, unselectPOI, unselectAllPOIs | getSelectedPoi, getSelectedPois, unselectPoi, unselectAllPois |
selectPOI, centerToPOI | selectPoi, centerToPoi |
sortPOIsByGraphDistance, sortPOIsByDuration | sortPoisByGraphDistance, sortPoisByDuration |
GPSLocationSource | GpsLocationSource |
WemapVPSARCoreLocationSource | VpsARCoreLocationSource |
isVPSAvailableAt, distanceToVPSCoverageFrom | isVpsAvailableAt, distanceToVpsCoverageFrom |
PointOfInterest.imageURL / mediaURL / mediaThumbnailURL | imageUrl / mediaUrl / mediaThumbnailUrl |
AR and other two-letter acronyms stay uppercase (VpsARCoreLocationSource, GeoARView, ARNavigationManager).
Interfaces no longer carry an I prefix
Every interface is now named for the role, with no prefix; where a class already had the bare name, it is the
class that was qualified — *Impl where the interface has one implementation (NavigationManagerImpl), the
distinguishing axis where it has two (DirectionsServiceRemote beside DirectionsServiceLocal). None of those
classes is customer API: most are internal, and the four that are public for cross-module reasons
(NavigationManagerImpl, PointOfInterestManagerImpl, ItineraryProviderImpl, AbstractReferenceCounter)
carry @InternalWemapApi and are absent from api/*.api, so they can be renamed again in any 1.x.
// before
val manager: INavigationManager = session.navigationManager
fun observe(provider: IUserLocationProvider) { … }
// after
val manager: NavigationManager = session.navigationManager
fun observe(provider: UserLocationProvider) { … }
The public ones, in full: INavigationManager, IPointOfInterestManager, IUserLocationProvider,
ILoadPhaseProvider, IItineraryProvider, IPointOfInterestService, IMapNavigationManager,
IMapPointOfInterestManager, IPackdataService, IARNavigationManager, IARPointOfInterestManager — drop
the I from each. Nothing else about them changed.
The compact-logging contract is renamed
IStringConvertibleCompact → CompactStringConvertible, and its toStringCompact() → toCompactString(). The
implementors are unchanged: Coordinate, PointOfInterest, PointOfInterestType, Itinerary, TravelMode,
NavigationOptions, ItinerarySearchRules, ItineraryOptions and LineOptions.
NavigationInfo now implements it too, in place of its shortDescription property:
// before
textView.text = info.shortDescription
// after
textView.text = info.toCompactString()
Every public Float scalar is a Double
Coordinate.bearing, Coordinate.heightFromFloor, Coordinate.heightFromGround, Coordinate.bearingTo,
Coordinate.destination, Attitude.accuracy, Heading.accuracy, Itinerary.duration, Leg.duration,
Step.duration, NavigationInfo.remainingTime, NavigationOptions.arrivedDistanceThreshold,
NavigationOptions.userPositionThreshold, SimulationOptions.horizontalAccuracy.
Level values stay Float (Levels.Single(0f), Level.id).
Time is kotlin.time.Duration
Time-valued members lost their unit suffix and take a Duration:
| v0.x | v1.0 |
|---|---|
NavigationOptions.navigationRecalculationTimeInterval (raw) | same name, Duration |
CoreConstants POI timeout | SessionConfig.pointsOfInterestLoadingTimeout (Duration) |
MapConstants.STALE_TIMEOUT_MILLISECONDS | MapViewConfig.staleStateTimeout (Duration) |
VPSControllerConstants.BACKGROUND_SCAN_TIME_INTERVAL | VpsControllerConfig.backgroundScanTimeInterval (Duration) |
INavigationManager.infoUpdatePeriod + infoUpdateTimeUnit | navigationInfoUpdatesInterval (one Duration) |
SimulationOptions.dispatchPeriod + timeUnit | timeInterval (one Duration); restartDelay is a Duration |
startNavigation(…, timeout) | timeout is a Duration |
Because Duration is a Kotlin value class, these members are not accessible from Java.
Errors are sealed class hierarchies
Match with is, not by identity.
// before
if (error === NavigationError.noActiveNavigation) { … }
// after
if (error is NavigationError.NoActiveNavigation) { … }
| v0.x | v1.0 |
|---|---|
NavigationError.noActiveNavigation and siblings | NavigationError.NoActiveNavigation, FailedToAddNavigation, FailedToRemoveNavigation, FailedToRetrieveUserLocation, NavigationAlreadyExists, FailedToRecalculateNavigation, plus a new Unknown |
LocationSourceError.trackingLost | LocationSourceError.TrackingLost |
ItineraryServiceError | DirectionsServiceError — NoItinerariesFound(reason), NotSupportedTravelMode, InvalidRules, and the new RequestFailed(code, reason) and GraphUnavailable |
WemapVPSARCoreLocationSourceError | VpsARCoreLocationSourceError — ConnectionTimeout, NotConnectedToInternet, SlowConnectionDetected, TiltTooHigh, FailedToDownloadVpsCoverage, FailedToDownloadLevelsMapping, FailedToCalculateDistanceToVpsCoverage, Unknown |
Services and managers: ids bound once, results renamed
| v0.x | v1.0 |
|---|---|
IItineraryProvider.graph(id), ruleNames(graphId) | graph(), ruleNames() — the session binds the ids |
IItineraryProvider.itineraries(…, mapId) | itineraries(…) — no mapId |
itinerariesInfoToMultipleDestinations(origin, pois, mapID) | itinerariesInfoToMultipleDestinations(origin, destinations, travelMode, searchRules) returning List<CoordinateWithItineraryInfo>; itinerariesInfoToMultiplePois(…) is new |
IPointOfInterestService.pointsOfInterestById(mapId, poiId) | pointsOfInterest(id) / pointsOfInterest() |
PointOfInterestWithInfo (a Pair typealias) | PointOfInterestWithItineraryInfo |
ItineraryManager.itineraries | drawnItineraries |
ItineraryManager.getItineraries(…) | computeItineraries(…) |
ItineraryManager.searchRuleNames(graphId) | searchRuleNames() |
ItinerarySearchRules in core.model.services.parameters | core.model.services |
ItineraryInfo in core.model.services.responses | core.model.services |
NavigationInfoHandler() (public constructor) | session.navigationInfoHandler |
MapNavigationManager.startNavigation gained a trailing cameraMode: Int? on every overload, and its
itineraryOptions parameter is nullable — null keeps the current options, a value replaces them.
Watch out: MapPointOfInterestManager.defaultZoom is read-only. It was a settable knob in v0.x; pass zoom
to selectPoi / centerToPoi instead.
7 · Views: LoadPhase and non-null managers
Rule: wait for the view with awaitLoaded() (or collect loadPhases), then use its managers directly.
// before
mapView.getMapViewAsync { view, map, style, data ->
view.navigationManager.startNavigation(…)
}
if (mapView.isLoaded) { … }
// after
lifecycleScope.launch {
runCatching { mapView.awaitLoaded() }
.onSuccess { mapView.navigationManager.startNavigation(…) }
.onFailure { error -> /* the map failed to load */ }
}
| v0.x | v1.0 |
|---|---|
WemapMapView.getMapViewAsync(OnMapViewReadyCallback) and the lambda overload | loadPhases: StateFlow<LoadPhase> + suspend fun awaitLoaded() |
GeoARView.getARViewAsync(OnGeoARViewReadyCallback) and the lambda overload | same |
OnMapViewReadyCallback, OnGeoARViewReadyCallback | removed |
WemapMapView.isLoaded / GeoARView.isLoaded | loadPhase.isReady / loadPhase.isLoading / loadPhase.error |
WemapMapView.mapData / GeoARView.mapData | removed — see step 2 |
WemapMapView.map (settable) | read-only |
LoadPhase is Loading / Ready / Failed(error), with isLoading, isReady and error. awaitLoaded() is
an extension on LoadPhaseProvider, so it needs import com.getwemap.sdk.core.awaitLoaded; it returns the
concrete view type, as the old callback did.
Failures are now reported. A map that fails to load settles Failed; in v0.x it logged and told nobody, and
there was nothing to await. A points-of-interest download failure or timeout settles Failed as well, and in that
case every manager stays built and usable. MapError.FailedToLoadMap is the error MapLibre failures carry.
The view drives its own lifecycle. WemapMapView and GeoARView observe a Lifecycle — auto-discovered from
the view tree, or assigned to mapView.lifecycle, or handed to the constructor as sharedLifecycle. Delete your
onCreate / onStart / onResume / onPause / onStop / onSaveInstanceState / onLowMemory / onDestroy
forwarding.
Watch out: those inherited MapLibre methods still compile — WemapMapView extends MapLibre's MapView, so
they cannot be hidden. While a lifecycle is set they are logged and ignored. Leaving the forwarding in place is
silent dead code, not an error.
Watch out: isLoaded was false after a failure, but it was also false while loading, so its closest
equivalent is loadPhase.isReady for the success check and !loadPhase.isLoading for "has settled".
Watch out: one view of each kind per session. A session holds one map-renderer slot and one AR-renderer slot,
so a second WemapMapView on the same session evicts the first, which keeps drawing while no manager drives it.
Watch out: do not read a manager off mapView / geoARView in a Fragment's onDestroyView. ON_DESTROY
reaches the view lifecycle before that callback, so the view is already torn down and every manager getter
throws. Nothing view-owned needs undoing there.
8 · If you use positioning (VPS or GPS)
Rule: construct the source with the session and a VpsConfig, and collect its flows instead of registering a
listener.
// before
VPSControllerConstants.BACKGROUND_SCAN_DISTANCE_THRESHOLD = 20.0
StateManagerConstants.DEGRADED_DISTANCE_THRESHOLD = 60.0
val source = WemapVPSARCoreLocationSource(context, mapData)
source.addListener(vpsListener)
// after
val source = VpsARCoreLocationSource(
context, session,
VpsConfig(
controller = VpsControllerConfig(backgroundScanDistanceThreshold = 20.0),
stateManager = VpsStateManagerConfig(degradedDistanceThreshold = 60.0)
)
)
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch { source.states.collect { state -> /* … */ } }
launch { source.scanStatuses.collect { status -> /* … */ } }
}
}
VpsConfig composes VpsLocationSourceConfig, VpsControllerConfig, VpsStateManagerConfig,
VpsStaticPositionDetectorConfig and VpsConveyingDetectorConfig — under the parameter names locationSource,
controller, stateManager, staticPositionDetector and conveyingDetector — replacing VPSControllerConstants
and StateManagerConstants.
WemapVPSARCoreLocationSourceListener and WemapVPSARCoreLocationSourceObserver are removed in favour of
states, scanStatuses, backgroundScanStatuses, cameraTrackingStates, userLocalizationUpdates and errors.
listenerExecutor, observers and vpsListeners are gone with them.
The State enum became a sealed class, folding the reasons in:
// before
if (source.state == State.DEGRADED_POSITIONING &&
source.degradedPositioningReason == DegradedPositioningReason.…) { … }
// after
val state = source.state
if (state is State.DegradedPositioning && state.reason == …) { … }
| v0.x | v1.0 |
|---|---|
State.NOT_POSITIONING + notPositioningReason | State.NotPositioning(reason) |
State.DEGRADED_POSITIONING + degradedPositioningReason | State.DegradedPositioning(reason) |
State.ACCURATE_POSITIONING | State.AccuratePositioning |
| comparing to enum constants | is State.X, or the state.isAccurate / state.isLost predicates |
trackingFailureReason | the cameraTrackingStates flow of CameraTrackingState, merging ARCore's TrackingState and TrackingFailureReason |
GPSLocationSource(context, mapData) | GpsLocationSource(context, session) |
CameraTrackingState.copy() and UserLocalizationUpdate.copy() were removed — both types are produced by the
SDK only, and their constructors were already internal. Destructuring and the property getters are unaffected.
Watch out: a source your app starts stays your app's to stop. The SDK reference-counts its own start and
stop requests, so a view disabling its location component no longer stops a source it did not start. Conversely,
do not call start() / stop() on a session-shared source from inside a view callback.
9 · If you use offline maps
Rule: get a packdata service from the session, then open the downloaded package as a session.
// before
val manager: IPackdataManager = …
val packdata = manager.downloadPackdata(mapID = 19158)
val mapData = manager.loadMapData(File(packdata.filePath))
mapView.mapData = mapData
// after
val service = MapSession.createPackdataService(mapId = 19158, environment = Environment.PROD)
val packdata = service.downloadPackdata()
val session = MapSession.create(context, offlineZip = File(packdata.filePath), config = SessionConfig())
mapView.configure(session, MapViewConfig())
| v0.x | v1.0 |
|---|---|
IPackdataManager | PackdataService, from MapSession.createPackdataService(mapId, environment) |
downloadPackdata(mapID), isNewPackdataAvailable(mapID, eTag) | downloadPackdata(), isNewPackdataAvailable(eTag) — the map id is bound at construction |
loadMapData(zip) | MapSession.create(context, offlineZip, config) |
Packdata still carries filePath, eTag, version and fileName — filePath is a String, so wrap it in a
File for MapSession.create.
10 · If you use GeoAR
Rule: same as the map — session in, LoadPhase out, GeoARViewConfig for the tunables.
// before
geoARView.mapData = mapData
geoARView.getARViewAsync { view -> view.navigationManager.startNavigation(…) }
// after
geoARView.configure(session, GeoARViewConfig())
lifecycleScope.launch {
runCatching { geoARView.awaitLoaded() }
.onSuccess { geoARView.navigationManager.startNavigation(…) }
.onFailure { error -> /* … */ }
}
GeoARView.configure takes a CoreSession — a MapSession is one, so a screen with both views passes the same
instance to each.
ARConstants and ARConstants.DirectionalArrow became GeoARViewConfig and GeoARViewConfig.DirectionalArrow
(step 3); ARConstants itself is internal now. GeoARView.mapData, isLoaded and getARViewAsync are gone
(step 7).
GeoARView is a FrameLayout that holds a SceneView rather than extending one, so everything it used to
inherit — engine, scene, view, renderer, materialLoader, modelLoader, childNodes, addChildNode,
skybox, collisionSystem, cameraNode, startRecording and the rest of SceneView's surface — is no longer
reachable through it, and GeoNode / GeoCameraNode are gone with it. The AR scene is driven through
pointOfInterestManager, navigationManager and locationManager.
Watch out: GeoARView.destroy() is idempotent in v1 but the Compose wrapper also calls it — if you drive the
view yourself, destroy() is still yours to call exactly once at teardown.
Watch out: GeoARView is final. If you subclassed it, move the behaviour to the caller or to a wrapping
FrameLayout.
11 · If you implement a custom LocationSource
Rule: publish coordinates through flows instead of a listener.
// before
class MyLocationSource : LocationSource {
override var listener: LocationSourceListener? = null
override val supportsHeading = true
override val isStarted get() = started
fun report(c: Coordinate) { listener?.onCoordinateChanged(c) }
override fun start() { … }
override fun stop() { … }
override fun deinit() { … }
}
// after
class MyLocationSource(private val session: CoreSession) : LocationSource {
private val coordinatesFlow = MutableSharedFlow<Coordinate>(extraBufferCapacity = 1)
override val coordinates: Flow<Coordinate> = coordinatesFlow
override val attitudes: Flow<Attitude> = emptyFlow()
override val errors: Flow<Throwable> = emptyFlow()
override val supportsAttitude = false
override val isStarted get() = started
fun report(c: Coordinate) { coordinatesFlow.tryEmit(c) }
override fun start() { … }
override fun stop() { … }
override fun deinit() { … }
}
mapView.locationManager.locationSource = MyLocationSource(session) // unchanged
| v0.x | v1.0 |
|---|---|
LocationSource.listener (LocationSourceListener) | coordinates: Flow<Coordinate> / attitudes: Flow<Attitude> / errors: Flow<Throwable> |
LocationSource.supportsHeading | supportsAttitude |
SimulationOptions.simulateHeading | simulateAttitude |
The assignment point is unchanged: mapView.locationManager.locationSource for the map and
geoARView.locationManager.locationSource for AR.
Watch out: deinit() now unregisters the source from the session that holds it, so a screen no longer has to
null locationManager.locationSource before releasing it. If you override deinit(), keep the super call — the
SDK's own sources detach identity-checked, so a source cannot clear a successor that already claimed the slot.
12 · Symbols that are no longer public API
Rule: these were public in v0.x and are not part of the supported surface any more. There is no
customer-facing replacement for any of them — if your app imports one, plan the migration off it.
They are gated by @InternalWemapApi, a @RequiresOptIn(level = ERROR) marker, and/or moved into an
*.internal.* package that the generated documentation suppresses. Reaching one now fails the build with
"This declaration is experimental and its usage should be marked with @InternalWemapApi".
MapDataandMapDataSerializerCoreConstantsLocationFactoryCoordinate.ecef/cartesian/ecefToEusRot/eusToEcefRot, and thefromECEF/fromCartesian/fromPoint/equals(lhs, rhs, eps, epsAlt)factories (step 4)IItineraryServiceand the Directions wire types —ItineraryParameters,ItinerariesParametersMultiDestinations,IItineraryParametersBase,ItinerariesResponse,ItineraryInfoResponse,ItinerariesInfoResponse,Status,StatusCode,RuleNames—internalrather than gated, exceptItinerariesResponseandStatus;IItineraryParametersBaseis removedMapConstants.VISUAL_DEBUGGER_CONFIG,CoreConstants.INCLUDE_PROJECTION_DISTANCE_INTO_ACCURACYandARConstants.VISUAL_DEBUGGER_ENABLED(step 3)- the
GeoARViewcamera-provider members —bindCameraProjection,bindCameraCapture,startCameraCapture,unbindCamera,onFrame,onCameraProjectionChanged,viewParameters(step 10) GeoARView.addGeoNodeandgeoCamera, and theGeoNode/GeoCameraNodetypes themselves —internal, not gated, becauseGeoARViewno longer extendsSceneView(step 10)GeoComponent- everything else that was still
publicin an*.internal.*package: the graph types (Edge,Vertex,Graphand theirProperties),LocationSourceManager/LocationSources,NavigationManager,PointOfInterestManager,SystemAttitudeSource/AttitudeSourceListener,CameraCaptureProvider/CameraCaptureDelegate/ViewParameters,FileService,PointOfInterestRenderer,ReferenceCounter/ReferenceCounter,ItineraryProvider,ConveyingBuffer,Inclination,Status,LocalServiceBase, every*Serializer,GeoUtils/MathUtils/GeoConstants/PointOfInterestIdGenerator,Polygon.contains/distanceToandMultiPolygon.contains/distanceTo, and — in:map—StyleData,MapAttitudeEngineandStringGenerator - the rest of the
Doublemath port, which stays incom.getwemap.sdk.core.internal.math—Double2,Double4,Bool2/3/4,Mat2/3/4,VectorComponent,MatrixColumn,QuaternionComponent,RotationsOrder, and the top-level functions over them.QuaternionandDouble3are not in this list: see Moved, not gated below.
Moved, not gated: Quaternion and Double3 are now com.getwemap.sdk.core.math.Quaternion /
…core.math.Double3, and need no opt-in. Attitude.quaternion is the reason: on iOS it is simd_quatd and
every operation on it is plain public API, so gating the Android equivalent left the same property readable but
unusable. Update the import, and you can read q.w/x/y/z and call normalize, inverse, conjugate, length,
dot, cross, angle, slerp/lerp/nlerp, the arithmetic operators and toStringDecimals from
com.getwemap.sdk.core.math. The members that route through a still-gated type — the Double2 swizzles
(xy/rg/st), xyzw, the VectorComponent/QuaternionComponent accessors, equals(…, delta) and
toMatrix() — keep the marker. Attitude.headingDegrees / headingRadians / toHeading() are unchanged and
still need nothing.
Moved, not gated: PaginationContainer is now com.getwemap.sdk.core.model.entities.PaginationContainer.
It is what PointOfInterestService.pointsOfInterest() returns, so it is genuinely public API and only its
package was wrong — update the import; nothing else about it changed. (iOS has always had it in
Core/Sources/Model/Entities.)
Removed outright: PointOfInterest.customerID, NavigationInstructions.direction (read Step.direction),
Coordinate.ecefToEnuRot / enuToEcefRot.
Changes that compile but behave differently
Nothing in this list produces a compiler error. Check each one against your app.
| Change | What to do |
|---|---|
| The views drive their own lifecycle | your mapView.onStart() / onResume() / … forwarding is logged and ignored — delete it (step 7) |
A map that fails to load settles LoadPhase.Failed | awaitLoaded() throws instead of never returning — handle it |
isLoaded was false while loading and after a failure | loadPhase.isReady for success, !loadPhase.isLoading for "has settled" |
A coordinate's time is encoded in Unix seconds | it was milliseconds on Android; if you persisted or forwarded encoded coordinates, re-check the unit |
MapCameraState equality is epsilon-based | a camera you applied compares equal to the one the map reports back |
| The user-location indicator greys the moment tracking is lost | it no longer stays blue for staleStateTimeout |
startNavigation sets the camera mode itself | it follows with compass when navigation starts |
A second WemapMapView or GeoARView on one session evicts the first | one view of each kind per session; a second map needs its own |
LocationSource.deinit() unregisters the source from its session | your manual locationManager.locationSource = null at teardown is now redundant |
| A source your app started is no longer stopped by a view | the SDK reference-counts only its own requests — stop what you started |
NavigationInfo, Itinerary, Leg and TravelMode print a summary | toString() no longer dumps the itinerary geometry — read leg, legs, coordinates or steps directly, or toCompactString() for a log line |
focusedBuildings / loadPhases / states replay, other flows do not | a listener that used to be called back at registration has no equivalent on an event flow — read the property |
"I'm getting this error"
| Error | Go to |
|---|---|
Unresolved reference: WemapCoreSDK / WemapMapSDK | Step 2 |
Unresolved reference: ServiceFactory / DependencyManager | Step 2 |
Unresolved reference: mapData on a view | Step 2 |
Cannot access 'MapData': it is internal / unresolved after an import cleanup | Step 2, Step 12 |
Suspend function 'create' should be called only from a coroutine | Step 2 |
Val cannot be reassigned on a config property | Step 3 |
Unresolved reference: CoreConstants / MapConstants / ARConstants | Step 3 |
Unresolved reference: Prod on Environment | Step 3 |
Type mismatch: inferred type is List<Float> but Levels was expected | Step 4 |
Unresolved reference: accuracy on Coordinate | Step 4 |
Unresolved reference: location on Coordinate | Step 4 |
Unresolved reference: Level after importing from core.model.entities | Step 4 |
Unresolved reference: addListener on a manager | Step 5 |
Unresolved reference: PointOfInterestManagerListener | Step 5 |
Unresolved reference: addPOI / getPOIs / selectPOI (any all-caps acronym) | Step 6 |
Type mismatch: inferred type is Float but Double was expected | Step 6 |
Type mismatch: inferred type is Long but Duration was expected | Step 6 |
Incompatible types: NavigationError and NavigationError.Companion | Step 6 |
Unresolved reference: ItineraryServiceError | Step 6 |
Unresolved reference: getMapViewAsync / getARViewAsync | Step 7 |
Unresolved reference: isLoaded | Step 7 |
Unresolved reference: awaitLoaded | Step 7 — add import com.getwemap.sdk.core.awaitLoaded |
Unresolved reference: WemapVPSARCoreLocationSource | Step 8 |
Unresolved reference: NOT_POSITIONING / notPositioningReason | Step 8 |
Unresolved reference: IPackdataManager | Step 9 |
This class cannot be extended on GeoARView | Step 10 |
'listener' overrides nothing in a custom LocationSource | Step 11 |
'supportsHeading' overrides nothing | Step 11 |
This declaration is experimental … @InternalWemapApi | Step 12 |
Reference: renamed, moved and removed API
Sorted by old name. The authoritative list is the diff between the 0.29.2 and 1.0.0 public-API dumps
(*/api/*.api, produced by ./gradlew apiDump).
| v0.x symbol | v1.0 replacement | Step |
|---|---|---|
ARConstants | GeoARViewConfig | 3 |
Attitude.accuracy: Float? | Double? | 6 |
Coordinate.accuracy | Coordinate.horizontalAccuracy (Double) | 4 |
Coordinate.ecefToEnuRot / enuToEcefRot | removed | 12 |
Coordinate.levels: List<Float> | Coordinate.levels: Levels | 4 |
Coordinate.location | Coordinate.toLocation() | 4 |
Coordinate.time | Coordinate.timestamp | 4 |
CoreConstants | SessionConfig | 3 |
DependencyManager | CoreSession / MapSession | 2 |
Environment.Prod() / IEnvironment | Environment.PROD (enum) | 3 |
GPSLocationSource | GpsLocationSource | 6, 8 |
IARNavigationManager | ARNavigationManager | 6 |
IARPointOfInterestManager | ARPointOfInterestManager | 6 |
IItineraryProvider | ItineraryProvider | 6 |
IItineraryProvider.graph(id) | graph() | 6 |
ILoadPhaseProvider | LoadPhaseProvider | 6 |
IMapNavigationManager | MapNavigationManager | 6 |
IMapPointOfInterestManager | MapPointOfInterestManager | 6 |
IMapPointOfInterestManager.defaultZoom (settable) | read-only | 6 |
INavigationManager | NavigationManager | 6 |
IPackdataManager | PackdataService | 9 |
IPointOfInterestManager | PointOfInterestManager | 6 |
IPointOfInterestManager.getPOIs() | getPois() | 6 |
IPointOfInterestService | PointOfInterestService | 6 |
IPointOfInterestService.pointsOfInterestById(mapId, poiId) | pointsOfInterest(id) | 6 |
IStringConvertibleCompact | CompactStringConvertible | 6 |
ItineraryManager.getItineraries(…) | computeItineraries(…) | 6 |
ItineraryManager.itineraries | drawnItineraries | 6 |
ItineraryServiceError | DirectionsServiceError | 6 |
IUserLocationProvider | UserLocationProvider | 6 |
Level (core.model.entities) | Level (map.buildings), no copy() | 4 |
LocationSource.listener | coordinates / attitudes / errors | 5, 11 |
LocationSource.supportsHeading | supportsAttitude | 11 |
LocationSourceError.trackingLost | LocationSourceError.TrackingLost | 6 |
MapConstants.STALE_TIMEOUT_MILLISECONDS | MapViewConfig.staleStateTimeout (Duration) | 3, 6 |
MapData | session.mapId / mapCenter / isVpsEnabled | 2, 12 |
NavigationError.noActiveNavigation | NavigationError.NoActiveNavigation | 6 |
NavigationInfo.shortDescription | NavigationInfo.toCompactString() | 6 |
NavigationInfoHandler() | session.navigationInfoHandler | 6 |
NavigationInstructions.direction | Step.direction | 12 |
OnMapViewReadyCallback / OnGeoARViewReadyCallback | loadPhases / awaitLoaded() | 7 |
PointOfInterest.customerID | removed | 12 |
PointOfInterest.imageURL | imageUrl | 6 |
PointOfInterestManagerListener | selectionUpdates / touchedPois | 5 |
PointOfInterestWithInfo | PointOfInterestWithItineraryInfo | 6 |
ServiceFactory | CoreSession / MapSession | 2 |
SimulationOptions.simulateHeading | simulateAttitude | 11 |
State (enum) | State (sealed) | 8 |
StateManagerConstants | VpsStateManagerConfig | 3, 8 |
toStringCompact() | toCompactString() | 6 |
VPSControllerConstants | VpsControllerConfig | 3, 8 |
WemapCoreSDK.instance | CoreSession | 2 |
WemapMapSDK.mapData(id, token) | MapSession.create(context, mapId, token, config) | 2 |
WemapMapView.getMapViewAsync | awaitLoaded() / loadPhases | 7 |
WemapMapView.isLoaded | loadPhase | 7 |
WemapVPSARCoreLocationSource | VpsARCoreLocationSource | 6, 8 |
WemapVPSARCoreLocationSourceError | VpsARCoreLocationSourceError | 6 |
WemapVPSARCoreLocationSourceListener / …Observer | states / scanStatuses / errors / … | 5, 8 |
Getting help
Sample apps: wemap-sdk-sample-apps-android. Anything unclear or missing here — contact the Wemap team.