Shared KMP module: Difference between revisions

From Aquarium-Control
Jump to navigation Jump to search
 
(One intermediate revision by the same user not shown)
Line 35: Line 35:
         implementation(project(":shared"))
         implementation(project(":shared"))
     }
     }
</source>
 
   
=== 2. Dependency Injection ===
=== 2. Dependency Injection ===
The Android app utilizes <code>kotlin-inject</code> to build the final dependency graph. The <code>AppComponent</code> inside the <code>app</code> module
The Android app utilizes <code>kotlin-inject</code> to build the final dependency graph. The <code>AppComponent</code> inside the <code>app</code> module
inherits from the <code>SharedModule</code> interface defined in the KMP code.
inherits from the <code>SharedModule</code> interface defined in the KMP code.


<source>
     @Component
     @Component
     @ApplicationScope
     @ApplicationScope

Latest revision as of 17:21, 6 June 2026

Aquarium Control Shared Module (KMP)

The Shared KMP Module is the core of the mobile Aquarium Control application, implemented using Kotlin (KMP). It encapsulates the entire data layer, business logic, and network communication, allowing that the code to be written once and shared natively across both the Android and iOS applications.

Architecture

The shared module follows a clean architecture approach, serving as the Data and Domain layer for the MVVM (Model-View-ViewModel) pattern used by the client

  • Domain Layer: Contains the core entities (e.g., FeedProfile, HeatingSetVals, SummaryData) and repository

interfaces. These define the strict contracts that the applications interact with.

  • Data Layer: Contains the concrete implementations of the repositories.
    • Networking: Utilizes Ktor for all REST API communication with the aquarium controller hardware.
    • Persistence: Uses Room (KMP compatible) for complex local relational data storage (such as history logs and controller profiles) and DataStore for lightweight preferences (such as the default server profile ID).
  • Reactive State: Data is exposed to the presentation layers exclusively via Kotlin Coroutines and StateFlow / SharedFlow.

This ensures the UI is always reacting to a single source of truth.

  • Dependency Injection: Managed cross-platform using kotlin-inject, providing compile-time safe dependency

graphs for both Android (AppComponent) and iOS (IosComponent).

Functional Content

The module is organized into distinct feature domains:

  • Summary: Real-time polling of all sensors (temperature, pH, conductivity, tank levels) and actuator states.
  • Controller Profile & Session: Manages the connection details (IP, port, credentials) for multiple aquariums and tracks the currently active session.
  • Schedule: Global management of time-based rules for all actuators (Start/Stop times, limitations).
  • TimeData (Telemetry): Paged and real-time visualization of sensor telemetry over time.
  • Heating: Management of switch-on/switch-off thresholds and historical heating statistics/runtime.
  • Ventilation: Management of surface ventilation thresholds and safety sanity checks.
  • Feed: Complex multi-phase profile creation (pausing skimmers/pumps) and schedule execution, alongside historical event logging.
  • Balling (Dosing): Configuration of dosing pump volumes and historical logging of dosed fluids.
  • Refill: Manual control triggers (Start, Stop, Reset) and historical logging of ATO (Auto Top-Off) events.

Android Integration

On Android, the shared module is consumed as a standard Gradle project dependency.

1. Gradle Configuration

In the `app` module and feature modules, the shared module is included in the build.gradle.kts: <source lang="kotlin">

   dependencies {
       implementation(project(":shared"))
   }

2. Dependency Injection

The Android app utilizes kotlin-inject to build the final dependency graph. The AppComponent inside the app module inherits from the SharedModule interface defined in the KMP code.

   @Component
   @ApplicationScope
   abstract class AppComponent(
       @get:Provides val context: Context
   ) : SharedModule { ... }

3. Initialization

The graph is initialized once in the custom Android Application class: <source lang="kotlin">

   class AquariumControlApplication : Application() {
       lateinit var appComponent: AppComponent
       override fun onCreate() {
           super.onCreate()
           appComponent = AppComponent::class.create(this)
       }
   }

Android ViewModels then inject and observe the StateFlow properties from the shared repositories directly.

iOS Integration

On iOS, the shared module is compiled into a native Apple Framework using the Kotlin Multiplatform Gradle plugin and embedded into the Xcode project.

1. Xcode Build Phase

The Xcode project uses a "Run Script" build phase to automatically compile the Kotlin code into an iOS framework (e.g., embedAndSignAppleFrameworkForXcode) during the standard Xcode build process.

2. Swift Dependency Injection Bridge

Because Swift cannot directly evaluate Kotlin's @Inject annotations, the shared module defines an IosComponent and a factory function to instantiate the graph natively. In Swift, a Singleton wrapper (DependencyInjection.shared) is created to hold this component and expose the repositories to the SwiftUI views: <source lang="swift">

   import SharedAquariumControl
   class DependencyInjection {
       static let shared = DependencyInjection()
       private let iosComponent: IosComponent
        
       // Repositories exposed to Swift
       let summaryRepository: SummaryRepository
       let heatingSetValsRepository: HeatingSetValsRepository
       // ...
        
       private init() {
           self.iosComponent = IosComponentFactoryKt.createIosComponent()
           self.summaryRepository = iosComponent.summaryRepository
           self.heatingSetValsRepository = iosComponent.heatingSetValsRepository
           // ...
       }
   }

3. Flow Collection in Swift

Kotlin's StateFlow is not automatically bridged to Swift's AsyncSequence or @Published properties. To consume the reactive streams, the iOS app uses a custom FlowCollector wrapper class. Swift UI DataProviders initialize a Task to collect these flows and update their local @Observable properties: <source lang="swift">

   Task {
       self.repository.heatingSetValsFlow.collect(
           collector: FlowCollector<DataFetchResult<HeatingSetVals>> { [weak self] result in
               self?.handleResult(result)
           }
       )
  }

This ensures the SwiftUI interface remains fully reactive to background network syncs and database changes occurring within the KMP layer.