Wire up the real 2GIS MapKit SDK (was a placeholder)

Added the actual ru.dgis.sdk:sdk-map + compose-map dependencies (from
https://artifactory.2gis.dev/sdk-maven-release) instead of the Canvas
placeholder guess. Verified the exact API surface by downloading the
AARs directly and inspecting classes with javap (DGis.initialize,
MapOptions, MapComposableState/MapComposable, CameraPosition/GeoPoint)
rather than relying on possibly-stale docs.

DgisSdkProvider lazily calls DGis.initialize() once per process,
catching failure so a missing key doesn't crash the app; DgisMapView
renders the real MapComposable when that succeeds, falling back to
the old placeholder canvas otherwise.

Important finding: the existing DGIS_API_KEY (2GIS's public REST/JS
API key format) does NOT work with this native SDK. It requires a
separate dgissdk.key file issued per-app-package from dev.2gis.com,
placed in app/src/main/assets/ (gitignored). Documented in README.
Without it the app still runs fine on the placeholder map.

Also restricted ndk.abiFilters to arm64-v8a — the SDK's native libs
otherwise balloon the APK from ~19MB to ~190MB.

Verified: testDebugUnitTest passes, assembleDebug produces a working
(arm64-only, ~69MB) APK. Not runtime-verified against a real map key
or device — no emulator/device available in this environment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
vrubelroman 2026-07-09 21:28:52 +00:00
parent 7f1a6d51fc
commit 7c4d5727e4
9 changed files with 134 additions and 20 deletions

1
.gitignore vendored
View file

@ -27,6 +27,7 @@ android/captures/
android/.externalNativeBuild/ android/.externalNativeBuild/
*.keystore *.keystore
*.jks *.jks
android/app/src/main/assets/dgissdk.key
# IDE # IDE
.vscode/ .vscode/

View file

@ -68,11 +68,39 @@ Wi-Fi/LAN.
Run unit tests before building — `./gradlew testDebugUnitTest`, then Run unit tests before building — `./gradlew testDebugUnitTest`, then
`./gradlew :app:assembleDebug`. `./gradlew :app:assembleDebug`.
## 2GIS map SDK
The app depends on the real 2GIS MapKit SDK (`ru.dgis.sdk:sdk-map` +
`compose-map`, from `https://artifactory.2gis.dev/sdk-maven-release`, wired
up in `settings.gradle.kts`/`app/build.gradle.kts`). It needs a **separate**
key from the `DGIS_API_KEY` above:
- `DGIS_API_KEY` (the `b4df01a...` value) is a 2GIS **public REST/JS API**
key — not used by the native SDK at all currently.
- The native MapKit SDK instead needs a **`dgissdk.key` file**, issued
per-app (tied to the package name `com.guidecity.app`) from
https://dev.2gis.com/. Place it at `android/app/src/main/assets/dgissdk.key`
(gitignored — don't commit it).
Without that key file, `DgisSdkProvider` fails to initialize (logged, not
crashed) and `DgisMapView` falls back to a placeholder canvas rendering
(user location + nearby place dots, no real map tiles). Once a real key is
in place, the real map should render automatically — no code changes needed.
Also note: the SDK bundles native libraries per CPU architecture, which
balloons APK size a lot (~19MB → ~190MB unfiltered). `app/build.gradle.kts`
restricts `ndk.abiFilters` to `arm64-v8a` only (~69MB) since that covers
virtually all real devices today — remove that filter if you need to test
on an x86 emulator or 32-bit device.
## Notes / current scope ## Notes / current scope
This is iteration 1: Moscow only (Gorky Park, Red Square area, Moscow-City), This is iteration 1: Moscow only (Gorky Park, Red Square area, Moscow-City),
no user accounts (favorites are local/Room-only), no speed/heading-aware no user accounts (favorites are local/Room-only), no speed/heading-aware
auto content-length selection yet (reserved API params exist, unused). auto content-length selection yet (reserved API params exist, unused).
Nearby-search radius caps at 10km. Place markers aren't yet plotted on the
real 2GIS map (only the placeholder does that) — follow-up work.
The 2GIS API key above is used directly via `local.properties`/`BuildConfig` The 2GIS REST/JS API key above is used directly via
for development convenience — don't ship it as-is in a public repo. `local.properties`/`BuildConfig` for development convenience — don't ship it
as-is in a public repo. Same goes for `dgissdk.key` once you have one.

View file

@ -42,6 +42,14 @@ android {
"API_BASE_URL", "API_BASE_URL",
"\"${localProp("API_BASE_URL", "http://10.0.2.2:8000/")}\"", "\"${localProp("API_BASE_URL", "http://10.0.2.2:8000/")}\"",
) )
// The 2GIS SDK ships native libs for every ABI, which balloons the APK
// (~19MB -> ~190MB unfiltered). Almost all real devices are arm64
// today, and this keeps debug builds small enough to send anywhere
// (e.g. under Telegram bot's 50MB upload limit).
ndk {
abiFilters += "arm64-v8a"
}
} }
buildTypes { buildTypes {
@ -110,9 +118,12 @@ dependencies {
implementation(libs.play.services.location) implementation(libs.play.services.location)
implementation(libs.kotlinx.coroutines.play.services) implementation(libs.kotlinx.coroutines.play.services)
// 2GIS MapKit SDK: add once the exact Maven coordinates/repository are // 2GIS MapKit SDK. Requires a real dgissdk.key in app/src/main/assets/,
// confirmed from https://docs.2gis.com/ — see map/DgisMapView.kt for the // obtained per-app from dev.2gis.com — see README for details. Without
// isolated integration point in the meantime. // it, DgisSdkProvider's init fails gracefully and DgisMapView falls back
// to its placeholder rendering.
implementation(libs.dgis.sdk.map)
implementation(libs.dgis.compose.map)
testImplementation(libs.junit) testImplementation(libs.junit)
testImplementation(libs.mockk) testImplementation(libs.mockk)

View file

@ -7,37 +7,64 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp
import com.guidecity.app.data.remote.dto.NearbyPlaceDto import com.guidecity.app.data.remote.dto.NearbyPlaceDto
import com.guidecity.app.location.LatLon import com.guidecity.app.location.LatLon
import com.guidecity.app.theme.Mocha import com.guidecity.app.theme.Mocha
import ru.dgis.sdk.Context as DgisContext
import ru.dgis.sdk.coordinates.Bearing
import ru.dgis.sdk.coordinates.GeoPoint
import ru.dgis.sdk.coordinates.Latitude
import ru.dgis.sdk.coordinates.Longitude
import ru.dgis.sdk.compose.map.MapComposable
import ru.dgis.sdk.compose.map.MapComposableState
import ru.dgis.sdk.map.CameraPosition
import ru.dgis.sdk.map.MapOptions
import ru.dgis.sdk.map.Tilt
import ru.dgis.sdk.map.Zoom
import kotlin.math.cos import kotlin.math.cos
/** /**
* Isolated integration point for the 2GIS MapKit SDK. * Renders the real 2GIS MapKit map when the SDK initialized successfully
* (i.e. a valid `dgissdk.key` is present see [DgisSdkProvider]); otherwise
* falls back to a lightweight placeholder (user location centered, nearby
* places plotted by relative lat/lon offset) so the rest of the app
* permission flow, nearby fetch, navigation to place detail stays fully
* functional and demoable without a real key.
* *
* This currently renders a lightweight placeholder (user location centered, * Follow-up not yet implemented: plotting [places] as markers on the real
* nearby places plotted by relative lat/lon offset) so the rest of the app * map (needs the SDK's MapObjectManager/Marker API) for now the real map
* permission flow, nearby fetch, navigation to place detail is fully * only centers on [userLocation]; the placeholder still shows place dots.
* functional and demoable before the real SDK is wired in.
*
* To integrate the real map: replace the Canvas placeholder below with the
* 2GIS MapKit view (see android/build.gradle.kts and settings.gradle.kts for
* the pending dependency/repository TODOs the exact Maven coordinates need
* confirming from https://docs.2gis.com/), keeping this function's signature
* so callers (MapScreen, GuideScreen) don't need to change.
*/ */
@Composable @Composable
fun DgisMapView( fun DgisMapView(
sdkContext: DgisContext?,
userLocation: LatLon?, userLocation: LatLon?,
places: List<NearbyPlaceDto>, places: List<NearbyPlaceDto>,
onPlaceClick: (placeId: Int) -> Unit, onPlaceClick: (placeId: Int) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
if (sdkContext != null && userLocation != null) {
val mapState = remember(userLocation) {
MapComposableState(
MapOptions().apply {
position = CameraPosition(
point = GeoPoint(Latitude(userLocation.lat), Longitude(userLocation.lon)),
zoom = Zoom(15f),
tilt = Tilt(),
bearing = Bearing(),
)
},
)
}
MapComposable(modifier = modifier.fillMaxSize(), state = mapState)
return
}
Box( Box(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()

View file

@ -0,0 +1,39 @@
package com.guidecity.app.map
import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import ru.dgis.sdk.Context as DgisContext
import ru.dgis.sdk.DGis
import javax.inject.Inject
import javax.inject.Singleton
private const val TAG = "DgisSdkProvider"
/**
* Lazily initializes the 2GIS MapKit SDK once per process.
*
* Requires a real `dgissdk.key` file in `app/src/main/assets/`, issued
* per-app (tied to the package name) from https://dev.2gis.com/ — the
* `DGIS_API_KEY` used elsewhere in this app (2GIS's public REST/JS API key
* format) is a *different* product and will not work here. Without a valid
* key file, [sdkContext] is null and [DgisMapView] falls back to its
* placeholder rendering instead of crashing.
*/
@Singleton
class DgisSdkProvider @Inject constructor(
@ApplicationContext private val androidContext: android.content.Context,
) {
val sdkContext: DgisContext? by lazy {
runCatching { DGis.initialize(androidContext) }
.onSuccess { Log.i(TAG, "2GIS SDK initialized") }
.onFailure {
Log.e(
TAG,
"2GIS SDK initialization failed — is a valid dgissdk.key present in " +
"app/src/main/assets/? (see README)",
it,
)
}
.getOrNull()
}
}

View file

@ -85,6 +85,7 @@ fun MapScreen(
.padding(padding), .padding(padding),
) { ) {
DgisMapView( DgisMapView(
sdkContext = viewModel.dgisSdkContext,
userLocation = resolvedLocation, userLocation = resolvedLocation,
places = nearbyPlaces, places = nearbyPlaces,
onPlaceClick = {}, onPlaceClick = {},

View file

@ -9,6 +9,7 @@ import com.guidecity.app.data.repository.PlacesRepository
import com.guidecity.app.location.LatLon import com.guidecity.app.location.LatLon
import com.guidecity.app.location.LocationProvider import com.guidecity.app.location.LocationProvider
import com.guidecity.app.location.SelectedLocationHolder import com.guidecity.app.location.SelectedLocationHolder
import com.guidecity.app.map.DgisSdkProvider
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -34,8 +35,12 @@ class MapViewModel @Inject constructor(
private val locationProvider: LocationProvider, private val locationProvider: LocationProvider,
private val selectedLocationHolder: SelectedLocationHolder, private val selectedLocationHolder: SelectedLocationHolder,
private val placesRepository: PlacesRepository, private val placesRepository: PlacesRepository,
dgisSdkProvider: DgisSdkProvider,
) : ViewModel() { ) : ViewModel() {
/** Null until a valid `dgissdk.key` is present — see [DgisSdkProvider]. */
val dgisSdkContext = dgisSdkProvider.sdkContext
private val _uiState = MutableStateFlow<MapUiState>(MapUiState.CheckingPermission) private val _uiState = MutableStateFlow<MapUiState>(MapUiState.CheckingPermission)
val uiState: StateFlow<MapUiState> = _uiState.asStateFlow() val uiState: StateFlow<MapUiState> = _uiState.asStateFlow()

View file

@ -21,6 +21,7 @@ androidxTestExtJunit = "1.2.1"
espressoCore = "3.6.1" espressoCore = "3.6.1"
mockk = "1.13.12" mockk = "1.13.12"
kotlinxCoroutinesTest = "1.8.1" kotlinxCoroutinesTest = "1.8.1"
dgisSdk = "13.5.0"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -62,6 +63,9 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" }
dgis-sdk-map = { group = "ru.dgis.sdk", name = "sdk-map", version.ref = "dgisSdk" }
dgis-compose-map = { group = "ru.dgis.sdk", name = "compose-map", version.ref = "dgisSdk" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

View file

@ -11,9 +11,7 @@ dependencyResolutionManagement {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
// TODO: add the 2GIS MapKit maven repository here once its exact URL is maven { url = uri("https://artifactory.2gis.dev/sdk-maven-release") }
// confirmed from the 2GIS developer portal (see android/README section
// on the map SDK integration point).
} }
} }