@Observable support for Jetpack Compose UI#790
Conversation
| implementation(libs.androidx.compose.material3) | ||
| implementation(libs.androidx.compose.ui) | ||
| implementation(libs.androidx.compose.ui.tooling.preview) | ||
| debugImplementation(libs.androidx.compose.ui.tooling) |
There was a problem hiding this comment.
AFAICS all these are Apache 2 so we'll be fine, but I need to double check this
| } | ||
|
|
||
| /// Whether this is a `@ObservationIgnored` type. | ||
| var isObservationIgnored: Bool { |
There was a problem hiding this comment.
| var isObservationIgnored: Bool { | |
| var isSwiftObservationIgnored: Bool { |
for consistency?
| @@ -0,0 +1,6 @@ | |||
| package org.swift.swiftkit.compose | |||
There was a problem hiding this comment.
missing license headers everywhere in kt files
| @JavaMethod | ||
| public static func equals(environment: UnsafeMutablePointer<JNIEnv?>!, lhsPointer: Int64, lhsTypePointer: Int64, rhsPointer: Int64, rhsTypePointer: Int64) -> Bool { | ||
| // Fallback for non-equatable types (such as classes) | ||
| let isEquatableByIdentity = lhsPointer == rhsPointer && lhsTypePointer == rhsTypePointer |
There was a problem hiding this comment.
I think we landed this on main?
This should be isEqualByIdentity if not; it's not the equate-ability we're checking
There was a problem hiding this comment.
yup ,needs a rebase
| @@ -0,0 +1,6 @@ | |||
| package org.swift.swiftkit.compose | |||
|
|
|||
| interface SwiftObservable { | |||
| * the bridge calls [invalidate] to bump the counter and trigger | ||
| * recomposition of any composable that read it. | ||
| */ | ||
| class TrackingToken { |
There was a problem hiding this comment.
I am not quite sure; when initially prototyping Observation tokens were a more proliferated thing - however it ended up being something that is used less since it is better to have the Observable type manage it's own tracking via the registrar. This does two things, firstly it keeps the developer from needing to hold a bag of tokens around for no real use other than lifetime management, but secondly it avoids a centralized location for tracking because if you shove all observations of all things into one shared center it ends up not scaling easily to large numbers of observers and splitting it out into smaller chunks of observations is more efficient for the lookups for both add and removal of observation.
There was a problem hiding this comment.
That all being said; I am a firm believer in that bridges should favor the semantics they are bridging into not forcing the systems from where they are bridging from into some foreign system.
ktoso
left a comment
There was a problem hiding this comment.
I'll continue later, need to read up about how all this works :)
| * the bridge calls [invalidate] to bump the counter and trigger | ||
| * recomposition of any composable that read it. | ||
| */ | ||
| class TrackingToken { |
There was a problem hiding this comment.
I am not quite sure; when initially prototyping Observation tokens were a more proliferated thing - however it ended up being something that is used less since it is better to have the Observable type manage it's own tracking via the registrar. This does two things, firstly it keeps the developer from needing to hold a bag of tokens around for no real use other than lifetime management, but secondly it avoids a centralized location for tracking because if you shove all observations of all things into one shared center it ends up not scaling easily to large numbers of observers and splitting it out into smaller chunks of observations is more efficient for the lookups for both add and removal of observation.
| * the bridge calls [invalidate] to bump the counter and trigger | ||
| * recomposition of any composable that read it. | ||
| */ | ||
| class TrackingToken { |
There was a problem hiding this comment.
That all being said; I am a firm believer in that bridges should favor the semantics they are bridging into not forcing the systems from where they are bridging from into some foreign system.
| } | ||
|
|
||
| private func printObservableSubscriptionType(_ printer: inout CodePrinter, _ type: ImportedNominalType) { | ||
| printer.print("@MainActor") |
There was a problem hiding this comment.
Even though Observation is commonly used (almost exclusively) on the main actor the design does not limit it to just the main actor. Does this somehow restrict the observations to only be main actor? Is that a restriction of the bridged system or is this an imposed restriction to simplify the swift interface?
If it is the latter then I would say we should consider figuring out how to relax that constraint.
There was a problem hiding this comment.
I designed the current bridging for specifically Jetpack Compose use. So yeah as it is now, it's intended for UI.
This also affects some of the assumptions for the safety of the variables like the token count of observers, as they are main thread bound. So, it avoids the need for any additional synchronization.
I am not sure if we gain much from making it non UI-specific atm, but it could be an improvement in the future?
| private long observerPointer; | ||
|
|
||
| /** The count of how many are observing changes to this object */ | ||
| private int observerCount; |
There was a problem hiding this comment.
im not sure this can really be done safely - why is this needed? is it some sort of attempt at meshing GC?
There was a problem hiding this comment.
The safety of this relies on the main thread assumption.
But yes, this is used to setup the subscription (Observations sequence) when the first observer wants to listen. There could be multiple views/places that want to observe, so we only store one subscription and we dispose of it when no one is observing anymore.
So yeah, it is kind of GC.
Introduces support for backing JetCompose UI with
@ObservableSwift objects.This works by using a version-based strategy on the Kotlin side, that effectively increments a Compose integer state everything a change is detected. Changes are detected through the
Observationsasync sequence from Swift. Subscribing to values happens in the generated swift-java getter for any variables on the observable model. This logic is in theTrackingTokentype.This introduces a new Kotlin library
SwiftKitComposewhich exposes the necessary Kotlin helpers to integrate with the Compose library.I also added (with Claude's help) a sample app that demonstrates the functionality in various use cases.
We also ignore non-observable stuff like:
staticpropertieslet@ObservationIgnoredCode generated
CounterModel.swift:
Use-site:
Generated Java code
And then we trigger the "listen" inside the getter:
Generated Swift code