Replace 2GIS MapKit with OpenStreetMap (osmdroid) — no key needed
Getting a working mobile-SDK key from 2GIS turned out to require a sales-mediated B2B process (Platform Manager subscription, demo keys explicitly excluded from mobile SDK use per their own docs), not a quick self-serve signup. Swapped to osmdroid instead: free, no API key or account, works immediately. CityMapView (replacing DgisMapView/DgisSdkProvider) wraps osmdroid's View-based MapView via AndroidView, forwarding lifecycle events, with markers for the user's location and each nearby place. Configured required OSM tile-usage-policy user agent + app-private tile cache in GuideCityApp. Removed the now-unused DGIS_API_KEY plumbing and the arm64-only ABI filter (osmdroid has no heavy native libs, so it's not needed) — APK is back down to ~19.6MB from ~69MB. Verified: testDebugUnitTest passes, assembleDebug produces a working APK, sent via the Telegram bot (previously blocked by its 50MB limit with the 2GIS build). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
7c4d5727e4
commit
37edc96d50
12 changed files with 147 additions and 220 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -27,7 +27,6 @@ android/captures/
|
||||||
android/.externalNativeBuild/
|
android/.externalNativeBuild/
|
||||||
*.keystore
|
*.keystore
|
||||||
*.jks
|
*.jks
|
||||||
android/app/src/main/assets/dgissdk.key
|
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
|
||||||
43
README.md
43
README.md
|
|
@ -9,7 +9,7 @@ voice while showing a swipeable card stack. Starting city: Moscow.
|
||||||
```
|
```
|
||||||
┌────────────────────┐
|
┌────────────────────┐
|
||||||
│ Android app │ Kotlin + Jetpack Compose
|
│ Android app │ Kotlin + Jetpack Compose
|
||||||
│ (android/) │ 2GIS map, on-device TTS,
|
│ (android/) │ OSM map, on-device TTS,
|
||||||
└─────────┬──────────┘ location, Room favorites
|
└─────────┬──────────┘ location, Room favorites
|
||||||
│ HTTPS / REST (JSON)
|
│ HTTPS / REST (JSON)
|
||||||
┌─────────▼──────────┐
|
┌─────────▼──────────┐
|
||||||
|
|
@ -25,7 +25,7 @@ voice while showing a swipeable card stack. Starting city: Moscow.
|
||||||
- `backend/` — FastAPI service exposing city/place/nearby-search endpoints,
|
- `backend/` — FastAPI service exposing city/place/nearby-search endpoints,
|
||||||
backed by PostgreSQL+PostGIS. See `backend/` for details.
|
backed by PostgreSQL+PostGIS. See `backend/` for details.
|
||||||
- `android/` — Kotlin/Compose app skeleton (MVVM, Hilt, Retrofit, Room,
|
- `android/` — Kotlin/Compose app skeleton (MVVM, Hilt, Retrofit, Room,
|
||||||
DataStore, 2GIS MapKit, on-device TextToSpeech).
|
DataStore, OpenStreetMap via osmdroid, on-device TextToSpeech).
|
||||||
|
|
||||||
## Backend quickstart
|
## Backend quickstart
|
||||||
|
|
||||||
|
|
@ -56,7 +56,7 @@ which works from anywhere (not just the local network) and is the preferred
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp android/local.properties.example android/local.properties
|
cp android/local.properties.example android/local.properties
|
||||||
# fill in sdk.dir and DGIS_API_KEY (test key: b4df01a8-61db-4cb9-8286-7e069495987d)
|
# fill in sdk.dir (and API_BASE_URL if not using the default)
|
||||||
cd android && ./gradlew :app:assembleDebug
|
cd android && ./gradlew :app:assembleDebug
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -68,39 +68,18 @@ 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
|
## Map
|
||||||
|
|
||||||
The app depends on the real 2GIS MapKit SDK (`ru.dgis.sdk:sdk-map` +
|
Uses OpenStreetMap tiles via [osmdroid](https://github.com/osmdroid/osmdroid)
|
||||||
`compose-map`, from `https://artifactory.2gis.dev/sdk-maven-release`, wired
|
(`map/CityMapView.kt`) — free, no API key or account needed, works out of
|
||||||
up in `settings.gradle.kts`/`app/build.gradle.kts`). It needs a **separate**
|
the box. (An earlier iteration tried the native 2GIS MapKit SDK instead;
|
||||||
key from the `DGIS_API_KEY` above:
|
dropped because getting a working mobile-SDK key from 2GIS turned out to be
|
||||||
|
a sales-mediated B2B process, not a quick self-serve signup — see git
|
||||||
- `DGIS_API_KEY` (the `b4df01a...` value) is a 2GIS **public REST/JS API**
|
history if that's ever worth revisiting.)
|
||||||
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
|
Nearby-search radius caps at 10km.
|
||||||
real 2GIS map (only the placeholder does that) — follow-up work.
|
|
||||||
|
|
||||||
The 2GIS REST/JS API key above is used directly via
|
|
||||||
`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.
|
|
||||||
|
|
|
||||||
|
|
@ -32,24 +32,11 @@ android {
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
buildConfigField(
|
|
||||||
"String",
|
|
||||||
"DGIS_API_KEY",
|
|
||||||
"\"${localProp("DGIS_API_KEY", "")}\"",
|
|
||||||
)
|
|
||||||
buildConfigField(
|
buildConfigField(
|
||||||
"String",
|
"String",
|
||||||
"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 {
|
||||||
|
|
@ -118,12 +105,8 @@ 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. Requires a real dgissdk.key in app/src/main/assets/,
|
// OpenStreetMap tiles via osmdroid — free, no API key/registration needed.
|
||||||
// obtained per-app from dev.2gis.com — see README for details. Without
|
implementation(libs.osmdroid.android)
|
||||||
// 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)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package com.guidecity.app
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
import org.osmdroid.config.Configuration
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
private const val TAG = "GuideCityApp"
|
private const val TAG = "GuideCityApp"
|
||||||
|
|
||||||
|
|
@ -11,5 +13,20 @@ class GuideCityApp : Application() {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
Log.i(TAG, "Application created (debug=${BuildConfig.DEBUG}, apiBaseUrl=${BuildConfig.API_BASE_URL})")
|
Log.i(TAG, "Application created (debug=${BuildConfig.DEBUG}, apiBaseUrl=${BuildConfig.API_BASE_URL})")
|
||||||
|
configureOsmdroid()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenStreetMap's tile usage policy requires a distinct user agent per
|
||||||
|
* app. Cache paths point at app-private storage so no storage
|
||||||
|
* permission is needed.
|
||||||
|
*/
|
||||||
|
private fun configureOsmdroid() {
|
||||||
|
val osmdroidDir = File(cacheDir, "osmdroid")
|
||||||
|
Configuration.getInstance().apply {
|
||||||
|
userAgentValue = BuildConfig.APPLICATION_ID
|
||||||
|
osmdroidBasePath = osmdroidDir
|
||||||
|
osmdroidTileCache = File(osmdroidDir, "tiles")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
113
android/app/src/main/java/com/guidecity/app/map/CityMapView.kt
Normal file
113
android/app/src/main/java/com/guidecity/app/map/CityMapView.kt
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
package com.guidecity.app.map
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
|
import com.guidecity.app.data.remote.dto.NearbyPlaceDto
|
||||||
|
import com.guidecity.app.location.LatLon
|
||||||
|
import com.guidecity.app.theme.Mocha
|
||||||
|
import org.osmdroid.util.GeoPoint
|
||||||
|
import org.osmdroid.views.MapView
|
||||||
|
import org.osmdroid.views.overlay.Marker
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenStreetMap tiles via osmdroid — free, no API key or registration
|
||||||
|
* needed (unlike the 2GIS native MapKit SDK, which requires a paid/sales
|
||||||
|
* process for a mobile SDK key; see git history if that's ever revisited).
|
||||||
|
*
|
||||||
|
* Wraps osmdroid's View-based [MapView] via [AndroidView] since it has no
|
||||||
|
* native Compose API, forwarding lifecycle events so tile loading/caching
|
||||||
|
* behaves correctly.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun CityMapView(
|
||||||
|
userLocation: LatLon?,
|
||||||
|
places: List<NearbyPlaceDto>,
|
||||||
|
onPlaceClick: (placeId: Int) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
if (userLocation == null) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Mocha.Mantle),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Waiting for location…",
|
||||||
|
color = Mocha.Subtext1,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
var mapViewRef by remember { mutableStateOf<MapView?>(null) }
|
||||||
|
|
||||||
|
DisposableEffect(lifecycleOwner) {
|
||||||
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
|
when (event) {
|
||||||
|
Lifecycle.Event.ON_RESUME -> mapViewRef?.onResume()
|
||||||
|
Lifecycle.Event.ON_PAUSE -> mapViewRef?.onPause()
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleOwner.lifecycle.addObserver(observer)
|
||||||
|
onDispose {
|
||||||
|
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||||
|
mapViewRef?.onDetach()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AndroidView(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
factory = { context ->
|
||||||
|
MapView(context).apply {
|
||||||
|
setMultiTouchControls(true)
|
||||||
|
controller.setZoom(15.0)
|
||||||
|
mapViewRef = this
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update = { mapView ->
|
||||||
|
mapView.controller.setCenter(GeoPoint(userLocation.lat, userLocation.lon))
|
||||||
|
mapView.overlays.clear()
|
||||||
|
|
||||||
|
mapView.overlays.add(
|
||||||
|
Marker(mapView).apply {
|
||||||
|
position = GeoPoint(userLocation.lat, userLocation.lon)
|
||||||
|
title = "Вы здесь"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
places.forEach { place ->
|
||||||
|
mapView.overlays.add(
|
||||||
|
Marker(mapView).apply {
|
||||||
|
position = GeoPoint(place.location.lat, place.location.lon)
|
||||||
|
title = place.content.title
|
||||||
|
setOnMarkerClickListener { _, _ ->
|
||||||
|
onPlaceClick(place.id)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
mapView.invalidate()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
package com.guidecity.app.map
|
|
||||||
|
|
||||||
import androidx.compose.foundation.Canvas
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
|
||||||
import com.guidecity.app.data.remote.dto.NearbyPlaceDto
|
|
||||||
import com.guidecity.app.location.LatLon
|
|
||||||
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
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*
|
|
||||||
* Follow-up not yet implemented: plotting [places] as markers on the real
|
|
||||||
* map (needs the SDK's MapObjectManager/Marker API) — for now the real map
|
|
||||||
* only centers on [userLocation]; the placeholder still shows place dots.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun DgisMapView(
|
|
||||||
sdkContext: DgisContext?,
|
|
||||||
userLocation: LatLon?,
|
|
||||||
places: List<NearbyPlaceDto>,
|
|
||||||
onPlaceClick: (placeId: Int) -> Unit,
|
|
||||||
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(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(Mocha.Mantle),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
if (userLocation == null) {
|
|
||||||
Text(
|
|
||||||
text = "Map placeholder — waiting for location",
|
|
||||||
color = Mocha.Subtext1,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
)
|
|
||||||
return@Box
|
|
||||||
}
|
|
||||||
|
|
||||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
|
||||||
val centerX = size.width / 2f
|
|
||||||
val centerY = size.height / 2f
|
|
||||||
val metersPerDegreeLat = 111_320.0
|
|
||||||
val metersPerDegreeLon = 111_320.0 * cos(Math.toRadians(userLocation.lat))
|
|
||||||
val pixelsPerMeter = 0.6f
|
|
||||||
|
|
||||||
fun offsetFor(lat: Double, lon: Double): Offset {
|
|
||||||
val dyMeters = (lat - userLocation.lat) * metersPerDegreeLat
|
|
||||||
val dxMeters = (lon - userLocation.lon) * metersPerDegreeLon
|
|
||||||
return Offset(
|
|
||||||
x = centerX + (dxMeters * pixelsPerMeter).toFloat(),
|
|
||||||
y = centerY - (dyMeters * pixelsPerMeter).toFloat(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// User location marker.
|
|
||||||
drawCircle(color = Mocha.Blue, radius = 14f, center = Offset(centerX, centerY))
|
|
||||||
drawCircle(
|
|
||||||
color = Mocha.Blue,
|
|
||||||
radius = 22f,
|
|
||||||
center = Offset(centerX, centerY),
|
|
||||||
style = Stroke(width = 3f),
|
|
||||||
)
|
|
||||||
|
|
||||||
places.forEach { place ->
|
|
||||||
val point = offsetFor(place.location.lat, place.location.lon)
|
|
||||||
drawCircle(color = Mocha.Peach, radius = 10f, center = point)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -39,7 +39,7 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import com.guidecity.app.R
|
import com.guidecity.app.R
|
||||||
import com.guidecity.app.map.DgisMapView
|
import com.guidecity.app.map.CityMapView
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MapScreen(
|
fun MapScreen(
|
||||||
|
|
@ -84,8 +84,7 @@ fun MapScreen(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding),
|
.padding(padding),
|
||||||
) {
|
) {
|
||||||
DgisMapView(
|
CityMapView(
|
||||||
sdkContext = viewModel.dgisSdkContext,
|
|
||||||
userLocation = resolvedLocation,
|
userLocation = resolvedLocation,
|
||||||
places = nearbyPlaces,
|
places = nearbyPlaces,
|
||||||
onPlaceClick = {},
|
onPlaceClick = {},
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ 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
|
||||||
|
|
@ -35,12 +34,8 @@ 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,7 +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"
|
osmdroid = "6.1.20"
|
||||||
|
|
||||||
[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" }
|
||||||
|
|
@ -63,8 +63,7 @@ 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" }
|
osmdroid-android = { group = "org.osmdroid", name = "osmdroid-android", version.ref = "osmdroid" }
|
||||||
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" }
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,6 @@
|
||||||
|
|
||||||
sdk.dir=/path/to/your/Android/Sdk
|
sdk.dir=/path/to/your/Android/Sdk
|
||||||
|
|
||||||
# Test key provided for development; do not ship this as-is in a public repo.
|
|
||||||
DGIS_API_KEY=b4df01a8-61db-4cb9-8286-7e069495987d
|
|
||||||
|
|
||||||
# Base URL of the guideCity backend API. Options, in order of preference:
|
# Base URL of the guideCity backend API. Options, in order of preference:
|
||||||
# 1. Nginx-fronted HTTPS domain (works from anywhere, not just the LAN):
|
# 1. Nginx-fronted HTTPS domain (works from anywhere, not just the LAN):
|
||||||
API_BASE_URL=https://guidetest.vrubel.xyz/
|
API_BASE_URL=https://guidetest.vrubel.xyz/
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ dependencyResolutionManagement {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven { url = uri("https://artifactory.2gis.dev/sdk-maven-release") }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue