Skip to content

Conversation

@RudolfHladik
Copy link
Collaborator

@RudolfHladik RudolfHladik commented Aug 12, 2025

Refactor execution methods to be defined not as extensions but with context parameters for more flexibility.
Now we can customize behavior of execution methods like executeWithLifecycle

Summary by CodeRabbit

  • New Features

    • First screen now shows a live, lifecycle-aware timestamp that updates every second.
    • Counter now ticks every 1 second (previously 200 ms) for a calmer, more readable experience and reduced background activity.
  • Chores

    • Upgraded Kotlin, KSP, coroutines, Koin, Apollo, Ktor, and other libraries for compatibility and stability.
    • Build and Gradle settings adjusted; internal module dependency path updated.

Refactor lifecycle-aware use case operation into shared `executeWithLifecycle` functions for streamlined component integration. Remove duplicate implementations and apply adjustments in affected areas.
@coderabbitai
Copy link

coderabbitai bot commented Aug 12, 2025

Walkthrough

  • Gradle: Switched kotlinOptions to compilerOptions with JvmTarget; enabled -Xcontext-parameters in two modules; updated numerous dependency versions; disabled Gradle caching and configuration cache; changed includes/dependencies from arkitekt-decompose to arkitekt-decompose:architecture.
  • Arkitekt CR Usecases: Replaced UseCaseExecutionScope/SingleUseCaseExecutionScope/FlowUseCaseExecutionScope with CoroutineScopeOwner-based APIs; added UseCaseConfig and FlowUseCaseConfig builders; added new execute(...) and executeMapped(...) extensions for UseCase and FlowUseCase; added launchWithHandler/defaultErrorHandler/getOrCancel utilities; enabled context parameters.
  • Feature: Added TimeStampUseCase (flow of Instant every second) and lifecycle-aware executeWithLifecycle helpers; FirstComponent now depends on TimeStampUseCase and uses lifecycle-driven timestamp stream.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor UI
  participant FC as FirstComponent (CoroutineScopeOwner)
  participant L as LifecycleOwner
  participant TS as TimeStampUseCase (FlowUseCase)
  participant C as FlowUseCaseConfig
  participant W as Worker Dispatcher
  participant H as UseCaseErrorHandler

  UI->>FC: doOnCreate()
  FC->>FC: runTimestamp()
  FC->>C: Build FlowUseCaseConfig { onStart/onNext/onError/onComplete }
  FC->>L: Observe lifecycle(minState=RESUMED)
  L-->>FC: Emits active/inactive states
  FC->>TS: build(Unit)
  TS-->>FC: Flow<Instant>
  FC->>W: flowOn(worker)
  FC->>FC: flatMapLatest(active? flow : emptyFlow)
  FC->>FC: onStart -> update UI
  FC->>FC: onEach(onNext) -> updateCreatedAtTimestamp(Instant)
  FC->>FC: onCompletion
  alt Cancellation
    FC-->>FC: Ignore
  else Error
    FC->>H: globalOnErrorLogger(error)
    FC->>FC: onError(error)
  else Success
    FC-->>FC: onComplete()
  end
Loading
sequenceDiagram
  autonumber
  actor UI
  participant VM as CoroutineScopeOwner
  participant U as UseCase<ARGS,T>
  participant C as UseCaseConfig
  participant W as Worker Dispatcher
  participant H as UseCaseErrorHandler

  UI->>VM: Execute use case
  VM->>C: Build UseCaseConfig { onStart/onSuccess/onError/disposePrevious }
  VM->>VM: Cancel previous if configured
  VM->>VM: onStart()
  VM->>W: async(start=LAZY) { U.build(args) }
  W-->>VM: result or error
  alt Success
    VM-->>VM: onSuccess(value)
  else Cancellation
    VM-->>VM: propagate CancellationException
  else Error
    VM->>H: globalOnErrorLogger(error)
    VM-->>VM: onError(error)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/rh/context-params

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (8)
shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/FlowUseCaseConfig.kt (1)

20-27: Consider improving type safety for the mapping transformation.

The current design allows Builder<T, M> to be instantiated without ensuring that onMap is provided when T != M. This could lead to runtime issues when a transformation is expected but not configured.

Consider one of these approaches:

Option 1: Separate builders for mapped vs unmapped scenarios

+class Builder<T> {
+    // For T -> T scenarios (no mapping)
+    fun build(): FlowUseCaseConfig<T, T>
+}
+
+class MappingBuilder<T, M> {
+    // For T -> M scenarios (requires mapping)
+    fun onMap(onMap: (T) -> M): MappingBuilder<T, M>
+    fun build(): FlowUseCaseConfig<T, M>
+}

Option 2: Sealed interface with type constraints

+sealed interface ConfigBuilder<T, M> {
+    class Direct<T> : ConfigBuilder<T, T>
+    class Mapped<T, M>(val onMap: (T) -> M) : ConfigBuilder<T, M>
+}
shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/CoroutineScopeOwner.kt (2)

33-47: Consider documenting the rethrow behavior in defaultErrorHandler

The launchWithHandler method provides useful error handling, but the default behavior of rethrowing exceptions in defaultErrorHandler (line 53) could cause crashes if not overridden. Consider documenting this behavior more prominently in the method's KDoc.

Add a warning to the KDoc:

 /**
  * Launch suspend [block] in [viewModelScope].
  *
  * Encapsulates this call with try catch block and when an exception is thrown
  * then it is logged in [UseCaseErrorHandler.globalOnErrorLogger] and handled by [defaultErrorHandler].
  *
  * If exception is [CancellationException] then [defaultErrorHandler] is not called and
  * [UseCaseErrorHandler.globalOnErrorLogger] is called only if the root cause of this exception is not
  * [CancellationException] (e.g. when [Result.getOrCancel] is used).
+ *
+ * @warning The default implementation of [defaultErrorHandler] rethrows the exception,
+ * which will crash the application. Override [defaultErrorHandler] to handle errors gracefully.
  */

56-56: Add cause parameter to CancellationException for better debugging

The getOrCancel extension throws a CancellationException without preserving the original error as a cause, which makes debugging harder.

-fun <T> Result<T>.getOrCancel(): T = this.getOrElse { throw CancellationException("Result was not Success") }
+fun <T> Result<T>.getOrCancel(): T = this.getOrElse { throw CancellationException("Result was not Success", it) }
shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/UseCaseConfig.kt (1)

71-71: Remove commented closing brace

There's a commented closing brace that appears to be leftover from development.

-//}
shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/firstScreen/FirstComponent.kt (1)

80-94: Consider extracting the interval constant

The hardcoded interval value of 1000ms could be extracted to a constant for better maintainability, similar to COUNTER_ALERT_AT_SECONDS.

 companion object {
     private const val COUNTER_ALERT_AT_SECONDS = 30L
+    private const val COUNTER_INTERVAL_MS = 1000L
 }

 private fun observeCounter() {
-    counterUseCase.execute(CounterUseCaseArgs(interval = 1000.milliseconds)) {
+    counterUseCase.execute(CounterUseCaseArgs(interval = COUNTER_INTERVAL_MS.milliseconds)) {
shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/tools/LifecycleExecution.kt (2)

46-78: Consider simplifying the lifecycle state management logic

The current implementation has repetitive code for managing enableExecution state across different lifecycle callbacks. This could be simplified using a helper function.

+    fun updateExecutionState(currentState: Lifecycle.State) {
+        enableExecution.value = currentState.ordinal >= minActiveState.ordinal
+    }
+
     componentContext.lifecycle.subscribe(
         onCreate = {
-            if (minActiveState == Lifecycle.State.CREATED) {
-                enableExecution.value = true
-            }
+            if (minActiveState == Lifecycle.State.CREATED) updateExecutionState(Lifecycle.State.CREATED)
         },
         onDestroy = {
-            if (minActiveState == Lifecycle.State.CREATED) {
-                enableExecution.value = false
-            }
+            if (minActiveState == Lifecycle.State.CREATED) updateExecutionState(Lifecycle.State.DESTROYED)
         },
         onStart = {
-            if (minActiveState == Lifecycle.State.STARTED) {
-                enableExecution.value = true
-            }
+            if (minActiveState == Lifecycle.State.STARTED) updateExecutionState(Lifecycle.State.STARTED)
         },
         onStop = {
-            if (minActiveState == Lifecycle.State.STARTED) {
-                enableExecution.value = false
-            }
+            if (minActiveState == Lifecycle.State.STARTED) updateExecutionState(Lifecycle.State.CREATED)
         },
         onResume = {
-            if (minActiveState == Lifecycle.State.RESUMED) {
-                enableExecution.value = true
-            }
+            if (minActiveState == Lifecycle.State.RESUMED) updateExecutionState(Lifecycle.State.RESUMED)
         },
         onPause = {
-            if (minActiveState == Lifecycle.State.RESUMED) {
-                enableExecution.value = false
-            }
+            if (minActiveState == Lifecycle.State.RESUMED) updateExecutionState(Lifecycle.State.STARTED)
         },
     )

102-102: Consider adding a comment explaining the empty catch block

While the comment indicates errors are handled in onCompletion, it would be clearer to explain why we need the catch block at all.

-        .catch { /* handled in onCompletion */ }
+        .catch { /* Suppress exceptions as they're already handled in onCompletion */ }
shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/UseCase.kt (1)

85-119: Verify consistent parameter naming across execute methods.

The suspend version uses cancelPrevious while the async version uses disposePrevious (via config). Consider aligning the naming for consistency.

Apply this diff to align the parameter naming:

-    cancelPrevious: Boolean = true,
+    disposePrevious: Boolean = true,
): Result<T> {
-    if (cancelPrevious) {
+    if (disposePrevious) {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f7d6484 and ef91ad0.

📒 Files selected for processing (21)
  • androidApp/build.gradle.kts (3 hunks)
  • gradle.properties (1 hunks)
  • gradle/libs.versions.toml (1 hunks)
  • settings.gradle.kts (1 hunks)
  • shared/app/build.gradle.kts (2 hunks)
  • shared/arkitekt-cr-usecases/build.gradle.kts (1 hunks)
  • shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/FlowUseCase.kt (2 hunks)
  • shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/UseCase.kt (2 hunks)
  • shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/CoroutineScopeOwner.kt (2 hunks)
  • shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/FlowUseCaseConfig.kt (1 hunks)
  • shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/FlowUseCaseExecutionScope.kt (0 hunks)
  • shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/SingleUseCaseExecutionScope.kt (0 hunks)
  • shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/UseCaseConfig.kt (1 hunks)
  • shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/UseCaseExecutionScope.kt (0 hunks)
  • shared/arkitekt-cr-usecases/src/commonTest/kotlin/app/futured/arkitekt/crusecases/base/BaseUseCaseExecutionScopeTest.kt (2 hunks)
  • shared/arkitekt-decompose/architecture/src/commonMain/kotlin/app/futured/arkitekt/decompose/ext/NavigationExt.kt (0 hunks)
  • shared/arkitekt-decompose/architecture/src/commonMain/kotlin/app/futured/arkitekt/decompose/presentation/BaseComponent.kt (2 hunks)
  • shared/feature/build.gradle.kts (2 hunks)
  • shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/domain/TimeStampUseCase.kt (1 hunks)
  • shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/tools/LifecycleExecution.kt (1 hunks)
  • shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/firstScreen/FirstComponent.kt (4 hunks)
💤 Files with no reviewable changes (4)
  • shared/arkitekt-decompose/architecture/src/commonMain/kotlin/app/futured/arkitekt/decompose/ext/NavigationExt.kt
  • shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/UseCaseExecutionScope.kt
  • shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/SingleUseCaseExecutionScope.kt
  • shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/FlowUseCaseExecutionScope.kt
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-20T08:58:15.363Z
Learnt from: matejsemancik
PR: futuredapp/kmp-futured-template#125
File: shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/FlowUseCaseExecutionScope.kt:17-21
Timestamp: 2025-06-20T08:58:15.363Z
Learning: The useCaseJobPool map in FlowUseCaseExecutionScope overwrites job references for each UseCase instance rather than accumulating them, so cancelled jobs don't cause memory leaks as they become eligible for garbage collection when their references are replaced.

Applied to files:

  • shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/FlowUseCaseConfig.kt
  • shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/FlowUseCase.kt
🧬 Code Graph Analysis (5)
shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/CoroutineScopeOwner.kt (1)
shared/arkitekt-cr-usecases/src/commonTest/kotlin/app/futured/arkitekt/crusecases/UseCaseExecutionScopeTest.kt (3)
  • defaultErrorHandler (203-205)
  • defaultErrorHandler (224-226)
  • defaultErrorHandler (245-247)
shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/tools/LifecycleExecution.kt (1)
shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/domain/TimeStampUseCase.kt (2)
  • build (11-19)
  • build (13-18)
shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/UseCase.kt (2)
shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/FlowUseCase.kt (3)
  • execute (48-81)
  • execute (84-85)
  • build (31-31)
shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/UseCaseConfig.kt (1)
  • build (63-68)
shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/FlowUseCase.kt (9)
shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/domain/TimeStampUseCase.kt (2)
  • build (11-19)
  • build (13-18)
shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/FlowUseCaseConfig.kt (1)
  • build (82-89)
shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/UseCase.kt (4)
  • build (26-26)
  • execute (38-39)
  • execute (54-83)
  • execute (100-119)
shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/UseCaseConfig.kt (1)
  • build (63-68)
shared/arkitekt-cr-usecases/src/commonTest/kotlin/app/futured/arkitekt/crusecases/usecases/TestFailureUseCase.kt (2)
  • build (5-8)
  • build (7-7)
shared/arkitekt-cr-usecases/src/commonTest/kotlin/app/futured/arkitekt/crusecases/usecases/TestFlowUseCase.kt (1)
  • build (13-14)
shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/domain/CounterUseCase.kt (2)
  • build (12-22)
  • build (15-21)
shared/arkitekt-cr-usecases/src/commonTest/kotlin/app/futured/arkitekt/crusecases/usecases/TestFailureFlowUseCase.kt (2)
  • build (7-12)
  • build (9-11)
shared/arkitekt-cr-usecases/src/commonTest/kotlin/app/futured/arkitekt/crusecases/usecases/TestUseCase.kt (2)
  • build (6-12)
  • build (8-11)
shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/firstScreen/FirstComponent.kt (2)
shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/FlowUseCaseConfig.kt (2)
  • onNext (41-43)
  • onError (58-60)
shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/UseCaseConfig.kt (2)
  • onError (48-50)
  • onSuccess (39-41)
🪛 detekt (1.23.8)
shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/UseCase.kt

[warning] 75-75: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🔇 Additional comments (27)
shared/arkitekt-cr-usecases/build.gradle.kts (1)

24-26: LGTM! Context parameters support added correctly.

The -Xcontext-parameters compiler argument has been properly added to enable Kotlin context parameters feature, which aligns with the PR's objective to refactor from extension methods to context parameters for increased flexibility.

shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/FlowUseCaseConfig.kt (1)

8-15: LGTM! Clean data class design with proper encapsulation.

The private constructor ensures controlled instantiation through the builder pattern, and the nullable onMap parameter provides flexibility for optional transformations.

gradle/libs.versions.toml (2)

12-13: Confirm Koin 4.1.0 Upgrade with KoinAnnotations

Our scan didn’t find any direct Koin DSL usage (module { … } or startKoin { … })—the project relies entirely on KoinAnnotations (@Module, @Single, @Factory) via KSP. Before merging this bump:

  • Verify that koin-annotations 2.1.0 supports Koin core 4.1.0
  • Ensure the KSP plugin version in your build scripts aligns with koin-annotations 2.1.0
  • Review a few generated modules to confirm they import and invoke the correct Koin 4.1.0 APIs (e.g. single { … }, factory { … })

21-23: Compatibility Verification: Apollo 4.3.1, Ktorfit 2.6.1 & Ktor 3.2.1

Please run through this checklist to ensure there are no hidden incompatibilities in your Kotlin Multiplatform setup:

• Align Gradle & Kotlin toolchain
– Gradle 8+ with Kotlin 1.9+ (JVM/Android) or Kotlin 2.x (native/JS/Wasm)
– Matches JetBrains’ KMP compatibility matrix

• Apollo Kotlin 4.x migration
– Switch from apolloMetadata to Service.dependsOn(...)
– Centralize schema–type generation in your designated schema module

• HTTP engine consistency
– Default is OkHttp on JVM/Android – or add apollo-kotlin-ktor-support to use Ktor Client
– Ensure you don’t mix Ktor 2.x and Ktor 3.x artifacts; all Ktor client libs should be 3.x

• Ktorfit & Ktor alignment
– Apply the Ktorfit Gradle plugin as per its 2.6.1 changelog
– Verify or force its transitive Ktor client to match your explicit ktor = "3.2.1"

• Multiplatform features
– Confirm enabled Apollo features (e.g. normalized-cache-sqlite, http-cache) are supported on all your targets

To double-check your matrix, please share your exact Kotlin, Gradle, AGP and KSP versions so we can spot any toolchain mismatches.

settings.gradle.kts (2)

22-22: LGTM! Module structure reorganization is clear.

The change from :shared:arkitekt-decompose to :shared:arkitekt-decompose:architecture indicates a logical separation of concerns within the Arkitekt Decompose module structure.


22-24: All arkitekt-decompose module references are up-to-date

I’ve verified that there are no remaining references to the old “arkitekt-decompose” aggregate module and that each submodule is correctly wired:

• settings.gradle.kts
– includes “:shared:arkitekt-decompose:architecture”, “:annotation”, and “:processor”
• shared/feature/build.gradle.kts
– implementation(projects.shared.arkitektDecompose.architecture)
– implementation(projects.shared.arkitektDecompose.annotation)
• shared/app/build.gradle.kts
– export(projects.shared.arkitektDecompose.architecture)
– api(projects.shared.arkitektDecompose.architecture)
• androidApp/build.gradle.kts
– implementation(projects.shared.arkitektDecompose.architecture)
• shared/arkitekt-decompose/processor/build.gradle.kts
– implementation(projects.shared.arkitektDecompose.annotation)
• convention-plugins/src/main/kotlin/conventions-annotations-processor.gradle.kts
– add("kspCommonMainMetadata", project(":shared:arkitekt-decompose:processor"))

No legacy references remain.

shared/arkitekt-cr-usecases/src/commonTest/kotlin/app/futured/arkitekt/crusecases/base/BaseUseCaseExecutionScopeTest.kt (2)

3-3: Import updated to match new interface.

The import change from UseCaseExecutionScope to CoroutineScopeOwner correctly aligns with the architectural migration.


13-13: Class inheritance updated to new scope interface.

The class now correctly extends CoroutineScopeOwner instead of UseCaseExecutionScope, maintaining the same test infrastructure while supporting the new context parameter-based execution model.

shared/arkitekt-decompose/architecture/src/commonMain/kotlin/app/futured/arkitekt/decompose/presentation/BaseComponent.kt (2)

3-3: Import updated for architectural migration.

The import change from UseCaseExecutionScope to CoroutineScopeOwner correctly reflects the shift to context parameter-based execution.


29-29: Interface change aligns with PR objectives.

The class now implements CoroutineScopeOwner instead of UseCaseExecutionScope, enabling the new context parameter-based execution methods while maintaining existing functionality through the same viewModelScope property.

shared/feature/build.gradle.kts (2)

38-40: Context parameters compiler flag enables new execution model.

The -Xcontext-parameters compiler option correctly enables the Kotlin context receivers feature, which is essential for the new execution methods that use context parameters instead of extension functions.


58-58: Dependency updated to architecture submodule.

The change from arkitektDecompose to arkitektDecompose.architecture correctly reflects the modular restructuring while maintaining access to the base component functionality.

shared/app/build.gradle.kts (2)

50-50: iOS framework export updated for architecture submodule.

The export statement correctly points to the new architecture submodule, ensuring iOS consumers have access to the refactored base components.


91-91: API dependency updated for architecture submodule.

The dependency change maintains the same public API surface for iOS while correctly referencing the restructured module hierarchy.

shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/domain/TimeStampUseCase.kt (2)

11-12: Dependency injection and inheritance properly configured.

The @Factory annotation correctly registers the use case with Koin DI, and the class properly extends FlowUseCase<Unit, Instant> to leverage the new execution framework.


16-16: Use consistent delay intervals.

The 1000ms delay creates exactly 1-second intervals between emissions, which is appropriate for a timestamp service.

androidApp/build.gradle.kts (1)

18-20: LGTM! Correctly migrated to the new compiler options DSL

The migration from kotlinOptions to compilerOptions with JvmTarget is implemented correctly and aligns with Kotlin 2.0+ best practices.

shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/UseCaseConfig.kt (1)

63-68: LGTM! Well-structured builder pattern with sensible defaults

The builder implementation correctly provides defaults for all optional parameters and maintains a clean API. The default error handler appropriately rethrows exceptions to ensure errors are not silently swallowed.

shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/firstScreen/FirstComponent.kt (2)

54-64: Good use of lifecycle-aware execution for timestamp updates

The implementation correctly leverages the new executeWithLifecycle API to manage the timestamp stream based on the component's lifecycle, preventing memory leaks and unnecessary computations.


68-78: Improved error handling in syncData()

The refactored implementation with explicit onSuccess and onError handlers provides better separation of concerns and clearer error messaging.

shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/tools/LifecycleExecution.kt (1)

88-101: Well-implemented error handling with proper distinction of cancellation

The error handling correctly distinguishes between cancellation (which is expected during lifecycle transitions) and actual errors, with appropriate logging via UseCaseErrorHandler.

shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/UseCase.kt (2)

29-39: LGTM! Clean API for Unit-based use cases.

The context receiver pattern provides a clean extension for Unit-based use cases, eliminating the need to pass Unit explicitly. This improves the developer experience.


41-83: Consider extracting config building to reduce duplication.

The configuration building logic appears in multiple places across UseCase and FlowUseCase. Consider extracting this pattern to a shared utility function.

Additionally, the static analysis warning about swallowing CancellationException on line 75 is a false positive - the exception is being intentionally ignored as documented, which is the correct behavior for cooperative cancellation.

shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/FlowUseCase.kt (4)

34-81: LGTM! Well-structured flow execution with proper error handling.

The implementation correctly handles cancellation, error logging, and lifecycle callbacks. The error handling pattern with onCompletion and catch ensures all errors are properly captured and logged.


79-79: Empty catch block is intentional and correctly documented.

The empty catch block is appropriately handled in onCompletion, preventing duplicate error handling. The comment clearly indicates this is intentional.


83-89: LGTM! Consistent convenience methods for Unit-based flow use cases.

These extension functions provide a clean API for Unit-based flow use cases, maintaining consistency with the UseCase pattern.


91-139: Consider null safety implications of mapNotNull.

The use of mapNotNull on line 121 will silently filter out null values returned by onMap. This could lead to unexpected behavior if the mapper function intentionally returns null for certain values.

Consider documenting this behavior or using a different approach:

-        .mapNotNull { flowUseCaseConfig.onMap?.invoke(it) }
+        .map { flowUseCaseConfig.onMap?.invoke(it) }
+        .filterNotNull()

Or if null filtering is intentional, add documentation:

/**
 * ...
 * Note: Values mapped to null by onMap will be filtered out and not passed to onNext.
 * ...
 */

Comment on lines +4 to +6
org.gradle.caching=false
org.gradle.configureondemand=true
org.gradle.configuration-cache=true
org.gradle.configuration-cache=false
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify the necessity of disabling Gradle optimizations.

Disabling both caching and configuration cache will significantly slow down build times. These optimizations are generally safe and recommended for most projects.

Please confirm that these optimizations need to be disabled for this specific refactor. If they're causing issues with the context parameters feature or the architectural changes, consider:

  1. Identifying the specific incompatible plugins or configurations
  2. Using selective exclusions rather than global disabling
  3. Re-enabling these optimizations once the refactor stabilizes

🏁 Script executed:

#!/bin/bash
# Search for any build script configurations that might be incompatible with Gradle caching
echo "Searching for potential caching incompatibilities..."

# Look for custom tasks that might not be cache-friendly
ast-grep --pattern 'task $_ {'

# Check for configurations that might conflict with configuration cache
rg -A 3 -B 3 "configuration.*cache|cache.*configuration"

# Look for any explicit cache-related configurations
rg -A 5 "gradle\.cache|caching|configuration-cache"

Length of output: 1151


Re-enable Gradle caching and configuration-cache by default

Our search didn’t surface any custom tasks or configurations that are known to break Gradle’s caching features—only the flags in gradle.properties themselves. Disabling both org.gradle.caching and org.gradle.configuration-cache will noticeably slow down every build. Unless you’ve encountered concrete cache-related errors in this refactor, please restore the defaults and, if needed, opt out specific tasks or plugins instead of disabling globally.

• File: gradle.properties
Lines 4–6:

- org.gradle.caching=false
+ org.gradle.caching=true

- org.gradle.configuration-cache=false
+ org.gradle.configuration-cache=true

If you’ve run into build failures or stale outputs tied to configuration-cache or build caching, please document those errors and exclude only the incompatible tasks/plugins. Otherwise, let’s turn these optimizations back on to keep builds fast.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
org.gradle.caching=false
org.gradle.configureondemand=true
org.gradle.configuration-cache=true
org.gradle.configuration-cache=false
org.gradle.caching=true
org.gradle.configureondemand=true
org.gradle.configuration-cache=true
🤖 Prompt for AI Agents
In gradle.properties around lines 4–6, the file has globally disabled Gradle
build cache and configuration-cache which slows builds; revert these to defaults
by setting org.gradle.caching=true and org.gradle.configuration-cache=true (or
remove those properties so Gradle uses defaults), keep
org.gradle.configureondemand as desired, and if you previously encountered
cache-related failures, document the errors and opt-out only the incompatible
tasks/plugins instead of disabling caching globally.

Comment on lines +3 to +4
kotlin = "2.2.0"
ksp = "2.2.0-2.0.2" # Must be compatible with: `kotlin`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify compatibility between Kotlin 2.2.0 and KSP 2.2.0-2.0.2.

The major Kotlin version upgrade may introduce breaking changes or require adjustments in existing code.


🌐 Web query:

Are there any known compatibility issues or breaking changes when upgrading from Kotlin 2.1.10 to 2.2.0, especially regarding context parameters feature?

💡 Result:

Short answer: Yes — there are a few compatibility changes you should be aware of when moving from Kotlin 2.1.10 → 2.2.0, and the biggest user-facing change around the context feature is that context receivers are being replaced by the new context parameters preview. You can keep compiling, but you’ll likely need small code/build/IDE changes if your code uses context receivers (or other now-stricter language checks). (blog.jetbrains.com, kotlinlang.org)

What matters most (context parameters)

  • Context parameters replace the older context receivers feature in 2.2.0 (context parameters are available as a preview/preview‑flag feature). If you used context receivers in 2.1.x, you should migrate to context parameters; the compiler/IDE will help. (Blog: April 2025; docs: 2.2.0). (blog.jetbrains.com, kotlinlang.org)
  • To enable context parameters you must use the compiler flag -Xcontext-parameters (or add it to the kotlin.compilerOptions.freeCompilerArgs). Specifying both -Xcontext-receivers and -Xcontext-parameters at once is an error. (kotlinlang.org)
  • Migration notes / limitations in 2.2.0: callable references to functions with context parameters are NOT supported in 2.2.0 (you’ll need to replace such callable references with lambdas or other workarounds). Context receivers in classes have no direct automatic counterpart and require manual refactoring. IntelliJ (2025.1+) provides automatic quick‑fixes/inspections to help migrate file/module/project scopes. (blog.jetbrains.com, kotlinlang.org)
  • Some restrictions still apply to context parameters in 2.2.0 (constructors can’t declare them; properties with context parameters can’t have backing fields, initializers, or delegation). Check the docs for current list. (kotlinlang.org)

Other important breaking/deprecation items in Kotlin 2.2.0 you should check when upgrading from 2.1.10

  • kotlinOptions { } in Gradle: the old kotlinOptions block’s deprecation level is raised — use the new compilerOptions { } block (migrate build scripts). Failing to do this will become an error. (kotlinlang.org)
  • Compatibility/strictness changes (examples from the 2.2 compatibility guide you should scan for in your codebase):
    • field-targeted annotations on annotation properties are now an error.
    • reified type parameters are forbidden on type aliases (was warned previously; now an error).
    • stricter visibility / type accessibility checks (e.g., exposing private types via non-private inline functions now errors).
    • non-local returns in lambdas used as a parameter default value are now an error.
    • behavioral change: type checks on inline value classes for Number/Comparable were corrected.
      These are source/behavioral changes that can break code that previously compiled. See the compatibility guide and scan your code. (kotlinlang.org)

Practical checklist to upgrade safely

  1. Update Kotlin version to 2.2.0 in your build. Update your IDE plugin / IntelliJ to a version that bundles the 2.2.0 Kotlin plugin (IntelliJ 2025.1+ recommended for migration tooling). (kotlinlang.org, blog.jetbrains.com)
  2. Replace kotlinOptions { } with compilerOptions { } in Gradle builds. (kotlinlang.org)
  3. If you used context receivers:
    • Decide to migrate to context parameters and add -Xcontext-parameters to compiler free args; remove -Xcontext-receivers.
    • Use the IDE quick‑fix (Analyze / Run inspection / Migrate from context receivers to context parameters) to do bulk migrations where possible.
    • Manually fix cases the tool can’t convert (callable references, context receivers in classes). (blog.jetbrains.com, kotlinlang.org)
  4. Run a full clean build and test suite; fix compilation errors reported by the stricter checks (visibility/type accessibility, typealias/reified, annotation targets, etc.). Review the 2.2 compatibility guide and search your repo for patterns called out there. (kotlinlang.org)
  5. If you relied on experimental behavior that changed between 2.1.10 and 2.2.0, add small targeted tests and run them — especially for inline/inline-class behaviors and reflection/IR plugins. (kotlinlang.org)

If you want, I can:

  • scan your build.gradle(.kts) and a few Kotlin source files (paste them) for the specific patterns that will break (context receivers usage, kotlinOptions, callable references to context functions, reified in typealias, etc.) and give a line‑by‑line migration plan; or
  • produce the exact Gradle snippets to convert kotlinOptions → compilerOptions and to enable the -Xcontext-parameters flag.

Which would you prefer?

Citations:


Ensure Kotlin 2.2.0 and KSP 2.2.0-2.0.2 Compatibility

Kotlin 2.2.0 introduces several source‐breaking changes—including the switch from context receivers to context parameters (preview), stricter language checks, and a new Gradle DSL for compiler flags—that you must address when upgrading. Please update your build and code as follows:

• File ​gradle/libs.versions.toml (lines 3–4)

kotlin = "2.2.0"
ksp    = "2.2.0-2.0.2"  # aligns with Kotlin 2.2.0

• Build script changes

  • Replace the deprecated kotlinOptions { … } block with compilerOptions { … }.
  • Add -Xcontext-parameters to compilerOptions.freeCompilerArgs and remove any -Xcontext-receivers flags.

• Context-parameters migration

  • Use the IDE’s “Migrate from context receivers” inspection to bulk-convert to context parameters.
  • Manually refactor any remaining context-receiver usage (especially in classes) and replace callable references to context-parameter functions with lambdas.

• Global compatibility checks

  • Scan for now-errors such as reified type parameters on type aliases, field-targeted annotation property issues, stricter visibility/type accessibility violations, and non-local returns in default lambdas.
  • Review the Kotlin 2.2 compatibility guide: https://kotlinlang.org/docs/compatibility-guide-22.html

• Verify changes

  1. Update your IDE to IntelliJ 2025.1+ (bundles Kotlin 2.2.0 tooling).
  2. Run a clean build and full test suite; resolve any compilation or runtime failures.
  3. Add targeted tests if you relied on inline-class or IR plugin behaviors that changed.
🤖 Prompt for AI Agents
In gradle/libs.versions.toml around lines 3–4, you've pinned kotlin = "2.2.0"
and ksp = "2.2.0-2.0.2" but need to update build and code to be compatible with
Kotlin 2.2.0: replace any kotlinOptions { … } blocks in Gradle scripts with
compilerOptions { … }, add "-Xcontext-parameters" to
compilerOptions.freeCompilerArgs and remove any "-Xcontext-receivers" flags, run
the IDE “Migrate from context receivers” refactor and manually convert remaining
context-receiver usages (replace callable references to context-parameter
functions with lambdas where necessary), scan and fix new compilation errors
noted in the compatibility guide (reified type params on type aliases,
field-targeted annotation property issues, stricter visibility/type
accessibility, non-local returns in default lambdas, etc.), update to IntelliJ
2025.1+ Kotlin tooling, then run a clean build and full test suite and add tests
for any changed inline/IR plugin behavior.

Comment on lines +82 to +90
fun build(): FlowUseCaseConfig<T, M> = FlowUseCaseConfig(
onStart ?: { },
onNext ?: { },
onError ?: { throw it },
onComplete ?: { },
disposePrevious,
onMap,
)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify the default error handling strategy.

The default error handling { throw it } will rethrow exceptions, which might not be appropriate in all contexts, especially for UI-facing flow operations.

Please confirm this is the intended behavior. Consider whether a more graceful default error handler (e.g., logging and continuing) would be more appropriate for flow use cases.


🏁 Script executed:

#!/bin/bash
# Search for existing error handling patterns in the codebase
echo "Searching for existing error handling patterns in use cases..."

# Look for existing error handling in use case implementations
ast-grep --pattern 'onError = { $$ }'

# Check for any custom error handlers
rg -A 5 "onError|errorHandler|handleError"

# Look for existing flow error handling patterns
ast-grep --pattern 'catch { $$ }'

Length of output: 34060


Adjust default error handling in FlowUseCaseConfig
The current default onError lambda simply rethrows the exception, which will crash the coroutine if the caller doesn’t explicitly override it. We recommend changing the default to integrate with the global error logger (or at least log and swallow) so unhandled errors don’t bring down the host scope.

Locations to update:

  • shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/FlowUseCaseConfig.kt
    Lines 82–90 (the build() method)

Suggested diff snippet:

         fun build(): FlowUseCaseConfig<T, M> = FlowUseCaseConfig(
-            onStart ?: { },
-            onNext ?: { },
-            onError ?: { throw it },
+            onStart     ?: { },
+            onNext      ?: { },
+            onError     ?: { error ->
+                UseCaseErrorHandler.globalOnErrorLogger(error)
+                // swallow or rethrow depending on host requirements:
+                // throw error
+            },
            onComplete  ?: { },
            disposePrevious,
            onMap,
        )

– Replace throw it with a call to UseCaseErrorHandler.globalOnErrorLogger(error) (and only rethrow if desired) so flows without a custom onError don’t unconditionally crash.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun build(): FlowUseCaseConfig<T, M> = FlowUseCaseConfig(
onStart ?: { },
onNext ?: { },
onError ?: { throw it },
onComplete ?: { },
disposePrevious,
onMap,
)
}
fun build(): FlowUseCaseConfig<T, M> = FlowUseCaseConfig(
onStart ?: { },
onNext ?: { },
onError ?: { error ->
UseCaseErrorHandler.globalOnErrorLogger(error)
// swallow or rethrow depending on host requirements:
// throw error
},
onComplete ?: { },
disposePrevious,
onMap,
)
}
🤖 Prompt for AI Agents
In
shared/arkitekt-cr-usecases/src/commonMain/kotlin/app/futured/arkitekt/crusecases/scope/FlowUseCaseConfig.kt
around lines 82–90, the default onError currently rethrows the exception which
will crash the coroutine; change the default to call the global error logger
instead of throwing (e.g. invoke UseCaseErrorHandler.globalOnErrorLogger(error)
and do not rethrow) so unhandled errors are logged and swallowed by default, and
ensure the necessary import or reference to UseCaseErrorHandler is added; keep
the existing API so callers can still provide a custom onError that rethrows if
desired.

Comment on lines +13 to +18
override fun build(args: Unit): Flow<Instant> = flow {
for (i in 0..1000) {
emit(Clock.System.now())
delay(1000)
}
}
Copy link

@coderabbitai coderabbitai bot Aug 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider the finite emission count and resource implications.

The implementation emits timestamps for exactly 1001 iterations (0..1000) with 1-second delays, creating a flow that runs for approximately 16.7 minutes before completing. Consider whether this finite duration aligns with the intended use case behavior.

For a timestamp service that should run indefinitely, consider using an infinite loop:

-    override fun build(args: Unit): Flow<Instant> = flow {
-        for (i in 0..1000) {
-            emit(Clock.System.now())
-            delay(1000)
-        }
-    }
+    override fun build(args: Unit): Flow<Instant> = flow {
+        while (true) {
+            emit(Clock.System.now())
+            delay(1000)
+        }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override fun build(args: Unit): Flow<Instant> = flow {
for (i in 0..1000) {
emit(Clock.System.now())
delay(1000)
}
}
override fun build(args: Unit): Flow<Instant> = flow {
while (true) {
emit(Clock.System.now())
delay(1000)
}
}
🤖 Prompt for AI Agents
In
shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/domain/TimeStampUseCase.kt
around lines 13–18, the flow currently emits a finite 0..1000 sequence (~16.7
minutes) which likely isn't intended for a continuous timestamp service; replace
the fixed for-loop with an infinite, cancellable loop (e.g., while(true) or
while(coroutineContext.isActive)) that emits Clock.System.now() then delays
1_000ms, and make the loop cooperative with coroutine cancellation by checking
isActive or calling ensureActive/yield so the flow can be cancelled cleanly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid point

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matejsemancik Thank you for confirming! The infinite loop approach will ensure the timestamp flow continues until explicitly cancelled, which better suits a continuous monitoring use case. 👍

@matejsemancik
Copy link
Member

This one would be very easy approve from me, but we need to figure how to re-enable Gradle cache

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai can you generate docstrings for this file?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matejsemancik, here are the docstrings for the LifecycleExecution.kt file:

package app.futured.kmptemplate.feature.tools

import app.futured.arkitekt.crusecases.FlowUseCase
import app.futured.arkitekt.crusecases.error.UseCaseErrorHandler
import app.futured.arkitekt.crusecases.scope.CoroutineScopeOwner
import app.futured.arkitekt.crusecases.scope.FlowUseCaseConfig
import com.arkivanov.essenty.lifecycle.Lifecycle
import com.arkivanov.essenty.lifecycle.LifecycleOwner
import com.arkivanov.essenty.lifecycle.subscribe
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlin.coroutines.cancellation.CancellationException

/**
 * Executes a [FlowUseCase] with lifecycle awareness, gating flow emissions based on the component's lifecycle state.
 *
 * The flow will only emit values when the lifecycle has reached or exceeded the specified [minActiveState].
 * When the lifecycle falls below this state, the flow emissions are paused. This ensures that emissions
 * only occur when the component is in an appropriate state to handle them.
 *
 * ## Context Receivers
 * - [CoroutineScopeOwner]: Provides the coroutine scope and dispatcher for executing the flow
 * - [LifecycleOwner]: Provides the lifecycle to observe for state changes
 *
 * ## Lifecycle Behavior
 * The flow will start emitting when the lifecycle reaches [minActiveState] and pause when it drops below:
 * - [Lifecycle.State.CREATED]: Active between onCreate and onDestroy
 * - [Lifecycle.State.STARTED]: Active between onStart and onStop
 * - [Lifecycle.State.RESUMED]: Active between onResume and onPause
 *
 * ## Error Handling
 * Errors are logged via [UseCaseErrorHandler.globalOnErrorLogger] and forwarded to the [FlowUseCaseConfig.onError] callback.
 * Cancellation exceptions are ignored.
 *
 * @param ARGS The type of arguments required by the use case
 * @param T The type of values emitted by the flow
 * @param args Arguments to pass to the use case's build function
 * @param minActiveState The minimum lifecycle state required for the flow to emit values. Defaults to [Lifecycle.State.RESUMED]
 * @param config Configuration builder for lifecycle callbacks (onStart, onNext, onError, onComplete) and disposal behavior
 *
 * @see FlowUseCaseConfig
 * @see CoroutineScopeOwner
 * @see LifecycleOwner
 */
@OptIn(ExperimentalCoroutinesApi::class)
context(coroutineScopeOwner: CoroutineScopeOwner, componentContext: LifecycleOwner)
fun <ARGS, T : Any?> FlowUseCase<ARGS, T>.executeWithLifecycle(
    args: ARGS,
    minActiveState: Lifecycle.State = Lifecycle.State.RESUMED,
    config: FlowUseCaseConfig.Builder<T, T>.() -> Unit,
) = executeWithLifecycleInternal(args = args, minActiveState = minActiveState, config = config)

/**
 * Executes a [FlowUseCase] that requires no arguments with lifecycle awareness.
 *
 * This is a convenience overload for use cases where [ARGS] is [Unit]. The flow will only emit values
 * when the lifecycle has reached or exceeded the specified [minActiveState].
 *
 * ## Context Receivers
 * - [CoroutineScopeOwner]: Provides the coroutine scope and dispatcher for executing the flow
 * - [LifecycleOwner]: Provides the lifecycle to observe for state changes
 *
 * ## Lifecycle Behavior
 * The flow will start emitting when the lifecycle reaches [minActiveState] and pause when it drops below:
 * - [Lifecycle.State.CREATED]: Active between onCreate and onDestroy
 * - [Lifecycle.State.STARTED]: Active between onStart and onStop
 * - [Lifecycle.State.RESUMED]: Active between onResume and onPause
 *
 * ## Error Handling
 * Errors are logged via [UseCaseErrorHandler.globalOnErrorLogger] and forwarded to the [FlowUseCaseConfig.onError] callback.
 * Cancellation exceptions are ignored.
 *
 * @param T The type of values emitted by the flow
 * @param minActiveState The minimum lifecycle state required for the flow to emit values. Defaults to [Lifecycle.State.RESUMED]
 * @param config Configuration builder for lifecycle callbacks (onStart, onNext, onError, onComplete) and disposal behavior
 *
 * @see FlowUseCaseConfig
 * @see CoroutineScopeOwner
 * @see LifecycleOwner
 */
@OptIn(ExperimentalCoroutinesApi::class)
context(coroutineScopeOwner: CoroutineScopeOwner, componentContext: LifecycleOwner)
fun <T : Any?> FlowUseCase<Unit, T>.executeWithLifecycle(
    minActiveState: Lifecycle.State = Lifecycle.State.RESUMED,
    config: FlowUseCaseConfig.Builder<T, T>.() -> Unit,
) = executeWithLifecycleInternal(args = Unit, minActiveState = minActiveState, config = config)

/**
 * Internal implementation of lifecycle-aware flow execution.
 *
 * Creates a state flow that tracks whether execution should be enabled based on the current lifecycle state.
 * Uses [flatMapLatest] to switch between the target flow and an empty flow based on the lifecycle state.
 * Subscribes to lifecycle callbacks to update the execution state when the lifecycle transitions in and out
 * of the specified [minActiveState].
 *
 * The flow is executed on the worker dispatcher provided by [CoroutineScopeOwner] and launched in the
 * [CoroutineScopeOwner.viewModelScope].
 *
 * @param ARGS The type of arguments required by the use case
 * @param T The type of values emitted by the flow
 * @param args Arguments to pass to the use case's build function
 * @param minActiveState The minimum lifecycle state required for the flow to emit values
 * @param config Configuration builder for lifecycle callbacks and disposal behavior
 */
@OptIn(ExperimentalCoroutinesApi::class)
context(coroutineScopeOwner: CoroutineScopeOwner, componentContext: LifecycleOwner)
private fun <ARGS, T : Any?> FlowUseCase<ARGS, T>.executeWithLifecycleInternal(
    args: ARGS,
    minActiveState: Lifecycle.State = Lifecycle.State.RESUMED,
    config: FlowUseCaseConfig.Builder<T, T>.() -> Unit,
) {
    val flowUseCaseConfig = FlowUseCaseConfig.Builder<T, T>().run {
        config.invoke(this)
        return@run build()
    }

    if (flowUseCaseConfig.disposePrevious) {
        job?.cancel()
    }

    val enableExecution = MutableStateFlow(false)

    componentContext.lifecycle.subscribe(
        onCreate = {
            if (minActiveState == Lifecycle.State.CREATED) {
                enableExecution.value = true
            }
        },
        onDestroy = {
            if (minActiveState == Lifecycle.State.CREATED) {
                enableExecution.value = false
            }
        },
        onStart = {
            if (minActiveState == Lifecycle.State.STARTED) {
                enableExecution.value = true
            }
        },
        onStop = {
            if (minActiveState == Lifecycle.State.STARTED) {
                enableExecution.value = false
            }
        },
        onResume = {
            if (minActiveState == Lifecycle.State.RESUMED) {
                enableExecution.value = true
            }
        },
        onPause = {
            if (minActiveState == Lifecycle.State.RESUMED) {
                enableExecution.value = false
            }
        },
    )
    val targetFlow = build(args)

    job = enableExecution
        .flatMapLatest {
            if (it) targetFlow else emptyFlow()
        }
        .flowOn(coroutineScopeOwner.getWorkerDispatcher())
        .onStart { flowUseCaseConfig.onStart() }
        .onEach { flowUseCaseConfig.onNext(it) }
        .onCompletion { error ->
            when {
                error is CancellationException -> {
                    // ignore this exception
                }

                error != null -> {
                    UseCaseErrorHandler.globalOnErrorLogger(error)
                    flowUseCaseConfig.onError(error)
                }

                else -> flowUseCaseConfig.onComplete()
            }
        }
        .catch { /* handled in onCompletion */ }
        .launchIn(coroutineScopeOwner.viewModelScope)
}

The docstrings include:

  • Comprehensive descriptions of each function's purpose and behavior
  • Documentation of context receivers and their roles
  • Detailed lifecycle behavior explanation for each state
  • Error handling documentation
  • Parameter descriptions
  • Cross-references to related types
  • Clear examples of when emissions are active for each lifecycle state

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to see this unit tested + perhaps moved to arkitekt-decompose subproject

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants