Skip to main content
Version: 1.x (beta)

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.instance and WemapMapSDK.instance are gone; a CoreSession or MapSession loads the map data and is shared by the views and location sources of one screen.
  • Global mutable constants → immutable configs. CoreConstants, MapConstants, ARConstants, VPSControllerConstants and StateManagerConstants are replaced by data class configs, fixed at creation time.
  • Listeners → Flow. Every *Listener / *Observer interface on a manager or a location source is removed in favour of a read-only Flow property.
  • Kotlin idiom throughout. Acronyms are no longer all-caps in symbol names, every public Float scalar is a Double, time-valued members are kotlin.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 compileSdk 37 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.xv1.0
Kotlin1.9 — no pin, so whatever the 2.0.20 compiler emitted2.2 — set by the kotlinx-serialization :core re-exposes
JVM target1.81117 for :geo-ar and :geo-ar-compose
compileSdk3637

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.xv1.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.xv1.0Passed to
CoreConstants staticsSessionConfig (com.getwemap.sdk.core.configs)CoreSession.create / MapSession.create
WemapCoreSDK.setEnvironment(…) / CoreConstants.ENVIRONMENTSessionConfig.environmentCoreSession.create / MapSession.create
WemapCoreSDK.setItinerariesEnvironment(…)SessionConfig.directionsEnvironmentCoreSession.create / MapSession.create
MapConstants staticsMapViewConfig (com.getwemap.sdk.map.configs)mapView.configure(session, config)
ARConstants + ARConstants.DirectionalArrowGeoARViewConfig + GeoARViewConfig.DirectionalArrowgeoARView.configure(session, config)
VPSControllerConstants, StateManagerConstantsVpsConfig — see step 8VpsARCoreLocationSource(…)

Property-by-property:

v0.xv1.0
CoreConstants.ITINERARY_RECALCULATION_ENABLEDSessionConfig.itineraryRecalculationEnabled
CoreConstants.USER_LOCATION_PROJECTION_ON_ITINERARY_ENABLEDSessionConfig.userLocationProjectionOnItineraryEnabled
CoreConstants.USER_LOCATION_PROJECTION_ON_GRAPH_ENABLEDSessionConfig.userLocationProjectionOnGraphEnabled
MapConstants.SWITCH_LEVELS_AUTOMATICALLY_ON_USER_MOVEMENTSMapViewConfig.switchLevelsAutomaticallyOnUserMovements
MapConstants.STALE_TIMEOUT_MILLISECONDS (Long, ms)MapViewConfig.staleStateTimeout (Duration)
ARConstants.NAVIGATION_VISIBILITY_DISTANCEGeoARViewConfig.navigationVisibilityDistance — now nullable
ARConstants.STEP_INSTRUCTION_ALTITUDEGeoARViewConfig.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:

  • MapConstants moved from map.helpers to map.configs and keeps only WEMAP_BLUE. ARConstants is internal.
  • The Environment sealed class and IEnvironment were replaced by the com.getwemap.sdk.core.configs.Environment enumEnvironment.DEV / Environment.PROD. Environment.Prod() no longer compiles.
  • CoreConstants.ITINERARIES_HOST / ITINERARIES_BASE_URL were stored and settable; SessionConfig.directionsBaseUrl is computed from directionsEnvironment, 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.xv1.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: Longtimestamp: Long
location: LocationtoLocation()
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 :mapcom.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 listenerv1.0 flows
LocationSource.listener (LocationSourceListener)coordinates / attitudes / errors
INavigationManager.addListener / removeListenernavigationEvents (sealed NavigationEventStarted / Stopped / Arrived / Recalculated) / navigationInfoUpdates / errors
IPointOfInterestManager.addListener / removeListenerselectionUpdates (PointOfInterestSelectionUpdate) / touchedPois
BuildingManager.addListener / removeListenerfocusedBuildings / activeLevelChanges / errors
UserLocationManager and ARLocationManager listenerscoordinates / attitudes / errors
UserLocationManager.coordinateFlowcoordinates
WemapVPSARCoreLocationSourceListener / …Observersee step 8
view loading callbackssee 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 suspend in 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

POIPoi, VPSVps, GPSGps, URLUrl, IDId, ECEFEcef.

// before
pointOfInterestManager.addPOI(poi)
pointOfInterestManager.hideAllPOIs()
val selected = pointOfInterestManager.getSelectedPOI()

// after
pointOfInterestManager.addPoi(poi)
pointOfInterestManager.hideAllPois()
val selected = pointOfInterestManager.getSelectedPoi()
v0.xv1.0
getPOIs, addPOI, addPOIs, removePOI, removePOIsgetPois, addPoi, addPois, removePoi, removePois
showPOI, hidePOI, showPOIs, hidePOIs, showAllPOIs, hideAllPOIsshowPoi, hidePoi, showPois, hidePois, showAllPois, hideAllPois
getSelectedPOI, getSelectedPOIs, unselectPOI, unselectAllPOIsgetSelectedPoi, getSelectedPois, unselectPoi, unselectAllPois
selectPOI, centerToPOIselectPoi, centerToPoi
sortPOIsByGraphDistance, sortPOIsByDurationsortPoisByGraphDistance, sortPoisByDuration
GPSLocationSourceGpsLocationSource
WemapVPSARCoreLocationSourceVpsARCoreLocationSource
isVPSAvailableAt, distanceToVPSCoverageFromisVpsAvailableAt, distanceToVpsCoverageFrom
PointOfInterest.imageURL / mediaURL / mediaThumbnailURLimageUrl / 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

IStringConvertibleCompactCompactStringConvertible, 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.xv1.0
NavigationOptions.navigationRecalculationTimeInterval (raw)same name, Duration
CoreConstants POI timeoutSessionConfig.pointsOfInterestLoadingTimeout (Duration)
MapConstants.STALE_TIMEOUT_MILLISECONDSMapViewConfig.staleStateTimeout (Duration)
VPSControllerConstants.BACKGROUND_SCAN_TIME_INTERVALVpsControllerConfig.backgroundScanTimeInterval (Duration)
INavigationManager.infoUpdatePeriod + infoUpdateTimeUnitnavigationInfoUpdatesInterval (one Duration)
SimulationOptions.dispatchPeriod + timeUnittimeInterval (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.xv1.0
NavigationError.noActiveNavigation and siblingsNavigationError.NoActiveNavigation, FailedToAddNavigation, FailedToRemoveNavigation, FailedToRetrieveUserLocation, NavigationAlreadyExists, FailedToRecalculateNavigation, plus a new Unknown
LocationSourceError.trackingLostLocationSourceError.TrackingLost
ItineraryServiceErrorDirectionsServiceErrorNoItinerariesFound(reason), NotSupportedTravelMode, InvalidRules, and the new RequestFailed(code, reason) and GraphUnavailable
WemapVPSARCoreLocationSourceErrorVpsARCoreLocationSourceErrorConnectionTimeout, NotConnectedToInternet, SlowConnectionDetected, TiltTooHigh, FailedToDownloadVpsCoverage, FailedToDownloadLevelsMapping, FailedToCalculateDistanceToVpsCoverage, Unknown

Services and managers: ids bound once, results renamed

v0.xv1.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.itinerariesdrawnItineraries
ItineraryManager.getItineraries(…)computeItineraries(…)
ItineraryManager.searchRuleNames(graphId)searchRuleNames()
ItinerarySearchRules in core.model.services.parameterscore.model.services
ItineraryInfo in core.model.services.responsescore.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.xv1.0
WemapMapView.getMapViewAsync(OnMapViewReadyCallback) and the lambda overloadloadPhases: StateFlow<LoadPhase> + suspend fun awaitLoaded()
GeoARView.getARViewAsync(OnGeoARViewReadyCallback) and the lambda overloadsame
OnMapViewReadyCallback, OnGeoARViewReadyCallbackremoved
WemapMapView.isLoaded / GeoARView.isLoadedloadPhase.isReady / loadPhase.isLoading / loadPhase.error
WemapMapView.mapData / GeoARView.mapDataremoved — 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 compileWemapMapView 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.xv1.0
State.NOT_POSITIONING + notPositioningReasonState.NotPositioning(reason)
State.DEGRADED_POSITIONING + degradedPositioningReasonState.DegradedPositioning(reason)
State.ACCURATE_POSITIONINGState.AccuratePositioning
comparing to enum constantsis State.X, or the state.isAccurate / state.isLost predicates
trackingFailureReasonthe 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.xv1.0
IPackdataManagerPackdataService, 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 fileNamefilePath 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.xv1.0
LocationSource.listener (LocationSourceListener)coordinates: Flow<Coordinate> / attitudes: Flow<Attitude> / errors: Flow<Throwable>
LocationSource.supportsHeadingsupportsAttitude
SimulationOptions.simulateHeadingsimulateAttitude

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".

  • MapData and MapDataSerializer
  • CoreConstants
  • LocationFactory
  • Coordinate.ecef / cartesian / ecefToEusRot / eusToEcefRot, and the fromECEF / fromCartesian / fromPoint / equals(lhs, rhs, eps, epsAlt) factories (step 4)
  • IItineraryService and the Directions wire types — ItineraryParameters, ItinerariesParametersMultiDestinations, IItineraryParametersBase, ItinerariesResponse, ItineraryInfoResponse, ItinerariesInfoResponse, Status, StatusCode, RuleNamesinternal rather than gated, except ItinerariesResponse and Status; IItineraryParametersBase is removed
  • MapConstants.VISUAL_DEBUGGER_CONFIG, CoreConstants.INCLUDE_PROJECTION_DISTANCE_INTO_ACCURACY and ARConstants.VISUAL_DEBUGGER_ENABLED (step 3)
  • the GeoARView camera-provider members — bindCameraProjection, bindCameraCapture, startCameraCapture, unbindCamera, onFrame, onCameraProjectionChanged, viewParameters (step 10)
  • GeoARView.addGeoNode and geoCamera, and the GeoNode / GeoCameraNode types themselves — internal, not gated, because GeoARView no longer extends SceneView (step 10)
  • GeoComponent
  • everything else that was still public in an *.internal.* package: the graph types (Edge, Vertex, Graph and their Properties), 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 / distanceTo and MultiPolygon.contains / distanceTo, and — in :mapStyleData, MapAttitudeEngine and StringGenerator
  • the rest of the Double math port, which stays in com.getwemap.sdk.core.internal.mathDouble2, Double4, Bool2/3/4, Mat2/3/4, VectorComponent, MatrixColumn, QuaternionComponent, RotationsOrder, and the top-level functions over them. Quaternion and Double3 are 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.

ChangeWhat to do
The views drive their own lifecycleyour mapView.onStart() / onResume() / … forwarding is logged and ignored — delete it (step 7)
A map that fails to load settles LoadPhase.FailedawaitLoaded() throws instead of never returning — handle it
isLoaded was false while loading and after a failureloadPhase.isReady for success, !loadPhase.isLoading for "has settled"
A coordinate's time is encoded in Unix secondsit was milliseconds on Android; if you persisted or forwarded encoded coordinates, re-check the unit
MapCameraState equality is epsilon-baseda camera you applied compares equal to the one the map reports back
The user-location indicator greys the moment tracking is lostit no longer stays blue for staleStateTimeout
startNavigation sets the camera mode itselfit follows with compass when navigation starts
A second WemapMapView or GeoARView on one session evicts the firstone view of each kind per session; a second map needs its own
LocationSource.deinit() unregisters the source from its sessionyour manual locationManager.locationSource = null at teardown is now redundant
A source your app started is no longer stopped by a viewthe SDK reference-counts only its own requests — stop what you started
NavigationInfo, Itinerary, Leg and TravelMode print a summarytoString() 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 nota listener that used to be called back at registration has no equivalent on an event flow — read the property

"I'm getting this error"

ErrorGo to
Unresolved reference: WemapCoreSDK / WemapMapSDKStep 2
Unresolved reference: ServiceFactory / DependencyManagerStep 2
Unresolved reference: mapData on a viewStep 2
Cannot access 'MapData': it is internal / unresolved after an import cleanupStep 2, Step 12
Suspend function 'create' should be called only from a coroutineStep 2
Val cannot be reassigned on a config propertyStep 3
Unresolved reference: CoreConstants / MapConstants / ARConstantsStep 3
Unresolved reference: Prod on EnvironmentStep 3
Type mismatch: inferred type is List<Float> but Levels was expectedStep 4
Unresolved reference: accuracy on CoordinateStep 4
Unresolved reference: location on CoordinateStep 4
Unresolved reference: Level after importing from core.model.entitiesStep 4
Unresolved reference: addListener on a managerStep 5
Unresolved reference: PointOfInterestManagerListenerStep 5
Unresolved reference: addPOI / getPOIs / selectPOI (any all-caps acronym)Step 6
Type mismatch: inferred type is Float but Double was expectedStep 6
Type mismatch: inferred type is Long but Duration was expectedStep 6
Incompatible types: NavigationError and NavigationError.CompanionStep 6
Unresolved reference: ItineraryServiceErrorStep 6
Unresolved reference: getMapViewAsync / getARViewAsyncStep 7
Unresolved reference: isLoadedStep 7
Unresolved reference: awaitLoadedStep 7 — add import com.getwemap.sdk.core.awaitLoaded
Unresolved reference: WemapVPSARCoreLocationSourceStep 8
Unresolved reference: NOT_POSITIONING / notPositioningReasonStep 8
Unresolved reference: IPackdataManagerStep 9
This class cannot be extended on GeoARViewStep 10
'listener' overrides nothing in a custom LocationSourceStep 11
'supportsHeading' overrides nothingStep 11
This declaration is experimental … @InternalWemapApiStep 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 symbolv1.0 replacementStep
ARConstantsGeoARViewConfig3
Attitude.accuracy: Float?Double?6
Coordinate.accuracyCoordinate.horizontalAccuracy (Double)4
Coordinate.ecefToEnuRot / enuToEcefRotremoved12
Coordinate.levels: List<Float>Coordinate.levels: Levels4
Coordinate.locationCoordinate.toLocation()4
Coordinate.timeCoordinate.timestamp4
CoreConstantsSessionConfig3
DependencyManagerCoreSession / MapSession2
Environment.Prod() / IEnvironmentEnvironment.PROD (enum)3
GPSLocationSourceGpsLocationSource6, 8
IARNavigationManagerARNavigationManager6
IARPointOfInterestManagerARPointOfInterestManager6
IItineraryProviderItineraryProvider6
IItineraryProvider.graph(id)graph()6
ILoadPhaseProviderLoadPhaseProvider6
IMapNavigationManagerMapNavigationManager6
IMapPointOfInterestManagerMapPointOfInterestManager6
IMapPointOfInterestManager.defaultZoom (settable)read-only6
INavigationManagerNavigationManager6
IPackdataManagerPackdataService9
IPointOfInterestManagerPointOfInterestManager6
IPointOfInterestManager.getPOIs()getPois()6
IPointOfInterestServicePointOfInterestService6
IPointOfInterestService.pointsOfInterestById(mapId, poiId)pointsOfInterest(id)6
IStringConvertibleCompactCompactStringConvertible6
ItineraryManager.getItineraries(…)computeItineraries(…)6
ItineraryManager.itinerariesdrawnItineraries6
ItineraryServiceErrorDirectionsServiceError6
IUserLocationProviderUserLocationProvider6
Level (core.model.entities)Level (map.buildings), no copy()4
LocationSource.listenercoordinates / attitudes / errors5, 11
LocationSource.supportsHeadingsupportsAttitude11
LocationSourceError.trackingLostLocationSourceError.TrackingLost6
MapConstants.STALE_TIMEOUT_MILLISECONDSMapViewConfig.staleStateTimeout (Duration)3, 6
MapDatasession.mapId / mapCenter / isVpsEnabled2, 12
NavigationError.noActiveNavigationNavigationError.NoActiveNavigation6
NavigationInfo.shortDescriptionNavigationInfo.toCompactString()6
NavigationInfoHandler()session.navigationInfoHandler6
NavigationInstructions.directionStep.direction12
OnMapViewReadyCallback / OnGeoARViewReadyCallbackloadPhases / awaitLoaded()7
PointOfInterest.customerIDremoved12
PointOfInterest.imageURLimageUrl6
PointOfInterestManagerListenerselectionUpdates / touchedPois5
PointOfInterestWithInfoPointOfInterestWithItineraryInfo6
ServiceFactoryCoreSession / MapSession2
SimulationOptions.simulateHeadingsimulateAttitude11
State (enum)State (sealed)8
StateManagerConstantsVpsStateManagerConfig3, 8
toStringCompact()toCompactString()6
VPSControllerConstantsVpsControllerConfig3, 8
WemapCoreSDK.instanceCoreSession2
WemapMapSDK.mapData(id, token)MapSession.create(context, mapId, token, config)2
WemapMapView.getMapViewAsyncawaitLoaded() / loadPhases7
WemapMapView.isLoadedloadPhase7
WemapVPSARCoreLocationSourceVpsARCoreLocationSource6, 8
WemapVPSARCoreLocationSourceErrorVpsARCoreLocationSourceError6
WemapVPSARCoreLocationSourceListener / …Observerstates / scanStatuses / errors / …5, 8

Getting help

Sample apps: wemap-sdk-sample-apps-android. Anything unclear or missing here — contact the Wemap team.