feat: Migrate networking to Ktor and enhance multiplatform support (#4890)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-03-23 11:48:10 -05:00 committed by GitHub
parent acb328dae3
commit b3b38acc0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 435 additions and 897 deletions

View file

@ -79,6 +79,8 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
- **Dependency Injection:** Use **Koin Annotations** with the K2 compiler plugin (`koin-plugin` in version catalog). The `koin-annotations` library version is unified with `koin-core` (both use `version.ref = "koin"`). The `KoinConventionPlugin` uses the typed `KoinGradleExtension` to configure the K2 plugin (e.g., `compileSafety.set(false)`). Keep root graph assembly in `app`.
- **ViewModels:** Follow the MVI/UDF pattern. Use the multiplatform `androidx.lifecycle.ViewModel` in `commonMain`.
- **BLE:** All Bluetooth communication must route through `core:ble` using Kable.
- **Networking:** Pure **Ktor** — no OkHttp anywhere. Engines: `ktor-client-android` for Android, `ktor-client-java` for desktop/JVM. Use Ktor `Logging` plugin for HTTP debug logging (not OkHttp interceptors). `HttpClient` is provided via Koin in `app/di/NetworkModule` and `core:network/di/CoreNetworkAndroidModule`.
- **Image Loading (Coil):** Use `coil-network-ktor3` with `KtorNetworkFetcherFactory` on **all** platforms. `ImageLoader` is configured in host modules only (`app` via Koin `@Single`, `desktop` via `setSingletonImageLoaderFactory`). Feature modules depend only on `libs.coil` (coil-compose) for `AsyncImage` — never add `coil-network-*` or `coil-svg` to feature modules.
- **Dependencies:** Check `gradle/libs.versions.toml` before assuming a library is available.
- **JetBrains fork aliases:** Version catalog aliases for JetBrains-forked AndroidX artifacts use the `jetbrains-*` prefix (e.g., `jetbrains-lifecycle-runtime-compose`, `jetbrains-navigation3-ui`). Plain `androidx-*` aliases are true Google AndroidX artifacts. Never mix them up in `commonMain`.
- **Compose Multiplatform:** Version catalog aliases for Compose Multiplatform artifacts use the `compose-multiplatform-*` prefix (e.g., `compose-multiplatform-material3`, `compose-multiplatform-foundation`). Never use plain `androidx.compose` dependencies in common Main.

View file

@ -79,6 +79,8 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
- **Dependency Injection:** Use **Koin Annotations** with the K2 compiler plugin (`koin-plugin` in version catalog). The `koin-annotations` library version is unified with `koin-core` (both use `version.ref = "koin"`). The `KoinConventionPlugin` uses the typed `KoinGradleExtension` to configure the K2 plugin (e.g., `compileSafety.set(false)`). Keep root graph assembly in `app`.
- **ViewModels:** Follow the MVI/UDF pattern. Use the multiplatform `androidx.lifecycle.ViewModel` in `commonMain`.
- **BLE:** All Bluetooth communication must route through `core:ble` using Kable.
- **Networking:** Pure **Ktor** — no OkHttp anywhere. Engines: `ktor-client-android` for Android, `ktor-client-java` for desktop/JVM. Use Ktor `Logging` plugin for HTTP debug logging (not OkHttp interceptors). `HttpClient` is provided via Koin in `app/di/NetworkModule` and `core:network/di/CoreNetworkAndroidModule`.
- **Image Loading (Coil):** Use `coil-network-ktor3` with `KtorNetworkFetcherFactory` on **all** platforms. `ImageLoader` is configured in host modules only (`app` via Koin `@Single`, `desktop` via `setSingletonImageLoaderFactory`). Feature modules depend only on `libs.coil` (coil-compose) for `AsyncImage` — never add `coil-network-*` or `coil-svg` to feature modules.
- **Dependencies:** Check `gradle/libs.versions.toml` before assuming a library is available.
- **JetBrains fork aliases:** Version catalog aliases for JetBrains-forked AndroidX artifacts use the `jetbrains-*` prefix (e.g., `jetbrains-lifecycle-runtime-compose`, `jetbrains-navigation3-ui`). Plain `androidx-*` aliases are true Google AndroidX artifacts. Never mix them up in `commonMain`.
- **Compose Multiplatform:** Version catalog aliases for Compose Multiplatform artifacts use the `compose-multiplatform-*` prefix (e.g., `compose-multiplatform-material3`, `compose-multiplatform-foundation`). Never use plain `androidx.compose` dependencies in common Main.

View file

@ -251,17 +251,17 @@ dependencies {
implementation(libs.androidx.lifecycle.process)
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
implementation(libs.jetbrains.lifecycle.runtime.compose)
implementation(libs.jetbrains.navigation3.runtime)
implementation(libs.jetbrains.navigation3.ui)
implementation(libs.androidx.paging.compose)
implementation(libs.ktor.client.okhttp)
implementation(libs.ktor.client.android)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.coil.network.okhttp)
implementation(libs.ktor.client.logging)
implementation(libs.coil)
implementation(libs.coil.network.ktor3)
implementation(libs.coil.svg)
implementation(libs.androidx.core.splashscreen)
implementation(libs.kotlinx.serialization.json)
implementation(libs.okhttp3.logging.interceptor)
implementation(libs.usb.serial.android)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.koin.android)
@ -281,7 +281,6 @@ dependencies {
googleImplementation(libs.maps.compose)
googleImplementation(libs.maps.compose.utils)
googleImplementation(libs.maps.compose.widgets)
googleImplementation(libs.dd.sdk.android.okhttp)
googleImplementation(libs.dd.sdk.android.compose)
googleImplementation(libs.dd.sdk.android.logs)
googleImplementation(libs.dd.sdk.android.rum)

View file

@ -1,415 +0,0 @@
androidx.activity:activity-compose:1.12.3
androidx.activity:activity-ktx:1.12.3
androidx.activity:activity:1.12.3
androidx.annotation:annotation-experimental:1.5.1
androidx.annotation:annotation-jvm:1.9.1
androidx.annotation:annotation:1.9.1
androidx.appcompat:appcompat-resources:1.7.1
androidx.appcompat:appcompat:1.7.1
androidx.arch.core:core-common:2.2.0
androidx.arch.core:core-runtime:2.2.0
androidx.autofill:autofill:1.0.0
androidx.cardview:cardview:1.0.0
androidx.collection:collection-jvm:1.5.0
androidx.collection:collection-ktx:1.5.0
androidx.collection:collection:1.5.0
androidx.compose.animation:animation-android:1.11.0-alpha04
androidx.compose.animation:animation-core-android:1.11.0-alpha04
androidx.compose.animation:animation-core:1.11.0-alpha04
androidx.compose.animation:animation:1.11.0-alpha04
androidx.compose.foundation:foundation-android:1.11.0-alpha04
androidx.compose.foundation:foundation-layout-android:1.11.0-alpha04
androidx.compose.foundation:foundation-layout:1.11.0-alpha04
androidx.compose.foundation:foundation:1.11.0-alpha04
androidx.compose.material3.adaptive:adaptive-android:1.3.0-alpha07
androidx.compose.material3.adaptive:adaptive-layout-android:1.3.0-alpha07
androidx.compose.material3.adaptive:adaptive-layout:1.3.0-alpha07
androidx.compose.material3.adaptive:adaptive-navigation-android:1.3.0-alpha07
androidx.compose.material3.adaptive:adaptive-navigation:1.3.0-alpha07
androidx.compose.material3.adaptive:adaptive:1.3.0-alpha07
androidx.compose.material3:material3-adaptive-navigation-suite-android:1.5.0-alpha13
androidx.compose.material3:material3-adaptive-navigation-suite:1.5.0-alpha13
androidx.compose.material3:material3-android:1.5.0-alpha13
androidx.compose.material3:material3:1.5.0-alpha13
androidx.compose.material:material-android:1.11.0-alpha04
androidx.compose.material:material-icons-core-android:1.7.8
androidx.compose.material:material-icons-core:1.7.8
androidx.compose.material:material-icons-extended-android:1.7.8
androidx.compose.material:material-icons-extended:1.7.8
androidx.compose.material:material-ripple-android:1.11.0-alpha04
androidx.compose.material:material-ripple:1.11.0-alpha04
androidx.compose.material:material:1.11.0-alpha04
androidx.compose.runtime:runtime-android:1.11.0-alpha04
androidx.compose.runtime:runtime-annotation-android:1.11.0-alpha04
androidx.compose.runtime:runtime-annotation:1.11.0-alpha04
androidx.compose.runtime:runtime-livedata:1.11.0-alpha04
androidx.compose.runtime:runtime-retain-android:1.11.0-alpha04
androidx.compose.runtime:runtime-retain:1.11.0-alpha04
androidx.compose.runtime:runtime-saveable-android:1.11.0-alpha04
androidx.compose.runtime:runtime-saveable:1.11.0-alpha04
androidx.compose.runtime:runtime-tracing:1.11.0-alpha04
androidx.compose.runtime:runtime:1.11.0-alpha04
androidx.compose.ui:ui-android:1.11.0-alpha04
androidx.compose.ui:ui-geometry-android:1.11.0-alpha04
androidx.compose.ui:ui-geometry:1.11.0-alpha04
androidx.compose.ui:ui-graphics-android:1.11.0-alpha04
androidx.compose.ui:ui-graphics:1.11.0-alpha04
androidx.compose.ui:ui-text-android:1.11.0-alpha04
androidx.compose.ui:ui-text:1.11.0-alpha04
androidx.compose.ui:ui-tooling-android:1.11.0-alpha04
androidx.compose.ui:ui-tooling-data-android:1.11.0-alpha04
androidx.compose.ui:ui-tooling-data:1.11.0-alpha04
androidx.compose.ui:ui-tooling-preview-android:1.11.0-alpha04
androidx.compose.ui:ui-tooling-preview:1.11.0-alpha04
androidx.compose.ui:ui-tooling:1.11.0-alpha04
androidx.compose.ui:ui-unit-android:1.11.0-alpha04
androidx.compose.ui:ui-unit:1.11.0-alpha04
androidx.compose.ui:ui-util-android:1.11.0-alpha04
androidx.compose.ui:ui-util:1.11.0-alpha04
androidx.compose.ui:ui:1.11.0-alpha04
androidx.compose:compose-bom-alpha:2026.01.01
androidx.compose:compose-bom:2026.01.00
androidx.concurrent:concurrent-futures-ktx:1.1.0
androidx.concurrent:concurrent-futures:1.1.0
androidx.constraintlayout:constraintlayout-core:1.0.0
androidx.constraintlayout:constraintlayout:2.1.0
androidx.coordinatorlayout:coordinatorlayout:1.1.0
androidx.core:core-ktx:1.17.0
androidx.core:core-location-altitude-external-protobuf:1.0.0-beta01
androidx.core:core-location-altitude-proto:1.0.0-beta01
androidx.core:core-location-altitude:1.0.0-beta01
androidx.core:core-splashscreen:1.2.0
androidx.core:core-viewtree:1.0.0
androidx.core:core:1.17.0
androidx.cursoradapter:cursoradapter:1.0.0
androidx.customview:customview-poolingcontainer:1.0.0
androidx.customview:customview:1.1.0
androidx.databinding:viewbinding:8.13.2
androidx.datastore:datastore-android:1.2.0
androidx.datastore:datastore-core-android:1.2.0
androidx.datastore:datastore-core-okio-jvm:1.2.0
androidx.datastore:datastore-core-okio:1.2.0
androidx.datastore:datastore-core:1.2.0
androidx.datastore:datastore-preferences-android:1.2.0
androidx.datastore:datastore-preferences-core-android:1.2.0
androidx.datastore:datastore-preferences-core:1.2.0
androidx.datastore:datastore-preferences-external-protobuf:1.2.0
androidx.datastore:datastore-preferences-proto:1.2.0
androidx.datastore:datastore-preferences:1.2.0
androidx.datastore:datastore:1.2.0
androidx.documentfile:documentfile:1.0.0
androidx.drawerlayout:drawerlayout:1.1.1
androidx.dynamicanimation:dynamicanimation:1.1.0
androidx.emoji2:emoji2-emojipicker:1.6.0
androidx.emoji2:emoji2-views-helper:1.6.0
androidx.emoji2:emoji2:1.6.0
androidx.exifinterface:exifinterface:1.4.1
androidx.fragment:fragment-ktx:1.6.2
androidx.fragment:fragment:1.6.2
androidx.graphics:graphics-path:1.0.1
androidx.graphics:graphics-shapes-android:1.0.1
androidx.graphics:graphics-shapes:1.0.1
androidx.hilt:hilt-common:1.3.0
androidx.hilt:hilt-lifecycle-viewmodel-compose:1.3.0
androidx.hilt:hilt-lifecycle-viewmodel:1.3.0
androidx.hilt:hilt-work:1.3.0
androidx.interpolator:interpolator:1.0.0
androidx.legacy:legacy-support-core-utils:1.0.0
androidx.lifecycle:lifecycle-common-java8:2.10.0
androidx.lifecycle:lifecycle-common-jvm:2.10.0
androidx.lifecycle:lifecycle-common:2.10.0
androidx.lifecycle:lifecycle-livedata-core-ktx:2.10.0
androidx.lifecycle:lifecycle-livedata-core:2.10.0
androidx.lifecycle:lifecycle-livedata-ktx:2.10.0
androidx.lifecycle:lifecycle-livedata:2.10.0
androidx.lifecycle:lifecycle-process:2.10.0
androidx.lifecycle:lifecycle-runtime-android:2.10.0
androidx.lifecycle:lifecycle-runtime-compose-android:2.10.0
androidx.lifecycle:lifecycle-runtime-compose:2.10.0
androidx.lifecycle:lifecycle-runtime-ktx-android:2.10.0
androidx.lifecycle:lifecycle-runtime-ktx:2.10.0
androidx.lifecycle:lifecycle-runtime:2.10.0
androidx.lifecycle:lifecycle-service:2.10.0
androidx.lifecycle:lifecycle-viewmodel-android:2.10.0
androidx.lifecycle:lifecycle-viewmodel-compose-android:2.10.0
androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0
androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0
androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.10.0
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.10.0
androidx.lifecycle:lifecycle-viewmodel:2.10.0
androidx.loader:loader:1.0.0
androidx.localbroadcastmanager:localbroadcastmanager:1.1.0
androidx.metrics:metrics-performance:1.0.0-beta03
androidx.navigation3:navigation3-runtime-android:1.0.0
androidx.navigation3:navigation3-runtime:1.0.0
androidx.navigation3:navigation3-ui-android:1.0.0
androidx.navigation3:navigation3-ui:1.0.0
androidx.navigation:navigation-common-android:2.9.7
androidx.navigation:navigation-common:2.9.7
androidx.navigation:navigation-compose-android:2.9.7
androidx.navigation:navigation-compose:2.9.7
androidx.navigation:navigation-fragment:2.9.7
androidx.navigation:navigation-runtime-android:2.9.7
androidx.navigation:navigation-runtime:2.9.7
androidx.navigationevent:navigationevent-android:1.0.2
androidx.navigationevent:navigationevent-compose-android:1.0.2
androidx.navigationevent:navigationevent-compose:1.0.2
androidx.navigationevent:navigationevent:1.0.2
androidx.paging:paging-common-android:3.4.0
androidx.paging:paging-common:3.4.0
androidx.paging:paging-compose-android:3.4.0
androidx.paging:paging-compose:3.4.0
androidx.print:print:1.0.0
androidx.privacysandbox.ads:ads-adservices-java:1.1.0-beta11
androidx.privacysandbox.ads:ads-adservices:1.1.0-beta11
androidx.profileinstaller:profileinstaller:1.4.1
androidx.recyclerview:recyclerview:1.3.2
androidx.resourceinspection:resourceinspection-annotation:1.0.1
androidx.room:room-common-jvm:2.8.4
androidx.room:room-common:2.8.4
androidx.room:room-paging-android:2.8.4
androidx.room:room-paging:2.8.4
androidx.room:room-runtime-android:2.8.4
androidx.room:room-runtime:2.8.4
androidx.savedstate:savedstate-android:1.4.0
androidx.savedstate:savedstate-compose-android:1.4.0
androidx.savedstate:savedstate-compose:1.4.0
androidx.savedstate:savedstate-ktx:1.4.0
androidx.savedstate:savedstate:1.4.0
androidx.slidingpanelayout:slidingpanelayout:1.2.0
androidx.sqlite:sqlite-android:2.6.2
androidx.sqlite:sqlite-framework-android:2.6.2
androidx.sqlite:sqlite-framework:2.6.2
androidx.sqlite:sqlite:2.6.2
androidx.startup:startup-runtime:1.2.0
androidx.tracing:tracing-ktx:1.2.0
androidx.tracing:tracing-perfetto:1.0.1
androidx.tracing:tracing:1.2.0
androidx.transition:transition:1.6.0
androidx.vectordrawable:vectordrawable-animated:1.1.0
androidx.vectordrawable:vectordrawable:1.1.0
androidx.versionedparcelable:versionedparcelable:1.1.1
androidx.viewpager2:viewpager2:1.1.0-beta02
androidx.viewpager:viewpager:1.0.0
androidx.window:window-core-android:1.5.0
androidx.window:window-core:1.5.0
androidx.window:window:1.5.0
androidx.work:work-runtime-ktx:2.11.1
androidx.work:work-runtime:2.11.1
co.touchlab:kermit-android:2.0.8
co.touchlab:kermit-core-android:2.0.8
co.touchlab:kermit-core:2.0.8
co.touchlab:kermit:2.0.8
com.caverock:androidsvg-aar:1.4
com.datadoghq:dd-sdk-android-compose:3.6.0
com.datadoghq:dd-sdk-android-core:3.6.0
com.datadoghq:dd-sdk-android-internal:3.6.0
com.datadoghq:dd-sdk-android-logs:3.6.0
com.datadoghq:dd-sdk-android-okhttp:3.6.0
com.datadoghq:dd-sdk-android-rum:3.6.0
com.datadoghq:dd-sdk-android-session-replay-compose:3.6.0
com.datadoghq:dd-sdk-android-session-replay:3.6.0
com.datadoghq:dd-sdk-android-timber:3.6.0
com.datadoghq:dd-sdk-android-trace-api:3.6.0
com.datadoghq:dd-sdk-android-trace-internal:3.6.0
com.datadoghq:dd-sdk-android-trace-otel:3.6.0
com.datadoghq:dd-sdk-android-trace:3.6.0
com.github.mik3y:usb-serial-for-android:3.10.0
com.google.accompanist:accompanist-drawablepainter:0.37.3
com.google.accompanist:accompanist-permissions:0.37.3
com.google.android.datatransport:transport-api:3.2.0
com.google.android.datatransport:transport-backend-cct:3.3.0
com.google.android.datatransport:transport-runtime:3.3.0
com.google.android.gms:play-services-ads-identifier:18.0.0
com.google.android.gms:play-services-base:18.5.0
com.google.android.gms:play-services-basement:18.9.0
com.google.android.gms:play-services-location:21.3.0
com.google.android.gms:play-services-maps:20.0.0
com.google.android.gms:play-services-measurement-api:23.0.0
com.google.android.gms:play-services-measurement-base:23.0.0
com.google.android.gms:play-services-measurement-impl:23.0.0
com.google.android.gms:play-services-measurement-sdk-api:23.0.0
com.google.android.gms:play-services-measurement-sdk:23.0.0
com.google.android.gms:play-services-measurement:23.0.0
com.google.android.gms:play-services-stats:17.0.2
com.google.android.gms:play-services-tasks:18.4.0
com.google.android.material:material:1.13.0
com.google.auto.value:auto-value-annotations:1.6.3
com.google.code.findbugs:jsr305:3.0.2
com.google.code.gson:gson:2.13.2
com.google.dagger:dagger-lint-aar:2.59
com.google.dagger:dagger:2.59
com.google.dagger:hilt-android:2.59
com.google.dagger:hilt-core:2.59
com.google.errorprone:error_prone_annotations:2.41.0
com.google.firebase:firebase-analytics:23.0.0
com.google.firebase:firebase-annotations:17.0.0
com.google.firebase:firebase-bom:34.8.0
com.google.firebase:firebase-common:22.0.1
com.google.firebase:firebase-components:19.0.0
com.google.firebase:firebase-config-interop:16.0.1
com.google.firebase:firebase-crashlytics:20.0.4
com.google.firebase:firebase-datatransport:19.0.0
com.google.firebase:firebase-encoders-json:18.0.1
com.google.firebase:firebase-encoders-proto:16.0.0
com.google.firebase:firebase-encoders:17.0.0
com.google.firebase:firebase-installations-interop:17.2.0
com.google.firebase:firebase-installations:19.0.1
com.google.firebase:firebase-measurement-connector:20.0.1
com.google.firebase:firebase-sessions:3.0.4
com.google.guava:failureaccess:1.0.3
com.google.guava:guava:33.5.0-android
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
com.google.j2objc:j2objc-annotations:3.1
com.google.maps.android:android-maps-utils:4.0.0
com.google.maps.android:maps-compose-utils:8.0.0
com.google.maps.android:maps-compose-widgets:8.0.0
com.google.maps.android:maps-compose:8.0.0
com.google.maps.android:maps-ktx:6.0.0
com.google.maps.android:maps-utils-ktx:6.0.0
com.google.re2j:re2j:1.7
com.google.zxing:core:3.5.4
com.jakewharton.timber:timber:5.0.1
com.journeyapps:zxing-android-embedded:4.3.0
com.lyft.kronos:kronos-android:0.0.1-alpha11
com.lyft.kronos:kronos-java:0.0.1-alpha11
com.mikepenz:aboutlibraries-compose-core-android:13.2.1
com.mikepenz:aboutlibraries-compose-core:13.2.1
com.mikepenz:aboutlibraries-compose-m3-android:13.2.1
com.mikepenz:aboutlibraries-compose-m3:13.2.1
com.mikepenz:aboutlibraries-core-android:13.2.1
com.mikepenz:aboutlibraries-core:13.2.1
com.mikepenz:multiplatform-markdown-renderer-android:0.39.2
com.mikepenz:multiplatform-markdown-renderer-m3-android:0.39.2
com.mikepenz:multiplatform-markdown-renderer-m3:0.39.2
com.mikepenz:multiplatform-markdown-renderer:0.39.2
com.patrykandpatrick.vico:compose-android:3.0.0-beta.3
com.patrykandpatrick.vico:compose-m2-android:3.0.0-beta.3
com.patrykandpatrick.vico:compose-m2:3.0.0-beta.3
com.patrykandpatrick.vico:compose-m3-android:3.0.0-beta.3
com.patrykandpatrick.vico:compose-m3:3.0.0-beta.3
com.patrykandpatrick.vico:compose:3.0.0-beta.3
com.squareup.okhttp3:logging-interceptor:5.3.2
com.squareup.okhttp3:okhttp-android:5.3.2
com.squareup.okhttp3:okhttp:5.3.2
com.squareup.okio:okio-jvm:3.16.4
com.squareup.okio:okio:3.16.4
com.squareup.wire:wire-runtime-jvm:5.2.1
com.squareup.wire:wire-runtime:5.2.1
io.coil-kt.coil3:coil-android:3.3.0
io.coil-kt.coil3:coil-compose-android:3.3.0
io.coil-kt.coil3:coil-compose-core-android:3.3.0
io.coil-kt.coil3:coil-compose-core:3.3.0
io.coil-kt.coil3:coil-compose:3.3.0
io.coil-kt.coil3:coil-core-android:3.3.0
io.coil-kt.coil3:coil-core:3.3.0
io.coil-kt.coil3:coil-network-core-android:3.3.0
io.coil-kt.coil3:coil-network-core:3.3.0
io.coil-kt.coil3:coil-network-okhttp-jvm:3.3.0
io.coil-kt.coil3:coil-network-okhttp:3.3.0
io.coil-kt.coil3:coil-svg-android:3.3.0
io.coil-kt.coil3:coil-svg:3.3.0
io.coil-kt.coil3:coil:3.3.0
io.ktor:ktor-client-content-negotiation-jvm:3.4.0
io.ktor:ktor-client-content-negotiation:3.4.0
io.ktor:ktor-client-core-jvm:3.4.0
io.ktor:ktor-client-core:3.4.0
io.ktor:ktor-client-okhttp-jvm:3.4.0
io.ktor:ktor-client-okhttp:3.4.0
io.ktor:ktor-events-jvm:3.4.0
io.ktor:ktor-events:3.4.0
io.ktor:ktor-http-cio-jvm:3.4.0
io.ktor:ktor-http-cio:3.4.0
io.ktor:ktor-http-jvm:3.4.0
io.ktor:ktor-http:3.4.0
io.ktor:ktor-io-jvm:3.4.0
io.ktor:ktor-io:3.4.0
io.ktor:ktor-network-jvm:3.4.0
io.ktor:ktor-network:3.4.0
io.ktor:ktor-serialization-jvm:3.4.0
io.ktor:ktor-serialization-kotlinx-json-jvm:3.4.0
io.ktor:ktor-serialization-kotlinx-json:3.4.0
io.ktor:ktor-serialization-kotlinx-jvm:3.4.0
io.ktor:ktor-serialization-kotlinx:3.4.0
io.ktor:ktor-serialization:3.4.0
io.ktor:ktor-sse-jvm:3.4.0
io.ktor:ktor-sse:3.4.0
io.ktor:ktor-utils-jvm:3.4.0
io.ktor:ktor-utils:3.4.0
io.ktor:ktor-websocket-serialization-jvm:3.4.0
io.ktor:ktor-websocket-serialization:3.4.0
io.ktor:ktor-websockets-jvm:3.4.0
io.ktor:ktor-websockets:3.4.0
io.opentelemetry:opentelemetry-api:1.40.0
io.opentelemetry:opentelemetry-context:1.40.0
jakarta.inject:jakarta.inject-api:2.0.1
javax.inject:javax.inject:1
no.nordicsemi.android:dfu:2.10.1
no.nordicsemi.kotlin.ble:client-android:2.0.0-alpha12
no.nordicsemi.kotlin.ble:client-core-android:2.0.0-alpha12
no.nordicsemi.kotlin.ble:client-core:2.0.0-alpha12
no.nordicsemi.kotlin.ble:core:2.0.0-alpha12
org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5
org.jctools:jctools-core:3.3.0
org.jetbrains.androidx.lifecycle:lifecycle-common:2.9.6
org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.9.6
org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.9.6
org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.6
org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.9.6
org.jetbrains.androidx.savedstate:savedstate-compose:1.3.6
org.jetbrains.androidx.savedstate:savedstate:1.3.6
org.jetbrains.compose.animation:animation-core:1.10.0
org.jetbrains.compose.animation:animation:1.10.0
org.jetbrains.compose.annotation-internal:annotation:1.10.0
org.jetbrains.compose.collection-internal:collection:1.10.0
org.jetbrains.compose.components:components-resources-android:1.10.0
org.jetbrains.compose.components:components-resources:1.10.0
org.jetbrains.compose.foundation:foundation-layout:1.10.0
org.jetbrains.compose.foundation:foundation:1.10.0
org.jetbrains.compose.material3:material3:1.9.0
org.jetbrains.compose.material:material-ripple:1.10.0
org.jetbrains.compose.material:material:1.10.0
org.jetbrains.compose.runtime:runtime-saveable:1.10.0
org.jetbrains.compose.runtime:runtime:1.10.0
org.jetbrains.compose.ui:ui-backhandler-android:1.9.1
org.jetbrains.compose.ui:ui-backhandler:1.9.1
org.jetbrains.compose.ui:ui-geometry:1.10.0
org.jetbrains.compose.ui:ui-graphics:1.10.0
org.jetbrains.compose.ui:ui-text:1.10.0
org.jetbrains.compose.ui:ui-tooling-preview:1.10.0-rc02
org.jetbrains.compose.ui:ui-unit:1.10.0
org.jetbrains.compose.ui:ui-util:1.10.0
org.jetbrains.compose.ui:ui:1.10.0
org.jetbrains.kotlin:kotlin-bom:1.8.22
org.jetbrains.kotlin:kotlin-parcelize-runtime:2.3.0
org.jetbrains.kotlin:kotlin-stdlib-common:2.3.0
org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.3.0
org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.21
org.jetbrains.kotlin:kotlin-stdlib:2.3.0
org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.4.0
org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.10.2
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.7.1-0.6.x-compat
org.jetbrains.kotlinx:kotlinx-datetime:0.7.1-0.6.x-compat
org.jetbrains.kotlinx:kotlinx-io-bytestring-jvm:0.8.2
org.jetbrains.kotlinx:kotlinx-io-bytestring:0.8.2
org.jetbrains.kotlinx:kotlinx-io-core-jvm:0.8.2
org.jetbrains.kotlinx:kotlinx-io-core:0.8.2
org.jetbrains.kotlinx:kotlinx-serialization-bom:1.10.0
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.10.0
org.jetbrains.kotlinx:kotlinx-serialization-core:1.10.0
org.jetbrains.kotlinx:kotlinx-serialization-json-io-jvm:1.10.0
org.jetbrains.kotlinx:kotlinx-serialization-json-io:1.10.0
org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.10.0
org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0
org.jetbrains:annotations:23.0.0
org.jetbrains:markdown-jvm:0.7.3
org.jetbrains:markdown:0.7.3
org.jspecify:jspecify:1.0.0
org.slf4j:slf4j-api:2.0.17

View file

@ -27,8 +27,7 @@
-keep class com.google.protobuf.** { *; }
-keep class org.meshtastic.proto.** { *; }
# OkHttp
-dontwarn okhttp3.internal.platform.**
# Networking
-dontwarn org.conscrypt.**
-dontwarn org.bouncycastle.**
-dontwarn org.openjsse.**

View file

@ -16,11 +16,8 @@
*/
package org.meshtastic.app.di
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.koin.core.annotation.Module
import org.koin.core.annotation.Single
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.model.NetworkDeviceHardware
import org.meshtastic.core.model.NetworkFirmwareReleases
import org.meshtastic.core.network.service.ApiService
@ -28,18 +25,6 @@ import org.meshtastic.core.network.service.ApiService
@Module
class FDroidNetworkModule {
@Single
fun provideOkHttpClient(buildConfigProvider: BuildConfigProvider): OkHttpClient = OkHttpClient.Builder()
.addInterceptor(
interceptor =
HttpLoggingInterceptor().apply {
if (buildConfigProvider.isDebug) {
setLevel(HttpLoggingInterceptor.Level.BODY)
}
},
)
.build()
@Single
fun provideApiService(): ApiService = object : ApiService {
override suspend fun getDeviceHardware(): List<NetworkDeviceHardware> =

View file

@ -16,43 +16,13 @@
*/
package org.meshtastic.app.di
import android.content.Context
import com.datadog.android.okhttp.DatadogEventListener
import com.datadog.android.okhttp.DatadogInterceptor
import okhttp3.Cache
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.koin.core.annotation.Module
import org.koin.core.annotation.Single
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.network.service.ApiService
import org.meshtastic.core.network.service.ApiServiceImpl
import java.io.File
@Module
class GoogleNetworkModule {
@Single fun bindApiService(apiServiceImpl: ApiServiceImpl): ApiService = apiServiceImpl
@Single
fun provideOkHttpClient(context: Context, buildConfigProvider: BuildConfigProvider): OkHttpClient =
OkHttpClient.Builder()
.cache(
cache =
Cache(
directory = File(context.applicationContext.cacheDir, "http_cache"),
maxSize = 50L * 1024L * 1024L, // 50 MiB
),
)
.addInterceptor(
interceptor =
HttpLoggingInterceptor().apply {
if (buildConfigProvider.isDebug) {
setLevel(HttpLoggingInterceptor.Level.BODY)
}
},
)
.addInterceptor(interceptor = DatadogInterceptor.Builder(tracedHosts = listOf("meshtastic.org")).build())
.eventListenerFactory(eventListenerFactory = DatadogEventListener.Factory())
.build()
}

View file

@ -42,7 +42,10 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import co.touchlab.kermit.Logger
import coil3.ImageLoader
import coil3.compose.setSingletonImageLoaderFactory
import kotlinx.coroutines.launch
import org.koin.android.ext.android.get
import org.koin.android.ext.android.inject
import org.koin.androidx.compose.koinViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
@ -106,6 +109,10 @@ class MainActivity : ComponentActivity() {
}
setContent {
// Bridge Koin-provided ImageLoader (with flavor-specific HttpClient, SVG, debug logger)
// to Coil's singleton so all AsyncImage composables use the custom configuration.
setSingletonImageLoaderFactory { get<ImageLoader>() }
val theme by model.theme.collectAsStateWithLifecycle()
val dynamic = theme == MODE_DYNAMIC
val dark =

View file

@ -21,19 +21,21 @@ import android.content.Context
import android.net.ConnectivityManager
import android.net.nsd.NsdManager
import coil3.ImageLoader
import coil3.annotation.ExperimentalCoilApi
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.network.ktor3.KtorNetworkFetcherFactory
import coil3.request.crossfade
import coil3.svg.SvgDecoder
import coil3.util.DebugLogger
import coil3.util.Logger
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.engine.android.Android
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logging
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import org.koin.core.annotation.Module
import org.koin.core.annotation.Single
import org.meshtastic.core.common.BuildConfigProvider
@ -52,26 +54,24 @@ class NetworkModule {
fun provideNsdManager(application: Application): NsdManager =
application.getSystemService(Context.NSD_SERVICE) as NsdManager
@OptIn(ExperimentalCoilApi::class)
@Single
fun provideImageLoader(
okHttpClient: OkHttpClient,
httpClient: HttpClient,
application: Context,
buildConfigProvider: BuildConfigProvider,
): ImageLoader {
val sharedOkHttp = okHttpClient.newBuilder().build()
return ImageLoader.Builder(context = application)
.components {
add(OkHttpNetworkFetcherFactory(callFactory = { sharedOkHttp }))
add(SvgDecoder.Factory(scaleToDensity = true))
}
.memoryCache {
MemoryCache.Builder().maxSizePercent(context = application, percent = MEMORY_CACHE_PERCENT).build()
}
.diskCache { DiskCache.Builder().maxSizePercent(percent = DISK_CACHE_PERCENT).build() }
.logger(logger = if (buildConfigProvider.isDebug) DebugLogger(minLevel = Logger.Level.Verbose) else null)
.crossfade(enable = true)
.build()
}
): ImageLoader = ImageLoader.Builder(context = application)
.components {
add(KtorNetworkFetcherFactory(httpClient = httpClient))
add(SvgDecoder.Factory(scaleToDensity = true))
}
.memoryCache {
MemoryCache.Builder().maxSizePercent(context = application, percent = MEMORY_CACHE_PERCENT).build()
}
.diskCache { DiskCache.Builder().maxSizePercent(percent = DISK_CACHE_PERCENT).build() }
.logger(logger = if (buildConfigProvider.isDebug) DebugLogger(minLevel = Logger.Level.Verbose) else null)
.crossfade(enable = true)
.build()
@Single
fun provideJson(): Json = Json {
@ -80,9 +80,11 @@ class NetworkModule {
}
@Single
fun provideHttpClient(okHttpClient: OkHttpClient, json: Json): HttpClient = HttpClient(engineFactory = OkHttp) {
engine { preconfigured = okHttpClient }
install(plugin = ContentNegotiation) { json(json) }
}
fun provideHttpClient(json: Json, buildConfigProvider: BuildConfigProvider): HttpClient =
HttpClient(engineFactory = Android) {
install(plugin = ContentNegotiation) { json(json) }
if (buildConfigProvider.isDebug) {
install(plugin = Logging) { level = LogLevel.BODY }
}
}
}

View file

@ -25,7 +25,6 @@ import androidx.work.WorkerParameters
import io.ktor.client.HttpClient
import io.ktor.client.engine.HttpClientEngine
import kotlinx.coroutines.CoroutineDispatcher
import okhttp3.OkHttpClient
import org.junit.Test
import org.koin.test.verify.definition
import org.koin.test.verify.injectedParameters
@ -53,7 +52,6 @@ class KoinVerificationTest {
NodeIdLookup::class,
HttpClient::class,
HttpClientEngine::class,
OkHttpClient::class,
),
injections =
injectedParameters(

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -32,11 +32,7 @@ java {
targetCompatibility = JavaVersion.VERSION_17
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_17
}
}
kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_17 } }
dependencies {
// This allows the use of the 'libs' type-safe accessor in the Kotlin source of the plugins
@ -78,17 +74,16 @@ spotless {
target("src/*/kotlin/**/*.kt", "src/*/java/**/*.kt")
targetExclude("**/build/**/*.kt")
ktfmt().kotlinlangStyle().configure { it.setMaxWidth(120) }
ktlint(libs.versions.ktlint.get()).setEditorConfigPath(rootProject.file("../config/spotless/.editorconfig").path)
ktlint(libs.versions.ktlint.get())
.setEditorConfigPath(rootProject.file("../config/spotless/.editorconfig").path)
licenseHeaderFile(rootProject.file("../config/spotless/copyright.kt"))
}
kotlinGradle {
target("**/*.gradle.kts")
ktfmt().kotlinlangStyle().configure { it.setMaxWidth(120) }
ktlint(libs.versions.ktlint.get()).setEditorConfigPath(rootProject.file("../config/spotless/.editorconfig").path)
licenseHeaderFile(
rootProject.file("../config/spotless/copyright.kts"),
"(^(?![\\/ ]\\*).*$)"
)
ktlint(libs.versions.ktlint.get())
.setEditorConfigPath(rootProject.file("../config/spotless/.editorconfig").path)
licenseHeaderFile(rootProject.file("../config/spotless/copyright.kts"), "(^(?![\\/ ]\\*).*$)")
}
}
@ -98,12 +93,7 @@ detekt {
buildUponDefaultConfig = true
allRules = false
baseline = file("detekt-baseline.xml")
source.setFrom(
files(
"src/main/java",
"src/main/kotlin",
)
)
source.setFrom(files("src/main/java", "src/main/kotlin"))
}
gradlePlugin {
@ -196,6 +186,5 @@ gradlePlugin {
id = "meshtastic.root"
implementationClass = "RootConventionPlugin"
}
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.datadog.gradle.plugin.DdExtension
@ -29,8 +28,8 @@ import org.meshtastic.buildlogic.libs
import org.meshtastic.buildlogic.plugin
/**
* Convention plugin for analytics (Google Services, Crashlytics, Datadog).
* Segregates these plugins to only affect the "google" flavor and disables their tasks for "fdroid".
* Convention plugin for analytics (Google Services, Crashlytics, Datadog). Segregates these plugins to only affect the
* "google" flavor and disables their tasks for "fdroid".
*/
class AnalyticsConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
@ -50,7 +49,9 @@ class AnalyticsConventionPlugin : Plugin<Project> {
// This avoids iterating all tasks with a generic filter and improves configuration performance.
plugins.withId("com.google.gms.google-services") {
tasks.configureEach {
if (name.contains("GoogleServices", ignoreCase = true) && name.contains("fdroid", ignoreCase = true)) {
if (
name.contains("GoogleServices", ignoreCase = true) && name.contains("fdroid", ignoreCase = true)
) {
enabled = false
}
}
@ -66,10 +67,13 @@ class AnalyticsConventionPlugin : Plugin<Project> {
plugins.withId("com.datadoghq.dd-sdk-android-gradle-plugin") {
tasks.configureEach {
if ((name.contains("datadog", ignoreCase = true) ||
name.contains("uploadMapping", ignoreCase = true) ||
name.contains("buildId", ignoreCase = true)) &&
name.contains("fdroid", ignoreCase = true)) {
if (
(
name.contains("datadog", ignoreCase = true) ||
name.contains("uploadMapping", ignoreCase = true) ||
name.contains("buildId", ignoreCase = true)
) && name.contains("fdroid", ignoreCase = true)
) {
enabled = false
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import com.android.build.api.dsl.ApplicationExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
@ -26,19 +25,17 @@ import org.meshtastic.buildlogic.plugin
/**
* Compose configuration for Android applications.
*
* Note: This has identical implementation to AndroidLibraryComposeConventionPlugin.
* Both use the same configureAndroidCompose() function which works with CommonExtension.
* Kept separate to maintain explicit intent in build.gradle.kts configuration despite duplication.
*
* Note: This has identical implementation to AndroidLibraryComposeConventionPlugin. Both use the same
* configureAndroidCompose() function which works with CommonExtension. Kept separate to maintain explicit intent in
* build.gradle.kts configuration despite duplication.
*/
class AndroidApplicationComposeConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
apply(plugin = libs.plugin("compose-compiler").get().pluginId)
apply(plugin = libs.plugin("compose-multiplatform").get().pluginId)
extensions.configure<ApplicationExtension> {
configureAndroidCompose(this)
}
extensions.configure<ApplicationExtension> { configureAndroidCompose(this) }
}
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import com.android.build.api.dsl.ApplicationExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
@ -24,17 +23,13 @@ import org.meshtastic.buildlogic.configureFlavors
/**
* Flavor configuration for Android applications.
*
* Optimization note: This is nearly identical to AndroidLibraryFlavorsConventionPlugin.
* The underlying configureFlavors() function already handles both ApplicationExtension and LibraryExtension.
* Could be consolidated into a single plugin accepting CommonExtension, but kept separate for now
* to maintain explicit intent in build.gradle.kts declarations.
* Optimization note: This is nearly identical to AndroidLibraryFlavorsConventionPlugin. The underlying
* configureFlavors() function already handles both ApplicationExtension and LibraryExtension. Could be consolidated
* into a single plugin accepting CommonExtension, but kept separate for now to maintain explicit intent in
* build.gradle.kts declarations.
*/
class AndroidApplicationFlavorsConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
extensions.configure<ApplicationExtension> {
configureFlavors(this)
}
}
with(target) { extensions.configure<ApplicationExtension> { configureFlavors(this) } }
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import com.android.build.api.dsl.LibraryExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
@ -26,19 +25,17 @@ import org.meshtastic.buildlogic.plugin
/**
* Compose configuration for Android libraries.
*
* Note: This has identical implementation to AndroidApplicationComposeConventionPlugin.
* Both use the same configureAndroidCompose() function which works with CommonExtension.
* Kept separate to maintain explicit intent in build.gradle.kts configuration despite duplication.
*
* Note: This has identical implementation to AndroidApplicationComposeConventionPlugin. Both use the same
* configureAndroidCompose() function which works with CommonExtension. Kept separate to maintain explicit intent in
* build.gradle.kts configuration despite duplication.
*/
class AndroidLibraryComposeConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
apply(plugin = libs.plugin("compose-compiler").get().pluginId)
apply(plugin = libs.plugin("compose-multiplatform").get().pluginId)
extensions.configure<LibraryExtension> {
configureAndroidCompose(this)
}
extensions.configure<LibraryExtension> { configureAndroidCompose(this) }
}
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import com.android.build.api.dsl.LibraryExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
@ -24,17 +23,13 @@ import org.meshtastic.buildlogic.configureFlavors
/**
* Flavor configuration for Android libraries.
*
* Optimization note: This is nearly identical to AndroidApplicationFlavorsConventionPlugin.
* The underlying configureFlavors() function already handles both ApplicationExtension and LibraryExtension.
* Could be consolidated into a single plugin accepting CommonExtension, but kept separate for now
* to maintain explicit intent in build.gradle.kts declarations.
* Optimization note: This is nearly identical to AndroidApplicationFlavorsConventionPlugin. The underlying
* configureFlavors() function already handles both ApplicationExtension and LibraryExtension. Could be consolidated
* into a single plugin accepting CommonExtension, but kept separate for now to maintain explicit intent in
* build.gradle.kts declarations.
*/
class AndroidLibraryFlavorsConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
extensions.configure<LibraryExtension> {
configureFlavors(this)
}
}
with(target) { extensions.configure<LibraryExtension> { configureFlavors(this) } }
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import androidx.room3.gradle.RoomExtension
import com.google.devtools.ksp.gradle.KspExtension
import org.gradle.api.Plugin
@ -33,9 +32,7 @@ class AndroidRoomConventionPlugin : Plugin<Project> {
apply(plugin = "androidx.room3")
apply(plugin = "com.google.devtools.ksp")
extensions.configure<KspExtension> {
arg("room.generateKotlin", "true")
}
extensions.configure<KspExtension> { arg("room.generateKotlin", "true") }
extensions.configure<RoomExtension> {
// The schemas directory contains a schema file for each version of the Room database.
@ -50,13 +47,9 @@ class AndroidRoomConventionPlugin : Plugin<Project> {
pluginManager.withPlugin("org.jetbrains.kotlin.multiplatform") {
extensions.configure<KotlinMultiplatformExtension> {
sourceSets.getByName("commonMain").dependencies {
implementation(roomRuntime)
}
}
dependencies {
add("kspAndroid", roomCompiler)
sourceSets.getByName("commonMain").dependencies { implementation(roomRuntime) }
}
dependencies { add("kspAndroid", roomCompiler) }
}
pluginManager.withPlugin("org.jetbrains.kotlin.android") {

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
@ -26,10 +25,9 @@ import org.meshtastic.buildlogic.libs
/**
* Convention plugin for KMP feature modules.
*
* Composes [KmpLibraryConventionPlugin], [KmpLibraryComposeConventionPlugin], and
* [KoinConventionPlugin] and wires the common Compose / Lifecycle / Koin dependencies
* that every feature module needs. Feature `build.gradle.kts` files only declare
* their module-specific deps.
* Composes [KmpLibraryConventionPlugin], [KmpLibraryComposeConventionPlugin], and [KoinConventionPlugin] and wires the
* common Compose / Lifecycle / Koin dependencies that every feature module needs. Feature `build.gradle.kts` files only
* declare their module-specific deps.
*
* Modelled after the `AndroidFeatureImplConventionPlugin` pattern from
* [Now in Android](https://github.com/android/nowinandroid).
@ -71,12 +69,8 @@ class KmpFeatureConventionPlugin : Plugin<Project> {
implementation(libs.library("androidx-compose-ui-tooling-preview"))
}
sourceSets.getByName("commonTest").dependencies {
implementation(project(":core:testing"))
}
sourceSets.getByName("commonTest").dependencies { implementation(project(":core:testing")) }
}
}
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,20 +14,16 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.meshtastic.buildlogic.configureJvmAndroidMainHierarchy
/**
* Opt-in convention for KMP modules that intentionally share a `jvmAndroidMain` source set
* between the desktop JVM target and the Android target.
* Opt-in convention for KMP modules that intentionally share a `jvmAndroidMain` source set between the desktop JVM
* target and the Android target.
*/
class KmpJvmAndroidConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
configureJvmAndroidMainHierarchy()
}
with(target) { configureJvmAndroidMainHierarchy() }
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import dev.mokkery.gradle.MokkeryGradleExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
@ -23,6 +22,7 @@ import org.gradle.kotlin.dsl.configure
import org.meshtastic.buildlogic.configureAndroidMarketplaceFallback
import org.meshtastic.buildlogic.configureKmpTestDependencies
import org.meshtastic.buildlogic.configureKotlinMultiplatform
import org.meshtastic.buildlogic.configureTestOptions
import org.meshtastic.buildlogic.libs
import org.meshtastic.buildlogic.plugin
@ -38,12 +38,11 @@ class KmpLibraryConventionPlugin : Plugin<Project> {
apply(plugin = "meshtastic.kover")
apply(plugin = libs.plugin("mokkery").get().pluginId)
extensions.configure<MokkeryGradleExtension> {
stubs.allowConcreteClassInstantiation.set(true)
}
extensions.configure<MokkeryGradleExtension> { stubs.allowConcreteClassInstantiation.set(true) }
configureKotlinMultiplatform()
configureKmpTestDependencies()
configureTestOptions()
configureAndroidMarketplaceFallback()
}
}

View file

@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,22 +14,15 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.buildlogic
import com.android.build.api.dsl.CommonExtension
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
/**
* Configure Compose-specific options
*/
internal fun Project.configureAndroidCompose(
commonExtension: CommonExtension,
) {
commonExtension.apply {
buildFeatures.compose = true
}
/** Configure Compose-specific options */
internal fun Project.configureAndroidCompose(commonExtension: CommonExtension) {
commonExtension.apply { buildFeatures.compose = true }
val hasAndroidTest = project.projectDir.resolve("src/androidTest").exists()
dependencies {
@ -38,10 +31,9 @@ internal fun Project.configureAndroidCompose(
if (hasAndroidTest) {
"androidTestImplementation"(platform(bom))
}
"implementation"(libs.library("androidx-compose-ui-tooling"))
"debugImplementation"(libs.library("androidx-compose-ui-tooling"))
"implementation"(libs.library("androidx-compose-runtime"))
"runtimeOnly"(libs.library("androidx-compose-runtime-tracing"))
"debugImplementation"(libs.library("androidx-compose-ui-tooling"))
"implementation"(libs.library("compose-multiplatform-runtime"))
"implementation"(libs.library("compose-multiplatform-resources"))

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.buildlogic
import org.gradle.api.Project
@ -38,16 +37,8 @@ fun Project.configureDokka() {
}
// Dokka 2.x requires each source file to belong to exactly one source set.
val baseSourceSets = listOf(
"main",
"commonMain",
"androidMain",
"jvmMain",
"jvmAndroidMain",
"fdroid",
"google",
"release"
)
val baseSourceSets =
listOf("main", "commonMain", "androidMain", "jvmMain", "jvmAndroidMain", "fdroid", "google", "release")
val isCoreSourceSet = name in baseSourceSets
suppress.set(!isCoreSourceSet)
@ -59,8 +50,7 @@ fun Project.configureDokka() {
// Standardized repo-root based source links
localDirectory.set(project.projectDir)
val relativePath =
project.projectDir.relativeTo(rootProject.projectDir).path.replace("\\", "/")
val relativePath = project.projectDir.relativeTo(rootProject.projectDir).path.replace("\\", "/")
remoteUrl.set(URI("https://github.com/meshtastic/Meshtastic-Android/blob/main/$relativePath"))
remoteLineSuffix.set("#L")
}
@ -68,20 +58,14 @@ fun Project.configureDokka() {
}
}
/**
* Configure Dokka aggregation in a way that is compatible with Gradle Isolated Projects.
*/
/** Configure Dokka aggregation in a way that is compatible with Gradle Isolated Projects. */
fun Project.configureDokkaAggregation() {
extensions.configure<DokkaExtension> {
moduleName.set("Meshtastic App")
dokkaPublications.configureEach {
suppressInheritedMembers.set(true)
}
dokkaPublications.configureEach { suppressInheritedMembers.set(true) }
}
subprojects.forEach { subproject ->
subproject.pluginManager.withPlugin("org.jetbrains.dokka") {
dependencies.add("dokka", subproject)
}
subproject.pluginManager.withPlugin("org.jetbrains.dokka") { dependencies.add("dokka", subproject) }
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,15 +14,30 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.buildlogic
import com.android.build.api.attributes.ProductFlavorAttr
import org.gradle.api.Project
import org.gradle.api.attributes.Attribute
import org.gradle.api.attributes.AttributeDisambiguationRule
import org.gradle.api.attributes.MultipleCandidatesDetails
import javax.inject.Inject
private const val LEGACY_MARKETPLACE_ATTRIBUTE_NAME = "marketplace"
/**
* Registers [AttributeDisambiguationRule]s so Gradle can pick a default product flavor when a consumer configuration
* (e.g. `androidHostTestRuntimeClasspath` from a KMP module) does not carry the marketplace flavor attribute, but the
* producer (e.g. `core:barcode`) publishes multiple flavor variants.
*
* This replaces the previous `afterEvaluate { configurations.configureEach { } }` approach that stamped attributes on
* every resolvable Android configuration. Disambiguation rules fire during dependency resolution not configuration
* time so they are immune to KGP's lazy configuration creation order and fully compatible with Configuration Cache,
* Isolated Projects, and future Gradle/KGP changes.
*
* The default flavor is configurable via the `meshtastic.defaultMarketplace` Gradle property (defaults to the
* [MeshtasticFlavor] entry marked `default = true`, which is `google`).
*/
internal fun Project.configureAndroidMarketplaceFallback() {
val defaultMarketplace =
providers
@ -30,27 +45,34 @@ internal fun Project.configureAndroidMarketplaceFallback() {
.orElse(MeshtasticFlavor.entries.first { it.default }.name)
.get()
// AGP publishes the typed ProductFlavorAttr on flavored variant configurations.
val marketplaceAttr = ProductFlavorAttr.of(MeshtasticFlavor.fdroid.dimension.name)
dependencies.attributesSchema.attribute(marketplaceAttr) {
disambiguationRules.add(ProductFlavorDisambiguationRule::class.java) { params(defaultMarketplace) }
}
// Some AGP versions also publish a plain String "marketplace" attribute.
val legacyMarketplaceAttr = Attribute.of(LEGACY_MARKETPLACE_ATTRIBUTE_NAME, String::class.java)
afterEvaluate {
configurations.configureEach {
if (!isCanBeResolved || isCanBeConsumed) return@configureEach
if (!name.contains("android", ignoreCase = true)) return@configureEach
if (attributes.getAttribute(marketplaceAttr) != null && attributes.getAttribute(legacyMarketplaceAttr) != null) {
return@configureEach
}
// Prefer explicit flavor from configuration name; otherwise use configurable default.
val inferredMarketplace =
when {
name.contains(MeshtasticFlavor.fdroid.name, ignoreCase = true) -> MeshtasticFlavor.fdroid.name
name.contains(MeshtasticFlavor.google.name, ignoreCase = true) -> MeshtasticFlavor.google.name
else -> defaultMarketplace
}
attributes.attribute(marketplaceAttr, objects.named(ProductFlavorAttr::class.java, inferredMarketplace))
attributes.attribute(legacyMarketplaceAttr, inferredMarketplace)
}
dependencies.attributesSchema.attribute(legacyMarketplaceAttr) {
disambiguationRules.add(StringDisambiguationRule::class.java) { params(defaultMarketplace) }
}
}
/**
* Selects the default marketplace flavor when Gradle encounters ambiguous [ProductFlavorAttr] candidates during
* variant-aware dependency resolution.
*/
internal abstract class ProductFlavorDisambiguationRule @Inject constructor(private val defaultFlavor: String) :
AttributeDisambiguationRule<ProductFlavorAttr> {
override fun execute(details: MultipleCandidatesDetails<ProductFlavorAttr>) {
details.candidateValues.find { it.name == defaultFlavor }?.let { details.closestMatch(it) }
}
}
/** Selects the default marketplace for the legacy plain-String "marketplace" attribute. */
internal abstract class StringDisambiguationRule @Inject constructor(private val defaultFlavor: String) :
AttributeDisambiguationRule<String> {
override fun execute(details: MultipleCandidatesDetails<String>) {
details.candidateValues.find { it == defaultFlavor }?.let { details.closestMatch(it) }
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.buildlogic
import org.gradle.api.DefaultTask
@ -35,9 +34,7 @@ import org.gradle.kotlin.dsl.withType
import org.meshtastic.buildlogic.PluginType.Unknown
import kotlin.text.RegexOption.DOT_MATCHES_ALL
/**
* Declaration order is important, as only the first match will be retained.
*/
/** Declaration order is important, as only the first match will be retained. */
internal enum class PluginType(val id: String, val ref: String, val style: String) {
AndroidApplication(
id = "meshtastic.android.application",
@ -89,56 +86,62 @@ internal enum class PluginType(val id: String, val ref: String, val style: Strin
ref = "kmp-library",
style = "fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000",
),
Unknown(
id = "?",
ref = "unknown",
style = "fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000",
),
Unknown(id = "?", ref = "unknown", style = "fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000"),
}
/**
* Optimized and Isolated Projects compatible graph configuration.
*/
/** Optimized and Isolated Projects compatible graph configuration. */
internal fun Project.configureGraphTasks() {
if (!buildFile.exists()) return
val supportedConfigurations = providers.gradleProperty("graph.supportedConfigurations")
.map { it.split(",").toSet() }
.orElse(setOf("api", "implementation", "baselineProfile", "testedApks"))
val supportedConfigurations =
providers
.gradleProperty("graph.supportedConfigurations")
.map { it.split(",").toSet() }
.orElse(setOf("api", "implementation", "baselineProfile", "testedApks"))
val targetProjectPath = path
val dumpTask = tasks.register<GraphDumpTask>("graphDump") {
projectPath.set(targetProjectPath)
dependenciesData.set(providers.provider {
val deps = mutableMapOf<String, Set<Pair<String, String>>>()
val projectDeps = mutableSetOf<Pair<String, String>>()
configurations.filter { it.name in supportedConfigurations.get() }.forEach { config ->
config.dependencies.withType<ProjectDependency>().forEach { dep ->
projectDeps.add(config.name to dep.path)
}
}
deps[targetProjectPath] = projectDeps
deps
})
val dumpTask =
tasks.register<GraphDumpTask>("graphDump") {
projectPath.set(targetProjectPath)
pluginsData.set(providers.provider {
val projectPlugins = mutableMapOf<String, PluginType>()
val type = when {
pluginManager.hasPlugin("meshtastic.android.application") || pluginManager.hasPlugin("meshtastic.android.application.compose") -> PluginType.AndroidApplication
targetProjectPath.startsWith(":desktop") -> PluginType.ComposeDesktopApplication
pluginManager.hasPlugin("meshtastic.kmp.feature") -> PluginType.KmpFeature
targetProjectPath.startsWith(":feature:") -> PluginType.AndroidFeature
else -> PluginType.entries.firstOrNull { pluginManager.hasPlugin(it.id) } ?: Unknown
}
projectPlugins[targetProjectPath] = type
projectPlugins
})
dependenciesData.set(
providers.provider {
val deps = mutableMapOf<String, Set<Pair<String, String>>>()
val projectDeps = mutableSetOf<Pair<String, String>>()
configurations
.filter { it.name in supportedConfigurations.get() }
.forEach { config ->
config.dependencies.withType<ProjectDependency>().forEach { dep ->
projectDeps.add(config.name to dep.path)
}
}
deps[targetProjectPath] = projectDeps
deps
},
)
output.set(layout.buildDirectory.file("mermaid/graph.txt"))
legend.set(layout.buildDirectory.file("mermaid/legend.txt"))
}
pluginsData.set(
providers.provider {
val projectPlugins = mutableMapOf<String, PluginType>()
val type =
when {
pluginManager.hasPlugin("meshtastic.android.application") ||
pluginManager.hasPlugin("meshtastic.android.application.compose") ->
PluginType.AndroidApplication
targetProjectPath.startsWith(":desktop") -> PluginType.ComposeDesktopApplication
pluginManager.hasPlugin("meshtastic.kmp.feature") -> PluginType.KmpFeature
targetProjectPath.startsWith(":feature:") -> PluginType.AndroidFeature
else -> PluginType.entries.firstOrNull { pluginManager.hasPlugin(it.id) } ?: Unknown
}
projectPlugins[targetProjectPath] = type
projectPlugins
},
)
output.set(layout.buildDirectory.file("mermaid/graph.txt"))
legend.set(layout.buildDirectory.file("mermaid/legend.txt"))
}
tasks.register<GraphUpdateTask>("graphUpdate") {
projectPath.set(targetProjectPath)
@ -151,20 +154,15 @@ internal fun Project.configureGraphTasks() {
@CacheableTask
private abstract class GraphDumpTask : DefaultTask() {
@get:Input
abstract val projectPath: Property<String>
@get:Input abstract val projectPath: Property<String>
@get:Input
abstract val dependenciesData: MapProperty<String, Set<Pair<String, String>>>
@get:Input abstract val dependenciesData: MapProperty<String, Set<Pair<String, String>>>
@get:Input
abstract val pluginsData: MapProperty<String, PluginType>
@get:Input abstract val pluginsData: MapProperty<String, PluginType>
@get:OutputFile
abstract val output: RegularFileProperty
@get:OutputFile abstract val output: RegularFileProperty
@get:OutputFile
abstract val legend: RegularFileProperty
@get:OutputFile abstract val legend: RegularFileProperty
@TaskAction
operator fun invoke() {
@ -177,17 +175,20 @@ private abstract class GraphDumpTask : DefaultTask() {
val currentProject = projectPath.get()
val projectPlugins = pluginsData.get()
val projectDeps = dependenciesData.get()[currentProject] ?: emptySet()
appendLine(" $currentProject[${currentProject.substringAfterLast(":")}]:::${projectPlugins[currentProject]?.ref}")
appendLine(
" $currentProject[${currentProject.substringAfterLast(":")}]:::${projectPlugins[currentProject]?.ref}",
)
projectDeps.forEach { (config, depPath) ->
val link = when (config) {
"api" -> "-->"
else -> "-.->"
}
val link =
when (config) {
"api" -> "-->"
else -> "-.->"
}
appendLine(" $currentProject $link $depPath")
}
appendLine()
PluginType.entries.forEach { appendLine("classDef ${it.ref} ${it.style};") }
}
@ -206,16 +207,17 @@ private abstract class GraphDumpTask : DefaultTask() {
@CacheableTask
private abstract class GraphUpdateTask : DefaultTask() {
@get:Input
abstract val projectPath: Property<String>
@get:Input abstract val projectPath: Property<String>
@get:InputFile
@get:PathSensitive(NONE)
abstract val input: RegularFileProperty
@get:InputFile
@get:PathSensitive(NONE)
abstract val legend: RegularFileProperty
@get:OutputFile
abstract val output: RegularFileProperty
@get:OutputFile abstract val output: RegularFileProperty
@TaskAction
fun update() {
@ -223,10 +225,11 @@ private abstract class GraphUpdateTask : DefaultTask() {
if (!readme.exists()) return
val mermaid = input.get().asFile.readText()
val currentContent = readme.readText()
val newContent = currentContent.replace(
Regex("<!--region graph-->.*?<!--endregion-->", DOT_MATCHES_ALL),
"<!--region graph-->\n```mermaid\n$mermaid\n```\n<!--endregion-->"
)
val newContent =
currentContent.replace(
Regex("<!--region graph-->.*?<!--endregion-->", DOT_MATCHES_ALL),
"<!--region graph-->\n```mermaid\n$mermaid\n```\n<!--endregion-->",
)
if (currentContent != newContent) {
readme.writeText(newContent)
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.buildlogic
import com.android.build.api.dsl.ApplicationExtension
@ -35,12 +34,8 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinHierarchyTemplate
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
/**
* Configure base Kotlin with Android options
*/
internal fun Project.configureKotlinAndroid(
commonExtension: CommonExtension,
) {
/** Configure base Kotlin with Android options */
internal fun Project.configureKotlinAndroid(commonExtension: CommonExtension) {
val compileSdkVersion = configProperties.getProperty("COMPILE_SDK").toInt()
val minSdkVersion = configProperties.getProperty("MIN_SDK").toInt()
val targetSdkVersion = configProperties.getProperty("TARGET_SDK").toInt()
@ -49,7 +44,7 @@ internal fun Project.configureKotlinAndroid(
compileSdk = compileSdkVersion
defaultConfig.minSdk = minSdkVersion
if (this is ApplicationExtension) {
defaultConfig.targetSdk = targetSdkVersion
}
@ -62,9 +57,7 @@ internal fun Project.configureKotlinAndroid(
configureKotlin<KotlinAndroidProjectExtension>()
}
/**
* Configure Kotlin Multiplatform options
*/
/** Configure Kotlin Multiplatform options */
internal fun Project.configureKotlinMultiplatform() {
extensions.configure<KotlinMultiplatformExtension> {
// Standard KMP targets for Meshtastic
@ -80,7 +73,7 @@ internal fun Project.configureKotlinMultiplatform() {
extensions.findByType<KotlinMultiplatformAndroidLibraryTarget>()?.apply {
compileSdk = configProperties.getProperty("COMPILE_SDK").toInt()
minSdk = configProperties.getProperty("MIN_SDK").toInt()
// Set the namespace automatically if not already set
if (namespace == null) {
val pkg = this@configureKotlinMultiplatform.path.removePrefix(":").replace(":", ".")
@ -96,8 +89,10 @@ internal fun Project.configureKotlinMultiplatform() {
tasks.configureEach {
val taskName = name.lowercase()
if (taskName.contains("iosarm64") || taskName.contains("iossimulatorarm64")) {
if (taskName.startsWith("link") && taskName.contains("test") ||
taskName == "iosarm64test" || taskName == "iossimulatorarm64test" ||
if (
taskName.startsWith("link") && taskName.contains("test") ||
taskName == "iosarm64test" ||
taskName == "iossimulatorarm64test" ||
taskName.endsWith("testbinaries")
) {
enabled = false
@ -109,22 +104,18 @@ internal fun Project.configureKotlinMultiplatform() {
configureKotlin<KotlinMultiplatformExtension>()
}
/**
* Configure Mokkery for the project
*/
/** Configure Mokkery for the project */
internal fun Project.configureMokkery() {
pluginManager.withPlugin(libs.plugin("mokkery").get().pluginId) {
extensions.configure<MokkeryGradleExtension> {
stubs.allowConcreteClassInstantiation.set(true)
}
extensions.configure<MokkeryGradleExtension> { stubs.allowConcreteClassInstantiation.set(true) }
}
}
/**
* Configure a shared `jvmAndroidMain` source set using Kotlin's hierarchy template DSL.
*
* This is for modules that intentionally share JVM-only implementations between the desktop
* `jvm()` target and the Android target without hand-written `dependsOn` edges.
* This is for modules that intentionally share JVM-only implementations between the desktop `jvm()` target and the
* Android target without hand-written `dependsOn` edges.
*/
@OptIn(ExperimentalKotlinGradlePluginApi::class)
internal fun Project.configureJvmAndroidMainHierarchy() {
@ -133,8 +124,7 @@ internal fun Project.configureJvmAndroidMainHierarchy() {
common {
group("jvmAndroid") {
withCompilations { compilation ->
compilation.target.targetName == "android" ||
compilation.target.targetName == "jvm"
compilation.target.targetName == "android" || compilation.target.targetName == "jvm"
}
}
}
@ -142,9 +132,7 @@ internal fun Project.configureJvmAndroidMainHierarchy() {
}
}
/**
* Configure common test dependencies for KMP modules
*/
/** Configure common test dependencies for KMP modules */
internal fun Project.configureKmpTestDependencies() {
extensions.configure<KotlinMultiplatformExtension> {
sourceSets.apply {
@ -155,7 +143,7 @@ internal fun Project.configureKmpTestDependencies() {
implementation(libs.library("kotest-property"))
implementation(libs.library("turbine"))
}
// Configure androidHostTest if it exists
val androidHostTest = findByName("androidHostTest")
androidHostTest?.dependencies {
@ -167,23 +155,17 @@ internal fun Project.configureKmpTestDependencies() {
// Configure jvmTest if it exists
val jvmTest = findByName("jvmTest")
jvmTest?.dependencies {
implementation(libs.library("kotest-runner-junit6"))
}
jvmTest?.dependencies { implementation(libs.library("kotest-runner-junit6")) }
}
}
}
/**
* Configure base Kotlin options for JVM (non-Android)
*/
/** Configure base Kotlin options for JVM (non-Android) */
internal fun Project.configureKotlinJvm() {
configureKotlin<KotlinJvmProjectExtension>()
}
/**
* Configure base Kotlin options
*/
/** Configure base Kotlin options */
private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() {
extensions.configure<T> {
// Using Java 17 for better compatibility with consumers (e.g. plugins, older environments)
@ -203,7 +185,7 @@ private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() {
"-Xexpect-actual-classes",
"-Xcontext-parameters",
"-Xannotation-default-target=param-property",
"-Xskip-prerelease-check"
"-Xskip-prerelease-check",
)
}
}
@ -212,10 +194,12 @@ private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() {
}
}
val warningsAsErrors = providers.gradleProperty("warningsAsErrors").map { it.toBoolean() }.getOrElse(false)
tasks.withType<KotlinCompile>().configureEach {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
allWarningsAsErrors.set(false)
allWarningsAsErrors.set(warningsAsErrors)
freeCompilerArgs.addAll(
// Enable experimental coroutines APIs, including Flow
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
@ -225,7 +209,7 @@ private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() {
"-Xexpect-actual-classes",
"-Xcontext-parameters",
"-Xannotation-default-target=param-property",
"-Xskip-prerelease-check"
"-Xskip-prerelease-check",
)
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.buildlogic
import kotlinx.kover.gradle.plugin.dsl.KoverProjectExtension
@ -25,12 +24,8 @@ fun Project.configureKover() {
extensions.configure<KoverProjectExtension> {
reports {
total {
xml {
onCheck.set(true)
}
html {
onCheck.set(true)
}
xml { onCheck.set(true) }
html { onCheck.set(true) }
}
filters {
excludes {
@ -42,16 +37,14 @@ fun Project.configureKover() {
classes("*.R")
classes("*.R$*")
// Exclude iOS compile-only stubs (no test execution on these targets)
classes("*NoopStubs*")
// Exclude UI components
annotatedBy("*Preview")
// Exclude declarations
annotatedBy(
"*.Module",
"*.Provides",
"*.Binds",
"*.Composable",
)
annotatedBy("*.Module", "*.Provides", "*.Binds", "*.Composable")
// Suppress generated code
packages("koin_aggregated_deps")
@ -63,13 +56,11 @@ fun Project.configureKover() {
}
/**
* Configure Kover aggregation in a way that is compatible with Gradle Isolated Projects.
* Instead of blindly adding all subprojects, we only add those that have the Kover plugin applied.
* Configure Kover aggregation in a way that is compatible with Gradle Isolated Projects. Instead of blindly adding all
* subprojects, we only add those that have the Kover plugin applied.
*/
fun Project.configureKoverAggregation() {
subprojects.forEach { subproject ->
subproject.pluginManager.withPlugin("org.jetbrains.kotlinx.kover") {
dependencies.add("kover", subproject)
}
subproject.pluginManager.withPlugin("org.jetbrains.kotlinx.kover") { dependencies.add("kover", subproject) }
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.buildlogic
import org.gradle.api.Project
@ -37,17 +36,13 @@ import java.util.Properties
val Project.libs
get(): VersionCatalog = extensions.getByType<VersionCatalogsExtension>().named("libs")
fun VersionCatalog.library(alias: String): Provider<MinimalExternalModuleDependency> =
findLibrary(alias).get()
fun VersionCatalog.library(alias: String): Provider<MinimalExternalModuleDependency> = findLibrary(alias).get()
fun VersionCatalog.bundle(alias: String): Provider<ExternalModuleDependencyBundle> =
findBundle(alias).get()
fun VersionCatalog.bundle(alias: String): Provider<ExternalModuleDependencyBundle> = findBundle(alias).get()
fun VersionCatalog.plugin(alias: String): Provider<PluginDependency> =
findPlugin(alias).get()
fun VersionCatalog.plugin(alias: String): Provider<PluginDependency> = findPlugin(alias).get()
fun VersionCatalog.version(alias: String): String =
findVersion(alias).get().requiredVersion
fun VersionCatalog.version(alias: String): String = findVersion(alias).get().requiredVersion
val Project.configProperties: Properties
get() {
@ -59,19 +54,23 @@ val Project.configProperties: Properties
return properties
}
/**
* Configure common test options like parallel execution and logging.
*/
/** Configure common test options like parallel execution and logging. */
internal fun Project.configureTestOptions() {
tasks.withType<Test>().configureEach {
// Parallelize unit tests
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
maxHeapSize = "2g"
// Allow modules with no discovered tests to pass without failing the build
filter { isFailOnNoMatchingTests = false }
// Show test results in the console
testLogging {
events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
}
testLogging { events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) }
}
// Gradle 9+ fails when test sources exist but no test classes are discovered (e.g. all
// tests are commented out). Disable to avoid breaking builds for modules with WIP tests.
tasks.withType<AbstractTestTask>().configureEach {
failOnNoDiscoveredTests.set(false)
}
// Configure test retry if the plugin is applied

View file

@ -15,8 +15,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.kotlin.multiplatform.library) apply false
@ -26,7 +24,6 @@ plugins {
alias(libs.plugins.devtools.ksp) apply false
alias(libs.plugins.koin.compiler) apply false
alias(libs.plugins.firebase.crashlytics) apply false
alias(libs.plugins.firebase.perf) apply false
alias(libs.plugins.google.services) apply false
alias(libs.plugins.room) apply false
alias(libs.plugins.kotlin.android) apply false
@ -41,20 +38,9 @@ plugins {
alias(libs.plugins.spotless) apply false
alias(libs.plugins.dokka)
alias(libs.plugins.test.retry) apply false
alias(libs.plugins.dependency.guard) apply false
alias(libs.plugins.meshtastic.root)
}
dependencies {
dokkaPlugin(libs.dokka.android.documentation.plugin)
}
subprojects {
tasks.withType<Test> {
failOnNoDiscoveredTests = false
}
}

View file

@ -18,8 +18,5 @@ okio.ByteString
// Kotlin Immutable Collections
kotlinx.collections.immutable.*
// Java Time
java.time.*
// External Libraries
com.google.android.gms.maps.model.**

View file

@ -46,8 +46,6 @@ kotlin {
implementation(libs.androidx.lifecycle.runtime.ktx)
}
jvmMain.dependencies {}
commonTest.dependencies {
implementation(kotlin("test"))
implementation(libs.kotlinx.coroutines.test)

View file

@ -33,8 +33,6 @@ kotlin {
sourceSets {
commonMain.dependencies {
api(libs.aboutlibraries.core)
implementation(libs.aboutlibraries.compose.m3)
implementation(libs.kotlinx.atomicfu)
implementation(libs.kotlinx.coroutines.core)
api(libs.kotlinx.datetime)

View file

@ -18,6 +18,7 @@
plugins {
alias(libs.plugins.meshtastic.kmp.library)
alias(libs.plugins.meshtastic.kotlinx.serialization)
id("meshtastic.kmp.jvm.android")
id("meshtastic.koin")
}
@ -51,21 +52,18 @@ kotlin {
implementation(libs.kotlinx.collections.immutable)
}
// Room / SQLite runtime shared between Android and Desktop JVM targets
val jvmAndroidMain by getting {
dependencies {
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.paging)
implementation(libs.androidx.sqlite.bundled)
}
}
androidMain.dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.core.location.altitude)
// Needed because core:data references MeshtasticDatabase (supertype RoomDatabase)
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.paging)
implementation(libs.androidx.sqlite.bundled)
}
jvmMain.dependencies {
// Room / SQLite runtime for JVM target
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.paging)
implementation(libs.androidx.sqlite.bundled)
}
commonTest.dependencies {

View file

@ -28,7 +28,7 @@ kotlin {
commonMain.dependencies {
implementation(projects.core.resources)
implementation(libs.kotlinx.serialization.core)
implementation(libs.jetbrains.navigation3.runtime)
implementation(libs.jetbrains.navigation3.ui)
}
commonTest.dependencies { implementation(kotlin("test")) }

View file

@ -61,10 +61,6 @@ kotlin {
implementation(projects.core.ble)
implementation(projects.core.prefs)
implementation(libs.usb.serial.android)
implementation(libs.coil.network.okhttp)
implementation(libs.coil.svg)
implementation(libs.ktor.client.okhttp)
implementation(libs.okhttp3.logging.interceptor)
}
commonTest.dependencies {

View file

@ -16,18 +16,9 @@
*/
package org.meshtastic.core.network.di
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Module
import org.koin.core.annotation.Single
@Module
@ComponentScan("org.meshtastic.core.network")
class CoreNetworkAndroidModule {
@Single
fun provideHttpClient(json: Json): HttpClient = HttpClient(OkHttp) { install(ContentNegotiation) { json(json) } }
}
class CoreNetworkAndroidModule

View file

@ -121,6 +121,11 @@ dependencies {
implementation(libs.aboutlibraries.core)
implementation(libs.aboutlibraries.compose.m3)
// Coil image loading (network + SVG decoding for device hardware images)
implementation(libs.coil)
implementation(libs.coil.network.ktor3)
implementation(libs.coil.svg)
// Core KMP modules (JVM variants)
implementation(projects.core.common)
implementation(projects.core.di)
@ -177,7 +182,6 @@ dependencies {
implementation(libs.okio)
// Ktor HttpClient (Java engine for JVM/Desktop)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.java)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)

View file

@ -17,6 +17,7 @@
package org.meshtastic.desktop
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@ -28,7 +29,16 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.toComposeImageBitmap
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.isMetaPressed
import androidx.compose.ui.input.key.isShiftPressed
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.type
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Notification
@ -41,11 +51,21 @@ import androidx.compose.ui.window.rememberWindowState
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberNavBackStack
import co.touchlab.kermit.Logger
import coil3.ImageLoader
import coil3.compose.setSingletonImageLoaderFactory
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
import coil3.network.ktor3.KtorNetworkFetcherFactory
import coil3.request.crossfade
import coil3.svg.SvgDecoder
import kotlinx.coroutines.flow.first
import okio.Path.Companion.toPath
import org.jetbrains.skia.Image
import org.koin.core.context.startKoin
import org.meshtastic.core.common.util.MeshtasticUri
import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.navigation.MeshtasticNavSavedStateConfig
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.navigation.TopLevelDestination
import org.meshtastic.core.service.MeshServiceOrchestrator
import org.meshtastic.core.ui.theme.AppTheme
@ -73,6 +93,26 @@ import java.util.Locale
*/
private val LocalAppLocale = staticCompositionLocalOf { "" }
private const val MEMORY_CACHE_MAX_BYTES = 64L * 1024L * 1024L // 64 MiB
private const val DISK_CACHE_MAX_BYTES = 32L * 1024L * 1024L // 32 MiB
/**
* Loads a [Painter] from a Java classpath resource path (e.g. `"icon.png"`).
*
* This replaces the deprecated `androidx.compose.ui.res.painterResource(String)` API. Desktop native-distribution icons
* (`.icns`, `.ico`) remain in `src/main/resources` for the packaging plugin; this helper reads the same directory at
* runtime.
*/
@Composable
private fun classpathPainterResource(path: String): Painter {
val bitmap: ImageBitmap =
remember(path) {
val bytes = Thread.currentThread().contextClassLoader!!.getResourceAsStream(path)!!.readAllBytes()
Image.makeFromEncoded(bytes).toComposeImageBitmap()
}
return remember(bitmap) { BitmapPainter(bitmap) }
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
fun main(args: Array<String>) = application(exitProcessOnExit = false) {
Logger.i { "Meshtastic Desktop — Starting" }
@ -130,7 +170,7 @@ fun main(args: Array<String>) = application(exitProcessOnExit = false) {
var isAppVisible by remember { mutableStateOf(true) }
var isWindowReady by remember { mutableStateOf(false) }
val trayState = rememberTrayState()
val appIcon = painterResource("icon.png")
val appIcon = classpathPainterResource("icon.png")
val notificationManager = remember { koinApp.koin.get<DesktopNotificationManager>() }
val desktopPrefs = remember { koinApp.koin.get<DesktopPreferencesDataSource>() }
@ -188,14 +228,81 @@ fun main(args: Array<String>) = application(exitProcessOnExit = false) {
)
if (isWindowReady && isAppVisible) {
val backStack =
rememberNavBackStack(MeshtasticNavSavedStateConfig, TopLevelDestination.Connections.route as NavKey)
Window(
onCloseRequest = { isAppVisible = false },
title = "Meshtastic Desktop",
icon = appIcon,
state = windowState,
onPreviewKeyEvent = { event ->
if (event.type != KeyEventType.KeyDown || !event.isMetaPressed) return@Window false
when {
// ⌘Q → Quit
event.key == Key.Q -> {
exitApplication()
true
}
// ⌘, → Settings
event.key == Key.Comma -> {
if (
TopLevelDestination.Settings != TopLevelDestination.fromNavKey(backStack.lastOrNull())
) {
navigateTopLevel(backStack, TopLevelDestination.Settings.route)
}
true
}
// ⌘⇧T → Toggle theme
event.key == Key.T && event.isShiftPressed -> {
uiPrefs.setTheme(if (isDarkTheme) 1 else 2)
true
}
// ⌘1 → Conversations
event.key == Key.One -> {
navigateTopLevel(backStack, TopLevelDestination.Conversations.route)
true
}
// ⌘2 → Nodes
event.key == Key.Two -> {
navigateTopLevel(backStack, TopLevelDestination.Nodes.route)
true
}
// ⌘3 → Map
event.key == Key.Three -> {
navigateTopLevel(backStack, TopLevelDestination.Map.route)
true
}
// ⌘4 → Connections
event.key == Key.Four -> {
navigateTopLevel(backStack, TopLevelDestination.Connections.route)
true
}
// ⌘/ → About
event.key == Key.Slash -> {
backStack.add(SettingsRoutes.About)
true
}
else -> false
}
},
) {
val backStack =
rememberNavBackStack(MeshtasticNavSavedStateConfig, TopLevelDestination.Connections.route as NavKey)
// Configure Coil ImageLoader for desktop with SVG decoding and network fetching.
// This is the desktop equivalent of the Android app's NetworkModule.provideImageLoader().
setSingletonImageLoaderFactory { context ->
val cacheDir = System.getProperty("user.home") + "/.meshtastic/image_cache"
ImageLoader.Builder(context)
.components {
add(KtorNetworkFetcherFactory())
add(SvgDecoder.Factory())
}
.memoryCache { MemoryCache.Builder().maxSizeBytes(MEMORY_CACHE_MAX_BYTES).build() }
.diskCache {
DiskCache.Builder().directory(cacheDir.toPath()).maxSizeBytes(DISK_CACHE_MAX_BYTES).build()
}
.crossfade(true)
.build()
}
// Providing localePref via a staticCompositionLocalOf forces the entire subtree to
// recompose when the locale changes — CMP Resources' rememberResourceEnvironment then
@ -207,3 +314,11 @@ fun main(args: Array<String>) = application(exitProcessOnExit = false) {
}
}
}
/** Replaces the backstack with a single top-level destination route. */
private fun navigateTopLevel(backStack: MutableList<NavKey>, route: NavKey) {
backStack.add(route)
while (backStack.size > 1) {
backStack.removeAt(0)
}
}

View file

@ -1,6 +1,6 @@
# Roadmap
> Last updated: 2026-03-17
> Last updated: 2026-03-23
Forward-looking priorities for the Meshtastic KMP multi-target effort. For current state, see [`kmp-status.md`](./kmp-status.md). For the full gap analysis, see [`decisions/architecture-review-2026-03.md`](./decisions/architecture-review-2026-03.md).
@ -42,7 +42,7 @@ These items address structural gaps identified in the March 2026 architecture re
- Test navigation flows end-to-end
2. **Tier 2: Polish (High Priority)**
- Additional desktop-specific settings polish
- ✅ **MenuBar integration** and Keyboard shortcuts
- ✅ **Keyboard shortcuts** via `onPreviewKeyEvent` (MenuBar removed)
- Window management
- State persistence
3. **Tier 3: Advanced (Nice-to-have)**
@ -74,7 +74,7 @@ These items address structural gaps identified in the March 2026 architecture re
| Charts | ✅ Vico KMP charts wired in commonMain (Device, Environment, Signal, Power, Pax) |
| Debug Panel | ✅ Real screen (mesh log viewer via shared `DebugViewModel`) |
| Notifications | ✅ Desktop native notifications with system tray icon support |
| MenuBar | ✅ Done — Native application menu bar with File/View menus |
| MenuBar | ✅ Removed — replaced with `onPreviewKeyEvent` keyboard shortcuts (⌘Q, ⌘,, ⌘⇧T, ⌘1-4, ⌘/) |
| About | ✅ Shared `commonMain` screen (AboutLibraries KMP `produceLibraries` + per-platform JSON) |
| Packaging | ✅ Done — Native distribution pipeline in CI (DMG, MSI, DEB) |

View file

@ -45,7 +45,7 @@ kotlin {
implementation(projects.core.network)
implementation(projects.feature.settings)
implementation(libs.jetbrains.navigation3.runtime)
implementation(libs.jetbrains.navigation3.ui)
}
androidMain.dependencies { implementation(libs.usb.serial.android) }

View file

@ -32,7 +32,6 @@ kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.jetbrains.navigation3.runtime)
implementation(libs.jetbrains.navigation3.ui)
implementation(projects.core.ble)
implementation(projects.core.common)
@ -48,19 +47,18 @@ kotlin {
implementation(projects.core.resources)
implementation(projects.core.ui)
implementation(libs.coil)
implementation(libs.kable.core)
implementation(libs.kotlinx.collections.immutable)
implementation(libs.ktor.client.core)
implementation(libs.markdown.renderer)
implementation(libs.markdown.renderer.m3)
}
androidMain.dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.nordic.dfu)
implementation(libs.coil)
implementation(libs.coil.network.okhttp)
implementation(libs.markdown.renderer.android)
implementation(libs.markdown.renderer.m3)
implementation(libs.markdown.renderer)
}
val androidHostTest by getting {

View file

@ -36,11 +36,9 @@ kotlin {
implementation(projects.core.ui)
implementation(projects.core.resources)
implementation(libs.jetbrains.navigation3.runtime)
implementation(libs.jetbrains.navigation3.ui)
}
androidMain.dependencies { implementation(libs.jetbrains.navigation3.ui) }
androidUnitTest.dependencies {
implementation(libs.junit)
implementation(libs.robolectric)

View file

@ -29,9 +29,8 @@ kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.jetbrains.navigation3.runtime)
implementation(libs.jetbrains.navigation3.ui)
implementation(projects.core.common)
implementation(libs.kotlinx.collections.immutable)
implementation(projects.core.data)
implementation(projects.core.database)
implementation(projects.core.datastore)

View file

@ -42,7 +42,7 @@ kotlin {
implementation(projects.core.service)
implementation(projects.core.ui)
implementation(libs.jetbrains.navigation3.runtime)
implementation(libs.jetbrains.navigation3.ui)
implementation(libs.jetbrains.navigationevent.compose)
implementation(libs.androidx.paging.common)
implementation(libs.androidx.paging.compose)

View file

@ -48,12 +48,11 @@ kotlin {
implementation(projects.core.di)
implementation(projects.feature.map)
implementation(libs.jetbrains.navigation3.runtime)
implementation(libs.jetbrains.navigation3.ui)
implementation(libs.kotlinx.collections.immutable)
implementation(libs.markdown.renderer)
implementation(libs.markdown.renderer.m3)
implementation(libs.vico.compose)
implementation(libs.vico.compose.m2)
implementation(libs.vico.compose.m3)
// JetBrains Material 3 Adaptive (multiplatform ListDetailPaneScaffold)
@ -65,10 +64,7 @@ kotlin {
androidMain.dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.coil)
implementation(libs.markdown.renderer.android)
implementation(libs.markdown.renderer.m3)
implementation(libs.markdown.renderer)
}
androidUnitTest.dependencies {

View file

@ -47,7 +47,6 @@ kotlin {
implementation(libs.kotlinx.collections.immutable)
implementation(libs.aboutlibraries.compose.m3)
implementation(libs.jetbrains.navigation3.runtime)
implementation(libs.jetbrains.navigation3.ui)
}
@ -55,11 +54,6 @@ kotlin {
implementation(projects.core.barcode)
implementation(projects.core.nfc)
implementation(libs.androidx.appcompat)
implementation(libs.coil)
implementation(libs.markdown.renderer.android)
implementation(libs.markdown.renderer.m3)
implementation(libs.markdown.renderer)
}
androidUnitTest.dependencies {

View file

@ -33,6 +33,7 @@ dependencies {
implementation(projects.core.resources)
implementation(projects.core.repository)
implementation(libs.androidx.compose.ui) // LocalConfiguration, LocalDensity
implementation(libs.androidx.glance.appwidget)
implementation(libs.androidx.glance.material3)
implementation(libs.androidx.glance.preview)

View file

@ -11,12 +11,7 @@
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
#Sat Feb 28 21:28:07 CST 2026
android.enableJetifier=false
android.enableR8.fullMode=true
android.experimental.lint.analysisPerComponent=true
android.nonTransitiveRClass=true
android.useAndroidX=true
dependency.analysis.print.build.health=true
enableComposeCompilerMetrics=false
enableComposeCompilerReports=false
kotlin.code.style=official
@ -26,7 +21,6 @@ ksp.incremental=true
ksp.incremental.classpath=true
ksp.incremental.intermodule=true
ksp.run.in.process=true
ksp.useKSP2=true
org.gradle.caching=true
org.gradle.configuration-cache=true
org.gradle.configureondemand=false

View file

@ -98,9 +98,6 @@ jetbrains-lifecycle-runtime = { module = "org.jetbrains.androidx.lifecycle:lifec
jetbrains-lifecycle-runtime-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "jetbrains-lifecycle" }
jetbrains-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "jetbrains-lifecycle" }
jetbrains-lifecycle-viewmodel-navigation3 = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "jetbrains-lifecycle" }
# JetBrains Navigation 3 currently publishes `navigation3-ui` (no separate `navigation3-runtime` artifact).
# Both `jetbrains-navigation3-runtime` and `jetbrains-navigation3-ui` resolve to the same coordinate.
jetbrains-navigation3-runtime = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "navigation3" }
jetbrains-navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "navigation3" }
jetbrains-navigationevent-compose = { module = "org.jetbrains.androidx.navigationevent:navigationevent-compose", version.ref = "navigationevent" }
androidx-paging-common = { module = "androidx.paging:paging-common", version.ref = "paging" }
@ -178,12 +175,12 @@ kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serializa
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
# Networking
ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-java = { module = "io.ktor:ktor-client-java", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
okhttp3-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version = "5.3.2" }
# Testing
androidx-test-core = { module = "androidx.test:core", version = "1.7.0" }
@ -204,11 +201,10 @@ aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref
aboutlibraries-compose-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlibraries" }
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
coil = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
coil-network-ktor3 = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil" }
dd-sdk-android-compose = { module = "com.datadoghq:dd-sdk-android-compose", version.ref = "dd-sdk-android" }
dd-sdk-android-logs = { module = "com.datadoghq:dd-sdk-android-logs", version.ref = "dd-sdk-android" }
dd-sdk-android-okhttp = { module = "com.datadoghq:dd-sdk-android-okhttp", version.ref = "dd-sdk-android" }
dd-sdk-android-rum = { module = "com.datadoghq:dd-sdk-android-rum", version.ref = "dd-sdk-android" }
dd-sdk-android-timber = { module = "com.datadoghq:dd-sdk-android-timber", version.ref = "dd-sdk-android" }
dd-sdk-android-trace = { module = "com.datadoghq:dd-sdk-android-trace", version.ref = "dd-sdk-android" }
@ -233,7 +229,6 @@ kermit = { module = "co.touchlab:kermit", version = "2.1.0" }
usb-serial-android = { module = "com.github.mik3y:usb-serial-for-android", version = "3.10.0" }
vico-compose = { group = "com.patrykandpatrick.vico", name = "compose", version.ref = "vico" }
vico-compose-m2 = { group = "com.patrykandpatrick.vico", name = "compose-m2", version.ref = "vico" }
vico-compose-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico" }
# Build Logic
@ -283,7 +278,6 @@ secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugi
# Firebase
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebase-crashlytics-gradle" }
firebase-perf = { id = "com.google.firebase.firebase-perf", version = "2.0.2" }
# Other
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" }
@ -306,7 +300,6 @@ meshtastic-android-library-compose = { id = "meshtastic.android.library.compose"
meshtastic-android-library-flavors = { id = "meshtastic.android.library.flavors" }
meshtastic-android-lint = { id = "meshtastic.android.lint" }
meshtastic-android-room = { id = "meshtastic.android.room" }
meshtastic-android-test = { id = "meshtastic.android.test" }
meshtastic-detekt = { id = "meshtastic.detekt" }
meshtastic-koin = { id = "meshtastic.koin" }
meshtastic-kotlinx-serialization = { id = "meshtastic.kotlinx.serialization" }