Add SDK map module and migrate map fetching to Apollo with Hilt wiring.

This includes the new sdk module, MapService + mapper implementation, Apollo provider module, Hilt app integration, and the project .gitignore update.

Made-with: Cursor
This commit is contained in:
2026-02-28 16:38:52 +01:00
parent 54092f184f
commit 6dedaf0e8b
29 changed files with 292 additions and 215 deletions

View File

@@ -16,6 +16,11 @@ alwaysApply: true
- When referencing legacy implementation details, copy behavior intentionally into the new project rather than editing old files.
- If a task appears to require modifying the old project, stop and propose an equivalent change in `airmq-android-2026` instead.
## Naming convention
- Use `AirMQ` everywhere for product naming.
- Never use mixed-case variants in text, docs, comments, identifiers, filenames, or symbols.
## Baseline architecture and platform stack
Use this stack as the default foundation for all implementation work in `airmq-android-2026`:
@@ -23,7 +28,7 @@ Use this stack as the default foundation for all implementation work in `airmq-a
1. Jetpack Compose for UI.
2. Compose Navigation 3 as the in-app navigation system.
3. Firebase for mobile backend capabilities (for example auth, analytics, crash reporting, messaging, or remote config as needed).
4. Google Maps platform for map rendering and map-related interactions.
4. OpenStreetMap platform for map rendering and map-related interactions.
5. Apollo GraphQL for GraphQL schema integration, client generation, and network operations.
If a requested change conflicts with this baseline, ask for explicit approval before introducing an alternative.

4
.gitignore vendored
View File

@@ -7,9 +7,13 @@
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.idea/inspectionProfiles/
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
**/build/
.kotlin/
/.idea/**/workspace.xml

1
.idea/gradle.xml generated
View File

@@ -9,6 +9,7 @@
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/sdk" />
</set>
</option>
</GradleProjectSettings>

2
.idea/misc.xml generated
View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@@ -1,5 +1,7 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.hilt.android)
alias(libs.plugins.ksp)
alias(libs.plugins.kotlin.compose)
}
@@ -40,8 +42,15 @@ android {
}
dependencies {
// AirMQ SDK
implementation(project(":sdk"))
// Android
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
@@ -49,13 +58,18 @@ dependencies {
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.navigation.compose)
implementation(libs.hilt.android)
implementation(libs.androidx.hilt.navigation.compose)
ksp(libs.hilt.compiler)
// Firebase
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.analytics)
implementation(libs.firebase.crashlytics)
implementation(libs.firebase.messaging)
implementation(libs.google.maps.compose)
implementation(libs.play.services.maps)
implementation(libs.apollo.runtime)
implementation(libs.osmdroid.android)
// Tests
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

View File

@@ -1,24 +0,0 @@
package org.db3.airmq
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("org.db3.airmq", appContext.packageName)
}
}

View File

@@ -2,7 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".AirMQApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"

View File

@@ -1,10 +0,0 @@
query MapLocations {
locations {
_id
name
city
latitude
longitude
isOnline
}
}

View File

@@ -0,0 +1,7 @@
package org.db3.airmq
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class AirMQApplication : Application()

View File

@@ -6,16 +6,18 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import org.db3.airmq.features.navigation.AirMqNavGraph
import dagger.hilt.android.AndroidEntryPoint
import org.db3.airmq.features.navigation.AirMQNavGraph
import org.db3.airmq.ui.theme.AirMQTheme
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
AirMQTheme {
AirMqNavGraph(modifier = Modifier.fillMaxSize())
AirMQNavGraph(modifier = Modifier.fillMaxSize())
}
}
}

View File

@@ -1,110 +0,0 @@
package org.db3.airmq.features.map
import org.json.JSONArray
import org.json.JSONObject
import java.io.BufferedReader
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.net.URL
class AirMqMapApi {
fun fetchMapItems(): List<MapItem> {
val payload = JSONObject()
.put("query", LOCATIONS_QUERY)
.put("variables", JSONObject())
val connection = (URL(API_URL).openConnection() as HttpURLConnection).apply {
requestMethod = "POST"
connectTimeout = 15_000
readTimeout = 15_000
setRequestProperty("Content-Type", "application/json")
doInput = true
doOutput = true
}
return try {
OutputStreamWriter(connection.outputStream).use { writer ->
writer.write(payload.toString())
writer.flush()
}
val responseCode = connection.responseCode
val body = readResponseBody(connection, responseCode)
if (responseCode !in 200..299) {
throw IllegalStateException("Map API request failed: HTTP $responseCode")
}
parseMapItems(body)
} finally {
connection.disconnect()
}
}
private fun parseMapItems(rawJson: String): List<MapItem> {
val root = JSONObject(rawJson)
val errors = root.optJSONArray("errors")
if (errors != null && errors.length() > 0) {
throw IllegalStateException(firstGraphQlError(errors))
}
val data = root.optJSONObject("data")
?: throw IllegalStateException("Map API response is missing data")
val locations = data.optJSONArray("locations") ?: JSONArray()
return buildList {
for (index in 0 until locations.length()) {
val location = locations.optJSONObject(index) ?: continue
val id = location.optString("_id")
val latitude = location.optDouble("latitude", Double.NaN)
val longitude = location.optDouble("longitude", Double.NaN)
if (id.isBlank() || latitude.isNaN() || longitude.isNaN()) {
continue
}
add(
MapItem(
id = id,
title = location.optString("name").ifBlank { id },
city = location.optString("city").ifBlank { null },
latitude = latitude,
longitude = longitude,
isOnline = location.optBoolean("isOnline", false)
)
)
}
}
}
private fun readResponseBody(connection: HttpURLConnection, responseCode: Int): String {
val source = if (responseCode in 200..299) {
connection.inputStream
} else {
connection.errorStream ?: connection.inputStream
}
return source.bufferedReader().use(BufferedReader::readText)
}
private fun firstGraphQlError(errors: JSONArray): String {
val firstError = errors.optJSONObject(0)
return firstError?.optString("message").orEmpty().ifBlank {
"Map API returned an unknown GraphQL error"
}
}
companion object {
private const val API_URL = "https://api.airmq.cc"
private const val LOCATIONS_QUERY = """
query MapLocations {
locations {
_id
name
city
latitude
longitude
isOnline
}
}
"""
}
}

View File

@@ -1,5 +1,6 @@
package org.db3.airmq.features.map
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -12,6 +13,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@@ -24,8 +26,9 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.hilt.navigation.compose.hiltViewModel
import org.db3.airmq.features.common.AirMqContainedButton
import org.db3.airmq.sdk.map.domain.MapItem
import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
@@ -34,9 +37,42 @@ import org.osmdroid.views.overlay.Marker
@Composable
fun MapScreen(
viewModel: MapViewModel = viewModel()
viewModel: MapViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
val context = LocalContext.current
LaunchedEffect(uiState) {
Toast.makeText(context, uiState.items.count().toString(), Toast.LENGTH_LONG).show()
}
Box(modifier = Modifier.fillMaxSize()) {
AirMQMap(uiState.items)
if (uiState.isLoading) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.2f)),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(modifier = Modifier.size(40.dp))
}
}
if (uiState.errorMessage != null) {
ErrorOverlay(
message = uiState.errorMessage ?: "Unknown error",
onRetry = viewModel::refresh
)
}
}
}
@Composable
private fun AirMQMap(
items: List<MapItem>
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
@@ -65,47 +101,27 @@ fun MapScreen(
}
}
Box(modifier = Modifier.fillMaxSize()) {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { mapView },
update = { map ->
map.overlays.removeAll { it is Marker }
uiState.items.forEach { item ->
val marker = Marker(map).apply {
position = GeoPoint(item.latitude, item.longitude)
title = listOfNotNull(item.title, item.city).joinToString(" - ")
subDescription = if (item.isOnline) "Online" else "Offline"
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
}
map.overlays.add(marker)
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { mapView },
update = { map ->
map.overlays.removeAll { it is Marker }
items.forEach { item ->
val marker = Marker(map).apply {
position = GeoPoint(item.latitude, item.longitude)
title = listOfNotNull(item.title, item.city).joinToString(" - ")
subDescription = if (item.isOnline) "Online" else "Offline"
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
}
uiState.items.firstOrNull()?.let { first ->
map.controller.animateTo(GeoPoint(first.latitude, first.longitude))
}
map.invalidate()
map.overlays.add(marker)
}
)
if (uiState.isLoading) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.2f)),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(modifier = Modifier.size(40.dp))
items.firstOrNull()?.let { first ->
map.controller.animateTo(GeoPoint(first.latitude, first.longitude))
}
map.invalidate()
}
if (uiState.errorMessage != null) {
ErrorOverlay(
message = uiState.errorMessage ?: "Unknown error",
onRetry = viewModel::refresh
)
}
}
)
}
@Composable

View File

@@ -1,5 +1,7 @@
package org.db3.airmq.features.map
import org.db3.airmq.sdk.map.domain.MapItem
data class MapUiState(
val isLoading: Boolean = false,
val items: List<MapItem> = emptyList(),

View File

@@ -2,14 +2,18 @@ package org.db3.airmq.features.map
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.db3.airmq.sdk.map.MapService
class MapViewModel(
private val mapApi: AirMqMapApi = AirMqMapApi()
@HiltViewModel
class MapViewModel @Inject constructor(
private val mapService: MapService
) : ViewModel() {
private val _uiState = MutableStateFlow(MapUiState(isLoading = true))
@@ -22,7 +26,7 @@ class MapViewModel(
fun refresh() {
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
viewModelScope.launch(Dispatchers.IO) {
val result = runCatching { mapApi.fetchMapItems() }
val result = runCatching { mapService.fetchMapItems() }
_uiState.value = result.fold(
onSuccess = { items ->
MapUiState(

View File

@@ -1,17 +0,0 @@
package org.db3.airmq
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@@ -1,5 +1,10 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.apollo) apply false
alias(libs.plugins.hilt.android) apply false
}

View File

@@ -21,3 +21,5 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.disallowKotlinSourceSets=false
android.newDsl=false

View File

@@ -5,14 +5,19 @@ junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.10.0"
lifecycleViewmodel = "2.10.0"
activityCompose = "1.12.4"
kotlin = "2.0.21"
composeBom = "2024.09.00"
navigationCompose = "2.9.3"
firebaseBom = "34.4.0"
mapsCompose = "6.12.1"
playServicesMaps = "19.2.0"
apollo = "4.3.3"
osmdroid = "6.1.20"
apollo = "5.0.0-alpha.4"
hilt = "2.57.2"
hiltNavigationCompose = "1.2.0"
okhttpLogging = "4.12.0"
ksp = "2.0.21-1.0.27"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -20,6 +25,9 @@ junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeKtx" }
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodel" }
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodel" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
@@ -34,12 +42,20 @@ firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.r
firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" }
firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics" }
firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" }
google-maps-compose = { group = "com.google.maps.android", name = "maps-compose", version.ref = "mapsCompose" }
play-services-maps = { group = "com.google.android.gms", name = "play-services-maps", version.ref = "playServicesMaps" }
osmdroid-android = { group = "org.osmdroid", name = "osmdroid-android", version.ref = "osmdroid" }
apollo-runtime = { group = "com.apollographql.apollo", name = "apollo-runtime", version.ref = "apollo" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttpLogging" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
apollo = { id = "com.apollographql.apollo", version.ref = "apollo" }
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }

49
sdk/build.gradle.kts Normal file
View File

@@ -0,0 +1,49 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.ksp)
alias(libs.plugins.apollo)
}
android {
namespace = "org.db3.airmq.sdk"
compileSdk {
version = release(36) {
minorApiLevel = 1
}
}
defaultConfig {
minSdk = 24
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
buildFeatures {
buildConfig = true
}
}
apollo {
service("service") {
packageName.set("org.db3.airmq.sdk")
schemaFiles.from(file("../app/src/main/graphql/schema.graphqls"))
}
}
dependencies {
implementation(libs.apollo.runtime)
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
}

1
sdk/consumer-rules.pro Normal file
View File

@@ -0,0 +1 @@
# Consumer ProGuard rules for sdk module.

1
sdk/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1 @@
# Add project specific ProGuard rules here.

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,9 @@
query MapMarkers {
getMarkers {
_id
name
latitude
longitude
text
}
}

View File

@@ -0,0 +1,33 @@
package org.db3.airmq.sdk.di
import com.apollographql.apollo.ApolloClient
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import org.db3.airmq.sdk.map.ApolloMapService
import org.db3.airmq.sdk.map.MapService
@Module
@InstallIn(SingletonComponent::class)
object SDKModule {
private const val API_URL = "https://api.airmq.cc"
@Provides
@Singleton
fun provideApolloClient(): ApolloClient {
return ApolloClient.Builder()
.serverUrl(API_URL)
.build()
}
}
@Module
@InstallIn(SingletonComponent::class)
abstract class SDKBindModule {
@Binds
@Singleton
abstract fun bindMapService(impl: ApolloMapService): MapService
}

View File

@@ -0,0 +1,21 @@
package org.db3.airmq.sdk.map
import javax.inject.Inject
import org.db3.airmq.sdk.MapMarkersQuery
import org.db3.airmq.sdk.map.domain.MapItem
class ApolloMapItemMapper @Inject constructor() {
fun toDomain(marker: MapMarkersQuery.GetMarker, index: Int): MapItem? {
val latitude = marker.latitude ?: return null
val longitude = marker.longitude ?: return null
val id = marker._id?.ifBlank { null } ?: "marker-$index"
return MapItem(
id = id,
title = marker.name?.ifBlank { null } ?: marker.text?.ifBlank { null } ?: id,
city = null,
latitude = latitude,
longitude = longitude,
isOnline = false
)
}
}

View File

@@ -0,0 +1,33 @@
package org.db3.airmq.sdk.map
import android.util.Log
import com.apollographql.apollo.ApolloClient
import javax.inject.Inject
import org.db3.airmq.sdk.MapMarkersQuery
import org.db3.airmq.sdk.map.domain.MapItem
class ApolloMapService @Inject constructor(
private val apolloClient: ApolloClient,
private val mapper: ApolloMapItemMapper
) : MapService {
override suspend fun fetchMapItems(): List<MapItem> {
Log.d(TAG, "Executing MapMarkers Apollo query")
val response = apolloClient.query(MapMarkersQuery()).execute()
response.errors?.firstOrNull()?.let { gqlError ->
Log.e(TAG, "MapMarkers Apollo query failed: ${gqlError.message}")
throw IllegalStateException(gqlError.message)
}
val mappedItems = response.data?.getMarkers
.orEmpty()
.filterNotNull()
.mapIndexedNotNull { index, marker -> mapper.toDomain(marker, index) }
Log.d(TAG, "MapMarkers Apollo query returned ${mappedItems.size} items")
return mappedItems
}
private companion object {
private const val TAG = "ApolloMapService"
}
}

View File

@@ -0,0 +1,7 @@
package org.db3.airmq.sdk.map
import org.db3.airmq.sdk.map.domain.MapItem
interface MapService {
suspend fun fetchMapItems(): List<MapItem>
}

View File

@@ -1,4 +1,4 @@
package org.db3.airmq.features.map
package org.db3.airmq.sdk.map.domain
data class MapItem(
val id: String,

View File

@@ -24,4 +24,5 @@ dependencyResolutionManagement {
rootProject.name = "AirMQ"
include(":app")
include(":sdk")