Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ BuildLogic/out/
**/build/
lib/

# Android local SDK location + native build intermediates
local.properties
**/.cxx/

# Ignore JVM crash logs
**/*.log

Expand Down
41 changes: 41 additions & 0 deletions Samples/SwiftKitAndroidComposeSample/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// swift-tools-version: 6.2
// The swift-tools-version declares the minimum version of Swift required to build this package.

import CompilerPluginSupport
import PackageDescription

let package = Package(
name: "SwiftKitAndroidComposeSample",
platforms: [
.macOS(.v15)
],
products: [
.library(
name: "MySwiftLibrary",
type: .dynamic,
targets: ["MySwiftLibrary"]
),
],
dependencies: [
.package(name: "swift-java", path: "../../"),
.package(url: "https://github.com/swift-android-sdk/swift-android-native.git", from: "2.0.0")
],
targets: [
.target(
name: "MySwiftLibrary",
dependencies: [
.product(name: "SwiftJava", package: "swift-java"),
.product(name: "AndroidLooper", package: "swift-android-native", condition: .when(platforms: [.android]))
],
exclude: [
"swift-java.config"
],
swiftSettings: [
.swiftLanguageMode(.v5)
],
plugins: [
.plugin(name: "JExtractSwiftPlugin", package: "swift-java")
]
),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift.org project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Observation

@Observable
public class CounterModel {
public var count: Int64 = 0

public init() {}

public func increment() {
count += 1
}

public func reset() {
count = 0
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift.org project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Observation

/// A view model with **many observable states** of different primitive types.
/// Each property drives a different Compose control (text, slider, switch,
/// stepper). Only the property that actually changed should recompose the
/// widgets that read it.
@Observable
public class DashboardModel {
public var title: String = "My Dashboard"
public var counter: Int64 = 0
public var level: Int32 = 1
public var temperature: Double = 20.5
public var progress: Double = 0.25
public var isEnabled: Bool = true
public var isFavorite: Bool = false

public init() {}

public func increment() { counter += 1 }
public func decrement() { counter -= 1 }
public func levelUp() { level += 1 }
public func warmer() { temperature += 0.5 }
public func cooler() { temperature -= 0.5 }
public func toggleEnabled() { isEnabled.toggle() }
public func toggleFavorite() { isFavorite.toggle() }

public func reset() {
counter = 0
level = 1
temperature = 20.5
progress = 0.25
isEnabled = true
isFavorite = false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift.org project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Observation

/// Exercises the things that should **NOT** participate in observation:
///
/// - `static let` / `static var`: type-level, never tracked per-instance.
/// - `let`: immutable stored property; `@Observable` does not track it.
/// - `@ObservationIgnored var`: explicitly opted out of tracking.
///
/// Only `visibleCounter` is a normal observed `var`. The UI should recompose
/// when `visibleCounter` changes, but NOT when `hiddenCounter` changes — even
/// though `hiddenCounter` really is mutating under the hood (which we can prove
/// by copying it into `visibleCounter`, forcing a legitimate refresh).
@Observable
public class EdgeCasesModel {
/// Type-level constant — must not generate any per-instance observation.
public static let appName: String = "SwiftJava Observable Sample"

/// Type-level mutable — also must not be observed per instance.
public static var launchCount: Int64 = 0

/// Immutable stored property — never changes, so must not be observed.
public let createdLabel: String = "Created once at init"

/// Explicitly excluded from observation tracking.
@ObservationIgnored public var hiddenCounter: Int64 = 0

/// The only genuinely observed property.
public var visibleCounter: Int64 = 0

public init() {
EdgeCasesModel.launchCount += 1
}

/// Mutates an ignored property — the UI should NOT react to this.
public func bumpHidden() { hiddenCounter += 1 }

/// Mutates an observed property — the UI SHOULD react to this.
public func bumpVisible() { visibleCounter += 1 }

/// Pulls the (silently changed) hidden value into the observed one, so a
/// real notification fires and the UI finally reflects the hidden mutations.
public func revealHidden() { visibleCounter = hiddenCounter }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift.org project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Foundation
import Observation

/// Demonstrates **two-way bindings**: each `var` is read by a Compose
/// `TextField` and written back from its `onValueChange`. `fullName` is a
/// read-only computed property that should recompute (and recompose) whenever
/// `firstName` or `lastName` change.
@Observable
public class FormModel {
public var firstName: String = ""
public var lastName: String = ""
public var email: String = ""
public var bio: String = ""

public init() {}

public var fullName: String {
let joined = "\(firstName) \(lastName)"
return joined.trimmingCharacters(in: .whitespaces)
}

public var isComplete: Bool {
!firstName.isEmpty && !lastName.isEmpty && !email.isEmpty
}

public func clear() {
firstName = ""
lastName = ""
email = ""
bio = ""
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import AndroidLooper

public func setupAndroidMainLooper() {
AndroidMainActor.setupMainLooper()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift.org project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Observation

/// A child `@Observable` object, nested inside `ProfileModel`.
@Observable
public class AddressModel {
public var street: String = "1 Infinite Loop"
public var city: String = "Cupertino"
public var country: String = "USA"

public init() {}

public var oneLine: String {
"\(street), \(city), \(country)"
}
}

/// Demonstrates **nested observable objects**. `ProfileModel` owns an
/// `AddressModel`. Mutating a field on the nested object (e.g. `address.city`)
/// should be observable to a UI that is tracking the nested object.
///
/// Note: the parent only directly observes its own `name` and the `address`
/// *reference*. To react to changes *inside* the nested object, the Compose
/// layer observes the child as well (see `NestedScreen`).
@Observable
public class ProfileModel {
public var name: String = "Jane Appleseed"
public var address: AddressModel = AddressModel()

public init() {}

public func moveToLondon() {
address.street = "10 Downing Street"
address.city = "London"
address.country = "UK"
}

/// Replaces the whole nested object (reference change on the parent).
public func replaceAddress() {
address = AddressModel()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift.org project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Observation

/// Demonstrates **arrays**. Mutating the `items` array (append/remove) is an
/// observable change, so a Compose list that reads `items` should recompose.
@Observable
public class TodoListModel {
public var items: [String] = ["Buy milk", "Walk the dog", "Learn swift-java"]

public init() {}

/// Read-only computed property derived from the array.
public var count: Int64 {
Int64(items.count)
}

public var isEmpty: Bool {
items.isEmpty
}

public func add(_ item: String) {
items.append(item)
}

public func removeLast() {
if !items.isEmpty {
items.removeLast()
}
}

public func removeAll() {
items.removeAll()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"javaPackage": "com.example.swift.compose",
"mode": "jni",
"observableComposeBridging": true,
"memoryManagementMode": "allowGlobalAutomatic"
}
Loading