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:
parent
7f1a6d51fc
commit
7c4d5727e4
9 changed files with 134 additions and 20 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -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/
|
||||||
|
|
|
||||||
32
README.md
32
README.md
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 = {},
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
|
|
|
||||||
|
|
@ -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).
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue