Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions ADOPTION.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Adoption Playbook

The objective is not a short-lived star count. It is a measurable path from a
repository visit to a correct integration that teams keep after evaluating it.

## First adopters

Offer hands-on help integrating one non-critical screen. Ask each team for:
Expand All @@ -16,6 +19,17 @@ Suggested message:
> with the integration and lifecycle review. In return, I’d value candid API
> feedback; public attribution is entirely optional.

Choose pilot screens that:

- Are non-critical and can be removed without a migration.
- Have one clearly owned ViewModel and one or two observable fields.
- Already use SKIE or KMP-NativeCoroutines successfully.
- Have tests or a deterministic interaction that exposes lifecycle mistakes.

For each pilot, record the time required to install, configure, render the first
value, and verify cancellation. Turn repeated confusion into documentation or a
compile-time diagnostic.

## Where to ask

- Kotlin Slack `#multiplatform`
Expand All @@ -39,16 +53,32 @@ For every consenting adopter, record:

Never imply production use without confirmation from the application owner.

## Success measures
Before publishing a name, logo, quotation, benchmark, or architecture detail,
obtain explicit written permission for that specific material. An anonymous
technical case study is a valid alternative.

## Adoption funnel

Review monthly:

- README visits to package-resolution or clone activity
- Swift Package Index builds
- Unique repository visitors and their referral sources
- Clones and package-resolution activity
- Unique cloners and dependent repositories
- Discussions that reach a working integration
- Evaluations that produce a rendered screen
- Evaluations that remain after one release cycle
- Time to first maintainer response
- Retention of known adopters after one release cycle

Stars are useful discovery signals, but successful integrations and retained
users are the primary measures.

## Evidence worth publishing

- A reproducible before/after adapter diff.
- Notification and body-evaluation counts for a real screen.
- Collector counts with parent, child, and environment wrappers.
- Cancellation behavior after navigation and model rebinding.
- Toolchain and framework versions for every benchmark.
- Limitations discovered during adoption and how they were resolved.

Never publish a synthetic benchmark as proof of application-level performance.
45 changes: 45 additions & 0 deletions CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Code of Conduct

## Our commitment

KMPObservableBridge is committed to a welcoming, respectful, and technically
constructive community for people of every background and experience level.

Participation includes repository activity, issues, pull requests,
discussions, reviews, community events, and project-related communication.

## Expected behavior

- Discuss ideas and evidence rather than attacking people.
- Assume good intent while remaining precise about technical tradeoffs.
- Welcome questions and explain unfamiliar Swift, SwiftUI, Kotlin, and
interoperability concepts without condescension.
- Give actionable review feedback with enough context to help.
- Disclose conflicts of interest when recommending competing or affiliated
projects.
- Respect privacy and never publish private integration details without
permission.

## Unacceptable behavior

- Harassment, discrimination, threats, or personal insults.
- Sexualized language or unwanted attention.
- Trolling, deliberate disruption, or repeated bad-faith argument.
- Publishing another person's private information.
- Pressuring adopters to provide public endorsements.
- Manipulating benchmarks, adoption claims, or project comparisons.

## Enforcement

Report conduct concerns privately to the repository owner. Do not open a public
issue containing personal or sensitive information. Reports will be reviewed
promptly and handled with appropriate confidentiality.

Maintainers may edit or remove contributions, comments, or other participation
that violates this policy. Repeated or serious violations may result in a
temporary or permanent ban from project spaces.

## Scope

This policy applies whenever someone represents the project or participates in
an official KMPObservableBridge space.
46 changes: 39 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,47 @@
# Contributing

1. Open an issue for public API or behavior changes.
2. Keep the core free of Kotlin framework and third-party dependencies.
3. Add tests for lifecycle, actor isolation, cancellation, and deallocation.
4. Run:
Thank you for helping make Kotlin state feel native in SwiftUI. Bug reports,
API critiques, documentation fixes, integration fixtures, and performance
evidence are all useful.

## Before changing code

1. Search the [issues](https://github.com/sonmbol/KMPObservableBridge/issues)
and [discussions](https://github.com/sonmbol/KMPObservableBridge/discussions).
2. Open an issue before changing public API or observable behavior.
3. Keep `KMPObservableBridge` independent of Kotlin frameworks and third-party
runtimes. Exporter-specific behavior belongs in its integration target.
4. Preserve existing consumer source compatibility unless the issue explicitly
targets a major release.

Good first contributions include clearer diagnostics, DocC examples, example
accessibility, and additional integration fixtures. Changes to invalidation,
ownership, cancellation, macros, or actor isolation require focused tests.

## Validation

Run:

```shell
swift test
swift test -Xswiftc -strict-concurrency=complete -Xswiftc -warnings-as-errors
./Scripts/check-api.sh
./Scripts/check-package-manifests.sh
git diff --check
```

Public APIs must include documentation and migration notes. Performance claims
must include a reproducible benchmark.
When changing the example integration, also build DailyPulse using the procedure
in [`Examples/DailyPulse/iosApp/README.md`](Examples/DailyPulse/iosApp/README.md).

## Pull requests

- Keep the change focused and explain its rendering and lifetime effects.
- Add regression tests for lifecycle, actor isolation, cancellation, rebinding,
dependency selection, and deallocation where applicable.
- Document public APIs and include migration notes for behavior changes.
- Include toolchain, configuration, commands, and raw measurements with every
performance claim.
- Never include private application code, credentials, or adopter information
without permission.

All participation is governed by the
[`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md).
178 changes: 93 additions & 85 deletions Docs/Choosing-SKIE-or-KMP-NativeCoroutines.md
Original file line number Diff line number Diff line change
@@ -1,135 +1,143 @@
# Choosing SKIE or KMP-NativeCoroutines for SwiftUI

SKIE and KMP-NativeCoroutines both make Kotlin coroutine APIs easier to consume
from Swift, but they offer different integration shapes. KMPObservableBridge
supports both without forcing either dependency into the Swift package.
SKIE and KMP-NativeCoroutines expose Kotlin coroutine APIs to Swift in different
shapes. KMPObservableBridge isolates their APIs in separate package products so
an application can choose its existing export strategy without leaking the
other integration into source or binaries.

## Quick comparison

| Question | SKIE | KMP-NativeCoroutines |
| --- | --- | --- |
| Typical Swift shape | `StateFlow` as an async sequence with value access | Generated `NativeFlow` callback or async wrappers |
| Shortest bridge syntax | Automatic lazy discovery | `KMPNativeObservable` canonical flow |
| Runtime getter interception | Used by automatic mode | No |
| Explicit typed path available | Yes | Yes |
| Best fit | Teams already standardizing on SKIE | Teams already generating NativeCoroutines APIs |

Neither choice is universally better. Prefer the tool already present in the
| Typical Swift shape | Typed `AsyncSequence` with synchronous StateFlow value access | Generated `NativeFlow` callback or async wrapper |
| Bridge product | `KMPObservableBridgeSKIE` | `KMPObservableBridgeNative` |
| Configuration | Explicit macro fields or wrapper key paths | Explicit NativeFlow key path |
| Field-level dependency metadata | Yes, for macro-configured StateFlows | Global unless a custom keyed adapter provides metadata |
| Runtime reflection | None | None |
| Best fit | Teams already using SKIE | Teams already generating NativeCoroutines APIs |

Neither exporter is universally better. Prefer the one already validated in the
application unless a measured limitation justifies migration.

## SKIE: shortest SwiftUI declaration
## SKIE

For compatible generated frameworks:
Declare current-value interoperability once in the application module:

```swift
@KMPStateObject
private var profile = ProfileViewModel()
```

KMPObservableBridge lazily inspects naturally accessed Kotlin getters. When a
returned value conforms to that framework's exported StateFlow protocol, the
bridge starts the matching SKIE iterator.
import shared
import KMPObservableBridgeSKIE

This removes per-property registration, but it relies on generated runtime
details. Debug builds log unavailable compatibility, successful getter
discovery, and incompatible iterator method shapes.
extension SkieSwiftStateFlow: @retroactive KMPValueProperty {}
```

Use an explicit key path when deterministic observation is more important than
the shortest declaration:
Then declare the StateFlows that form a ViewModel’s observation plan:

```swift
@KMPStateObject(
wrappedValue: ProfileViewModel(),
state: \.state
@KMPObservable(
ProfileViewModel.self,
fields: \.profileState, \.permissionsState
)
private var profile
extension ProfileViewModel: @retroactive KMPStaticallyObservable {}
```

## KMP-NativeCoroutines: explicit generated contract
The macro expands these compiler-checked key paths into the static observation
plan. Swift macros cannot inspect members of an imported Kotlin type, so fields
are intentionally explicit. The bridge does not use Objective-C discovery,
getter interception, or generated application source.

A model can expose one NativeFlow representing its canonical invalidation
stream:
Own or observe the model normally:

```swift
extension ProfileViewModel: KMPNativeObservable {
public var kmpObservationFlow: KMPNativeFlow<
ProfileState,
Error,
KotlinUnit
> {
profileStateFlow
}
}
@KMPStateObject
private var profile = ProfileViewModel()

Text($profile.profileState.title)
```

Then the same wrapper syntax selects NativeFlow before considering SKIE:
Use an explicit wrapper adapter when a conformance should remain local to one
use site:

```swift
@KMPStateObject
@KMPStateObject(state: \.profileState)
private var profile = ProfileViewModel()
```

This route performs no Objective-C method interception or SKIE iterator lookup.
Each emission enters the common main-actor, failure-policy, cancellation, and
coalescing store.
## KMP-NativeCoroutines

Link `KMPObservableBridgeNative` and observe the generated NativeFlow explicitly:

```swift
import shared
import KMPObservableBridgeNative

@KMPStateObject(
state: \.kmpObservationFlow,
updatePolicy: .immediate
)
private var example = BridgeExampleViewModel()
```

The NativeFlow is the notification and cancellation source. Render the
separately exported current property:

```swift
Text(example.nativeMessageValue)
```

The exact exported generic types depend on the generated framework. Follow the
generated NativeFlow signature rather than copying placeholder names from an
example.
The exact generated property and generic types depend on the Kotlin declaration
and NativeCoroutines version. Use the generated Swift interface as the contract.

## Performance considerations
## Performance and rendering behavior

For SKIE automatic discovery:
For macro-configured SKIE:

- compatible and incompatible runtime descriptors are cached per model class;
- eligible getters perform a small return-value protocol check;
- repeated reads of the same StateFlow identity are deduplicated;
- one active iterator is kept per discovered getter and model.
- One static hub shares the model’s configured collectors across wrappers.
- Consecutive equal StateFlow values are suppressed.
- Projected field reads can register field-level dependencies on
Observation-capable platforms.
- Direct model reads retain global invalidation semantics.

For NativeFlow:

- there is no getter interception;
- the primary cost is the Kotlin-to-Swift callback for each emission;
- cancellation propagates through the returned NativeFlow cancellation handle.
- The Kotlin-to-Swift callback crosses to `MainActor` once.
- Cancellation propagates through the generated cancellation handle.
- The adapter invalidates globally unless it supplies a known dependency key.

For both routes, SwiftUI rendering usually costs more than bridge dispatch.
The default `.coalesced` policy batches bursts arriving in one main-actor turn.
Use `.immediate` only when every emission must produce a distinct invalidation.
For both routes, `.coalesced` unions changes in one main-actor turn.
`.immediate` preserves every accepted emission when that semantic is required.
Neither route duplicates Kotlin business state in Swift.

## Selection guide

Choose automatic SKIE when:
Choose SKIE when:

- the application already uses and tests SKIE;
- minimum declaration ceremony is important;
- the Kotlin/SKIE version combination is pinned in CI;
- lazy observation matches the view's access pattern.

Choose explicit SKIE key paths when:

- observation topology must be obvious in code;
- only selected flows should invalidate a screen;
- maximum compile-time guidance is preferred.
- synchronous StateFlow value access is useful to rendering;
- explicit macro fields fit the project’s feature organization;
- field-aware Observation is valuable.

Choose KMP-NativeCoroutines when:

- it is already the application's coroutine export strategy;
- a canonical NativeFlow can represent screen invalidation;
- avoiding runtime getter interception is a priority.
- it is already the applications coroutine export strategy;
- a generated NativeFlow provides the desired invalidation contract;
- state is exposed separately through a current-value property.

Choose a custom adapter when the application already has a stable callback,
Combine, or platform-specific observation contract.
Choose callbacks, Combine, or a custom adapter when the application already has
a stable platform-specific contract that is easier to validate than migrating
exporters.

## Validate the decision
## Validate either decision

Whichever route you choose, test:
Test:

1. initial state replay;
2. a later Kotlin emission;
3. background delivery to the main actor;
4. model identity replacement;
5. cancellation when the view/store disappears;
6. ViewModel disposal only by its owner.
1. Initial value rendering.
2. A later Kotlin emission.
3. Foreign-thread delivery to `MainActor`.
4. Model identity replacement and stale-emission suppression.
5. Cancellation when the wrapper’s identity ends.
6. Disposal by owners only.
7. Multiple wrappers and their expected collector count.
8. Equal emissions and body-evaluation counts.

The included DailyPulse application builds a generated Kotlin framework with
both SKIE and KMP-NativeCoroutines examples.
The DailyPulse example builds a real Kotlin framework and demonstrates both
integration products.
Loading
Loading