Implement withLatestFrom for multiple flows (3-10 flows) with optimized Array-based implementation
This PR extends the withLatestFrom operator to support combining multiple flows (3-10 flows), addressing the issue request for "withLatestFrom for many flows".
Changes Made
Core Implementation
- Added 8 new overloads of
withLatestFrominsrc/commonMain/kotlin/com/hoc081098/flowext/withLatestFrom.ktsupporting 3-10 flows - Refactored implementation to eliminate code duplication by introducing a new public Array-based function that all overloads use internally
- Includes both transform function variants and a convenience overload returning
Triple<A, B, C>for 3 flows - Proper resource management with
coroutineScopeand job cancellation
Array-Based Core Function
-
New public function
withLatestFrom(others: Array<out Flow<*>>, transform: suspend (A, Array<Any?>) -> R)that serves as the foundation for all multi-flow overloads - Uses
Array<AtomicRef<Any?>>for efficient thread-safe latest value storage - Eliminates code duplication while maintaining the same API and performance characteristics
- Code reduction: 48% smaller implementation (736 → 382 lines)
API Examples
// 3 flows with Triple return
val result = sourceFlow
.withLatestFrom(flow2, flow3)
.collect { (a, b, c) -> println("$a, $b, $c") }
// 4 flows with custom transform
val result = sourceFlow
.withLatestFrom(flow2, flow3, flow4) { a, b, c, d ->
"$a-$b-$c-$d"
}
// Up to 10 flows supported
val result = sourceFlow
.withLatestFrom(f2, f3, f4, f5, f6, f7, f8, f9, f10) { a, b, c, d, e, f, g, h, i, j ->
// Transform logic here
}
// Direct Array-based usage (public API)
val result = sourceFlow
.withLatestFrom(arrayOf(flow2, flow3, flow4)) { value, others ->
// Custom transformation with others[0], others[1], others[2]
}
Test Coverage
- Added comprehensive tests in
src/commonTest/kotlin/com/hoc081098/flowext/WithLatestFromTest.kt - Tests cover basic functionality, timing semantics, error handling, cancellation, and null values
- All 259 tests pass, ensuring no regressions
Implementation Details
- Uses the same
INTERNAL_NULL_VALUEpattern for null handling as existing code - Follows the exact same concurrency model with
launch(start = CoroutineStart.UNDISPATCHED) - Consistent with the library's patterns established in
combine.kt - Proper cleanup of atomic references in finally blocks
- Maintainable design: All typed overloads delegate to the Array-based function, making future changes easier
The implementation maintains the core semantics of withLatestFrom: the source flow drives emissions, and only emits when all other flows have emitted at least once, using the latest values from each flow.
Original prompt
This section details on the original issue you should resolve
<issue_title>withLatestFrom for many flows</issue_title> <issue_description></issue_description>
<agent_instructions>unit tests, docs, and readme.md</agent_instructions>
Comments on the Issue (you are @copilot in this section)
💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click here to start the survey.
@copilot I see the code pattern is duplicate? Could you introduce a new function that accepts a Array or List of Flows, then 3-10 flows overloads use this function. The new function should be public.
@copilot I see the code pattern is duplicate? Could you introduce a new function that accepts a Array or List of Flows, then 3-10 flows overloads use this function. The new function should be public.
I've refactored the code to eliminate duplication as requested. Created a new public function that accepts Array<out Flow<*>> and updated all 3-10 flow overloads to use it. This reduced the file from 736 to 382 lines (48% reduction) while maintaining the same API and passing all 259 tests. The changes are in commit b468010.
[!WARNING]
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
dl.google.com
- Triggering command:
/usr/lib/jvm/temurin-17-jdk-amd64/bin/java --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED --add-opens=java.base/java.nio.charset=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens=java.xml/javax.xml.namespace=ALL-UNNAMED -XX:+UseParallelGC -Xmx4g -Dfile.encoding=UTF-8 -Duser.country -Duser.language=en -Duser.variant -cp /home/REDACTED/.gradle/wrapper/dists/gradle-9.0.0-bin/d6wjpkvcgsg3oed0qlfss3wgl/gradle-9.0.0/lib/gradle-daemon-main-9.0.0.jar -javaagent:/home/REDACTED/.gradle/wrapper/dists/gradle-9.0.0-bin/d6wjpkvcgsg3oed0qlfss3wgl/gradle-9.0.0/lib/agents/gradle-instrumentation-agent-9.0.0.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 9.0.0(dns block)If you need me to access, download, or install something from one of these locations, you can either:
- Configure Actions setup steps to set up my environment, which run before the firewall is enabled
- Add the appropriate URLs or hosts to the custom allowlist in this repository's Copilot coding agent settings (admins only)