Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building a macOS screen saver with Kotlin (Andr...

Building a macOS screen saver with Kotlin (Android Makers 2025)

Join me for a tale of Kotlin success and macOS failures, as we explore just what it takes to build a screen saver in Kotlin. We'll see how easy it is to call native platform APIs from Kotlin code, what challenges the development of such an unusual application presents, and how macOS is very much not our friend on this whole journey.

More details: https://zsmb.co/appearances/android-makers-2025-day1/

Márton Braun

April 09, 2025
Tweet

More Decks by Márton Braun

Other Decks in Programming

Transcript

  1. class KotlinLogosView : ScreenSaverView { override init?(frame: NSRect, isPreview: Bool)

    { super.init(frame: frame, isPreview: isPreview) animationTimeInterval = 1 / 60.0 } }
  2. class KotlinLogosView : ScreenSaverView { override init?(frame: NSRect, isPreview: Bool)

    { super.init(frame: frame, isPreview: isPreview) animationTimeInterval = 1 / 60.0 } required init?(coder decoder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
  3. class KotlinLogosView : ScreenSaverView { override init?(frame: NSRect, isPreview: Bool)

    { super.init(frame: frame, isPreview: isPreview) animationTimeInterval = 1 / 60.0 } required init?(coder decoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func animateOneFrame() { super.animateOneFrame() } }
  4. class KotlinLogosView : ScreenSaverView { override init?(frame: NSRect, isPreview: Bool)

    { super.init(frame: frame, isPreview: isPreview) animationTimeInterval = 1 / 60.0 } required init?(coder decoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func animateOneFrame() { super.animateOneFrame() } override var hasConfigureSheet: Bool { return false } override var configureSheet: NSWindow? { return nil } }
  5. import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework plugins { kotlin("multiplatform") version "2.1.10" } kotlin {

    val xcf = XCFramework("KotlinLogo") val targets = listOf(macosX64(), macosArm64()) targets.forEach { target ! target.binaries.framework { binaryOption("bundleId", "co.zsmb.KotlinLogos") baseName = "KotlinLogo" isStatic = true xcf.add(this) } } }
  6. let bundle: NSBundle = Bundle(for: type(of: self)) val bundle: NSBundle

    = NSBundle.bundleWithIdentifier("co.zsmb.KotlinLogos") import platform.Foundation.NSBundle
  7. let bundle: NSBundle = Bundle(for: type(of: self)) let image: NSImage

    = bundle.image(forResource: "kotlin0") val bundle: NSBundle = NSBundle.bundleWithIdentifier("co.zsmb.KotlinLogos") import platform.Foundation.NSBundle
  8. let bundle: NSBundle = Bundle(for: type(of: self)) let image: NSImage

    = bundle.image(forResource: "kotlin0") val bundle: NSBundle = NSBundle.bundleWithIdentifier("co.zsmb.KotlinLogos") val image: NSImage = bundle.imageForResource("kotlin0") import platform.AppKit.NSImage import platform.AppKit.imageForResource import platform.Foundation.NSBundle
  9. import platform.AppKit.NSImageView import platform.Foundation.NSMakeRect val imageView = NSImageView().apply { image

    = logoImage frame = NSMakeRect(x = 300.0, y = 300.0, w = 200.0, h = 200.0) } screenSaverView.addSubview(imageView)
  10. import platform.AppKit.NSImageView import platform.Foundation.NSMakeRect val imageView = NSImageView().apply { image

    = logoImage frame = NSMakeRect(x = 300.0, y = 300.0, w = 200.0, h = 200.0) } screenSaverView.addSubview(imageView)
  11. import platform.AppKit.NSImageView import platform.Foundation.NSMakeRect val imageView = NSImageView().apply { image

    = logoImage frame = NSMakeRect(x = 300.0, y = 300.0, w = 200.0, h = 200.0) } screenSaverView.addSubview(imageView) val frame = NSMakeRect(x = newX, y = newY, w = 200.0, h = 200.0) imageView.frame = frame
  12. Delete your old output files before running a new build

    Build your project Go to Activity Monitor and force quit all legacyScreenSaver processes Go to System Settings, choose any other screen saver Delete your custom screen saver from System Settings Quit System Settings Install your new screen saver 1 2 3 4 5 6 7
  13. Build your project Go to Activity Monitor and force quit

    all legacyScreenSaver processes Go to System Settings, choose any other screen saver Delete your custom screen saver from System Settings Quit System Settings Install your new screen saver 2 3 4 5 6 7
  14. import platform.Foundation.NSUserDefaults class LongUserDefaultDelegate : ReadWriteProperty<Any?, Long> { private val

    userDefaults = NSUserDefaults() override fun getValue(!!", property: KProperty!!#): Long { return userDefaults.objectForKey(property.name) as? Long !$ 0L } override fun setValue(!!", property: KProperty!!#, value: Long) { userDefaults.setInteger(value, forKey = property.name) } } object Preferences { var LOGO_SET by LongUserDefaultDelegate(0) var LOGO_SIZE by LongUserDefaultDelegate(200) var LOGO_COUNT by LongUserDefaultDelegate(1) var SPEED by LongUserDefaultDelegate(10) }
  15. import platform.Foundation.NSUserDefaults class LongUserDefaultDelegate : ReadWriteProperty<Any?, Long> { private val

    userDefaults = NSUserDefaults() override fun getValue(!!", property: KProperty!!#): Long { return userDefaults.objectForKey(property.name) as? Long !$ 0L } override fun setValue(!!", property: KProperty!!#, value: Long) { userDefaults.setInteger(value, forKey = property.name) } } object Preferences { var LOGO_SET by LongUserDefaultDelegate(0) var LOGO_SIZE by LongUserDefaultDelegate(200) var LOGO_COUNT by LongUserDefaultDelegate(1) var SPEED by LongUserDefaultDelegate(10) }
  16. import platform.Foundation.NSUserDefaults class LongUserDefaultDelegate : ReadWriteProperty<Any?, Long> { private val

    userDefaults = NSUserDefaults() override fun getValue(!!", property: KProperty!!#): Long { return userDefaults.objectForKey(property.name) as? Long !$ 0L } override fun setValue(!!", property: KProperty!!#, value: Long) { userDefaults.setInteger(value, forKey = property.name) } } object Preferences { var LOGO_SET by LongUserDefaultDelegate(0) var LOGO_SIZE by LongUserDefaultDelegate(200) var LOGO_COUNT by LongUserDefaultDelegate(1) var SPEED by LongUserDefaultDelegate(10) }
  17. import platform.AppKit.NSModalResponseOK import platform.AppKit.NSOpenPanel import platform.Foundation.NSURL val openPanel = NSOpenPanel().apply

    { allowsMultipleSelection = false canChooseDirectories = true canCreateDirectories = false canChooseFiles = false } openPanel.beginWithCompletionHandler { response ! if (response !" NSModalResponseOK !# openPanel.URLs.isNotEmpty()) { val url = openPanel.URLs.first() as NSURL !!$ } }
  18. import platform.Foundation.NSDirectoryEnumerationSkipsHiddenFiles import platform.Foundation.NSDirectoryEnumerationSkipsSubdirectoryDes import platform.Foundation.NSFileManager import platform.Foundation.NSURL val enum

    = NSFileManager.defaultManager.enumeratorAtURL( url = NSURL(string = folderUrl), includingPropertiesForKeys = null, options = NSDirectoryEnumerationSkipsHiddenFiles or NSDirectoryEnumerationSkipsSubdirectoryDescendants, errorHandler = null ) var next = enum.nextObject() while (next !% null) { !" !!# next = enum.nextObject() }
  19. import platform.Foundation.NSDirectoryEnumerationSkipsHiddenFiles import platform.Foundation.NSDirectoryEnumerationSkipsSubdirectoryDes import platform.Foundation.NSFileManager import platform.Foundation.NSURL val enum

    = NSFileManager.defaultManager.enumeratorAtURL( url = NSURL(string = folderUrl), includingPropertiesForKeys = null, options = NSDirectoryEnumerationSkipsHiddenFiles or NSDirectoryEnumerationSkipsSubdirectoryDescendants, errorHandler = null ) var next = enum.nextObject() while (next !% null) { !" !!# next = enum.nextObject() }
  20. legacyScreenSaver ScreenSaverView ScreenSaverView ScreenSaverView ScreenSaverView ScreenSaverView ScreenSaverView ScreenSaverView ScreenSaverView ScreenSaverView

    ScreenSaverView ScreenSaverView ScreenSaverView ScreenSaverView ScreenSaverView ScreenSaverVie ScreenSaverVi ScreenSaverV ScreenSaver ScreenSav ScreenSa ScreenS Screen
  21. class KotlinLogosView: ScreenSaverView { @objc func willStop(_ aNotification: Notification) {

    if (!isPreview) { NSApplication.shared.terminate(nil) } } }
  22. Do not try building a macOS screen saver Don’t be

    afraid to use interop with Kotlin/Native
  23. Do not try building a macOS screen saver Don’t be

    afraid to use interop with Kotlin/Native Put some Kotlin logos on your machine!
  24. Márton… You're giving a talk about Compose Hot Reload in

    this same room tomorrow at 12:10PM. Why isn’t this screensaver using Compose?!
  25. Do not try building a macOS screen saver Don’t be

    afraid to use interop with Kotlin/Native Put some Kotlin logos on your machine! Maybe build a cool screen saver with Compose?
  26. Office Hours: All Things Kotlin Salle 2.04 13:45 tomorrow Márton

    Braun Pamela Hill Marcin Moskała Office Hours: KMP/CMP Salle 2.04 14:45 today Márton Braun Pamela Hill John O’Reilly