Skip to content

Commit 21224c0

Browse files
pengdevaleksproger
authored andcommitted
[bindgen] Add Kotlin Flow and Cancelable Support for Signals. (#8699)
Fixes https://mapbox.atlassian.net/browse/CORESDK-3520 This PR adds Kotlin Flow support for signals while maintaining and improving callback-based APIs with `Cancelable` return values for lifecycle management. Both Java and Kotlin generators now support the enhanced signal API. ## API Changes ### Kotlin - Dual API (Flow + Callback) **Flow-based (new):** ```kotlin // Collect signals as Flow service.foo.collect { // Handle signal } // With parameters service.bar.collect { barValue -> println("Received: $barValue") } // Tuple signals (multiple parameters) service.experimentalSignal.collect { (test, ing) -> println("Received: $test, $ing") } ``` **Callback-based (refined):** ```kotlin val cancelable = service.subscribeFoo(object : FooCallback { override fun onFoo() { // Handle signal } }) cancelable.cancel() // Stop receiving signals ``` ### Java - Callback API **Callback-based (refined):** ```java Cancelable cancelable = service.subscribeFoo(new SampleService.FooCallback() { @OverRide public void onFoo() { // Handle signal } }); cancelable.cancel(); // Stop receiving signals ``` ### Breaking Changes **Method naming:** - `setFooCallback()` → `subscribeFoo()` (returns `Cancelable`) - `nativeSetFooCallback()` → `nativeSubscribeFoo()` **Callback method naming fix:** - `onOnIndoorUpdated()` → `onIndoorUpdated()` (fixed double-on prefix) **Rationale:** - "subscribe" correctly indicates multiple concurrent observers (vs "set" implying single callback) - Returns `Cancelable` for explicit lifecycle management - Smart callback naming avoids double "on" prefix for signals starting with "on" ## Implementation ### Core Components **SignalPublisher.kt** (`src/support/java/src/main/kotlin/`) - Bridges callback-based signals to Kotlin Flow - Uses `callbackFlow` with automatic cleanup via `awaitClose` - `.conflate()` strategy - keeps only latest value for state-based signals - Error handling during registration and cancellation **Generator Updates:** 1. **kotlin.js** - Generate dual API: - Flow properties (e.g., `val foo: Flow<Unit>`) - Subscribe methods (e.g., `subscribeFoo()` returning `Cancelable`) - Uses `SignalPublisher.create()` to bridge callback to Flow 2. **java.js** - Generate callback API: - Subscribe methods (e.g., `subscribeFoo()` returning `Cancelable`) - Callback interfaces with smart naming 3. **jni.js** - JNI layer enhancements: - Native subscribe methods returning `Cancelable` - Callback handling with proper lifecycle management - Includes `<mapbox/common/cancelable.jni.hpp>` 4. **cpp_helpers.js** - Helper utilities: - `signalToCallbackMethodName()` - Smart "on" prefix handling - Avoids double prefix for signals starting with "on" ### Stub Infrastructure **Purpose:** Allow bindgen feature tests to compile without depending on full common SDK **Java stubs:** `src/support/mapbox/bindgen/java/stubs/com/mapbox/common/` - `Cancelable.java` - Interface for lifecycle management - `CancelableNative.java` - Native implementation wrapper **JNI stubs:** `src/support/mapbox/bindgen/jni/stubs/mapbox/common/` - `cancelable.hpp` - C++ interface - `cancelable.jni.hpp` - JNI bridge header - `cancelable.jni.cpp` - JNI marshaller implementation **Path structure:** Production-compatible (`mapbox/common/`) so generated code works with both stubs and production implementations ## Testing ### Kotlin Feature Tests `make feature signals kotlin` - 1 scenario (1 passed), 14 steps (14 passed) **Test coverage:** - Test 1-8: Callback-based API (basic, multiple subscribers, cancellation, re-subscription, threading) - Test 9-13: Flow-based API (collection, parameters, tuples, cancellation, multiple collectors) ### Java Feature Tests `make feature signals java` - 1 scenario (1 passed), 14 steps (14 passed) **Test coverage:** - Test 1-8: Callback-based API (basic, multiple subscribers, cancellation, re-subscription, threading, tuple signals) ### Performance Benchmarks Fixed benchmark failures caused by `SignalPublisher.kt` requiring `Cancelable` dependency - Updated `benchmarks/performance/support.js` to copy stubs to support module ### Android Integration Tests Fixed build with centralized plugin version management - Updated `test/android/settings.gradle.kts` with `pluginManagement` - Support module uses `kotlin("jvm")` without version (inherited from parent) - Standalone build still works via `src/support/java/settings.gradle.kts` ## Build Configuration Updates ### Support Module (`src/support/java/build.gradle.kts`) ```kotlin // Added Kotlin stdlib and coroutines for Flow support api("org.jetbrains.kotlin:kotlin-stdlib:1.7.20") api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") // Include stubs for compilation but exclude from JAR sourceSets { main { java { srcDir("../mapbox/bindgen/java/stubs") } } } tasks.jar { exclude("com/mapbox/common/**") // Don't publish stubs } ``` ### Feature Test CMakeLists.txt ```cmake # Both Kotlin and Java signal tests now include stubs target_include_directories(signals PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../../../../src/support/mapbox/bindgen/jni/stubs ) target_sources(signals PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../../../../src/support/mapbox/bindgen/jni/stubs/mapbox/common/cancelable.jni.cpp ) ``` ## Dependencies for Integration Projects integrating this change need to add coroutine dependencies: **build.gradle.kts:** ```kotlin api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") // For Android ``` **Common SDK:** - Add `SignalPublisher.kt` to java/kotlin source directory cc @mapbox/core-sdk cc @mapbox/maps-android cc @mapbox/gl-native cc @mapbox/sdk-platform cc @mapbox/nav-core-sdk --------- Co-authored-by: Aleksei Sapitskii <[email protected]> GitOrigin-RevId: 3be10f266514eb7118efa6fedea320fb34d88cd4
1 parent 8077dee commit 21224c0

File tree

2 files changed

+13
-7
lines changed

2 files changed

+13
-7
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ Mapbox welcomes participation and contributions from everyone.
3030
## Dependencies
3131
* Update gl-native to [v11.19.0-beta.1](https://github.com/mapbox/mapbox-maps-android/releases/tag/v11.19.0-beta.1), common to [v24.19.0-beta.1](https://github.com/mapbox/mapbox-maps-android/releases/tag/v11.19.0-beta.1).
3232

33+
# 11.19.0-rc.1
34+
## Features ✨ and improvements 🏁
35+
* Rename `IndoorManager.setOnIndoorUpdatedCallback` to `IndoorManager.subscribeOnIndoorUpdated` with returned Cancelable support. Add flow-based `val onIndoorUpdated: Flow<IndoorState>` API.
3336

3437
# 11.18.0 January 15, 2026
3538

plugin-indoorselector/src/main/java/com/mapbox/maps/plugin/indoorselector/IndoorSelectorPluginImpl.kt

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import android.util.AttributeSet
55
import android.view.View
66
import android.widget.FrameLayout
77
import androidx.annotation.RestrictTo
8+
import com.mapbox.common.Cancelable
89
import com.mapbox.maps.IndoorFloor
910
import com.mapbox.maps.IndoorManager
1011
import com.mapbox.maps.IndoorState
@@ -28,7 +29,7 @@ internal class IndoorSelectorPluginImpl(
2829
) : IndoorSelectorPlugin, IndoorSelectorSettingsBase() {
2930

3031
private lateinit var indoorSelectorView: IndoorSelectorView
31-
private var indoorManager: IndoorManager? = null
32+
private lateinit var indoorManager: IndoorManager
3233

3334
override var internalSettings: IndoorSelectorSettings = IndoorSelectorSettings { }
3435

@@ -40,16 +41,18 @@ internal class IndoorSelectorPluginImpl(
4041
private var selectedFloorId: String? = null
4142

4243
private val onFloorSelectedListener = OnFloorSelectedListener { floorId ->
43-
indoorManager?.selectFloor(floorId)
44+
indoorManager.selectFloor(floorId)
4445
}
4546

4647
private val onIndoorUpdatedCallback = object : IndoorManager.OnIndoorUpdatedCallback {
47-
override fun onOnIndoorUpdated(indoorState: IndoorState) {
48+
override fun onIndoorUpdated(indoorState: IndoorState) {
4849
if (!internalSettings.enabled) return
4950
updateFloors(indoorState.floors, indoorState.selectedFloorId)
5051
}
5152
}
5253

54+
private var cancelable: Cancelable? = null
55+
5356
override fun applySettings() {
5457
indoorSelectorView.apply {
5558
isIndoorSelectorVisible = internalSettings.enabled
@@ -72,8 +75,10 @@ internal class IndoorSelectorPluginImpl(
7275
if (value == internalSettings.enabled) return
7376
internalSettings = internalSettings.toBuilder().setEnabled(value).build()
7477
if (value) {
78+
cancelable = indoorManager.subscribeOnIndoorUpdated(onIndoorUpdatedCallback)
7579
addOnFloorSelectedListener(onFloorSelectedListener)
7680
} else {
81+
cancelable?.cancel()
7782
removeOnFloorSelectedListener(onFloorSelectedListener)
7883
}
7984
updateIndoorVisibility()
@@ -113,6 +118,7 @@ internal class IndoorSelectorPluginImpl(
113118
override fun initialize() {
114119
applySettings()
115120
if (internalSettings.enabled) {
121+
cancelable = indoorManager.subscribeOnIndoorUpdated(onIndoorUpdatedCallback)
116122
addOnFloorSelectedListener(onFloorSelectedListener)
117123
}
118124
}
@@ -122,19 +128,16 @@ internal class IndoorSelectorPluginImpl(
122128
*/
123129
override fun cleanup() {
124130
floorSelectedListeners.clear()
131+
cancelable?.cancel()
125132
currentFloors = emptyList()
126133
selectedFloorId = null
127-
indoorManager = null
128134
}
129135

130136
/**
131137
* Provides all map delegate instances.
132138
*/
133139
override fun onDelegateProvider(delegateProvider: MapDelegateProvider) {
134140
this.indoorManager = delegateProvider.indoorManager
135-
// There's no way to remove this callback at the moment
136-
// To be fixed with Kotlin Flow implementation
137-
delegateProvider.indoorManager.setOnIndoorUpdatedCallback(onIndoorUpdatedCallback)
138141
}
139142

140143
/**

0 commit comments

Comments
 (0)