
Execute code once on first access with pluggable persistence (in-memory, preferences, custom storage). Runner pattern enables auto-disable, reset, and a simple three-method API for one-time flags.
A lightweight Kotlin Multiplatform library that helps you execute code once on first access, with flexible state persistence options. Use it in your Kotlin Multiplatform projects or natively in Android, iOS, JVM, or JavaScript applications. Now includes CounterFireAndForget for executing code a fixed number of times!
FireAndForget is a simple yet powerful utility for managing one-time operations in your applications. Whether you're building a Kotlin Multiplatform project or a native Android/iOS/JVM/JS app, FireAndForget provides a consistent API for one-time executions. It's perfect for scenarios like:
CounterFireAndForget)CounterFireAndForget
Add the dependency to your commonMain source set:
kotlin {
sourceSets {
commonMain.dependencies {
// Core library - required
implementation("com.alorma.fireandforget:core:$version")
// Optional: multiplatform-settings implementation
implementation("com.alorma.fireandforget:multiplatform-settings:$version")
}
}
}Add the dependency to your app's build.gradle.kts:
dependencies {
// Core library - required
implementation("com.alorma.fireandforget:core:$version")
// Optional: multiplatform-settings implementation
implementation("com.alorma.fireandforget:multiplatform-settings:$version")
}The library can be consumed as a Kotlin/Native framework in your iOS project. Add it to your shared Kotlin module and export it to iOS.
dependencies {
implementation("com.alorma.fireandforget:core:$version")
implementation("com.alorma.fireandforget:multiplatform-settings:$version")
}Check the latest version on Maven Central.
FireAndForget uses a Runner Pattern that separates the flag logic from state persistence:
FireAndForget (Abstract Class)
FireAndForgetRunner implementation for state persistencename to identify its stateCounterFireAndForget (Abstract Class, extends FireAndForget)
counter parameter for the number of allowed executionsisEnabled() callFireAndForgetRunner (Abstract Class)
isEnabled(): Check if the flag should executedisable(): Mark the flag as executedreset(): Reset the flag to allow re-executionCounterFireAndForget:
isEnabledCounter(): Check and decrement countergetCounter(): Retrieve current counter valuesetCounter(): Store counter valueresetCounter(): Reset counter to initial valueThis pattern allows you to choose or create your own state persistence strategy.
First, create a concrete class that extends FireAndForget:
class OnboardingFlag(
runner: FireAndForgetRunner,
) : FireAndForget(
fireAndForgetRunner = runner,
name = "user_onboarding",
defaultValue = true // Default: enabled (will execute)
)The library provides a ready-to-use implementation using russhwolf/multiplatform-settings:
import com.alorma.fireandforget.multiplatform.settings.SettingsFireAndForgetRunner
import com.russhwolf.settings.Settings
val settings = Settings()
val runner = SettingsFireAndForgetRunner(settings)
val onboardingFlag = OnboardingFlag(runner)This persists state across app restarts using platform-specific storage:
For temporary state that doesn't need to persist:
class InMemoryRunner : FireAndForgetRunner() {
private val map = mutableMapOf<String, Boolean>()
private val counterMap = mutableMapOf<String, Int>()
override fun checkEnabled(fireAndForget: FireAndForget): Boolean {
return map[fireAndForget.name] ?: fireAndForget.defaultValue
}
override fun disable(fireAndForget: FireAndForget) {
map[fireAndForget.name] = false
}
override fun reset(fireAndForget: FireAndForget) {
map.remove(fireAndForget.name)
}
override fun getCounter(counterFireAndForget: CounterFireAndForget): Int? {
return counterMap[counterFireAndForget.name]
}
override fun setCounter(counterFireAndForget: CounterFireAndForget, value: Int) {
counterMap[counterFireAndForget.name] = value
}
}Implement FireAndForgetRunner with your preferred storage solution (Room, DataStore, SQLDelight, etc.):
class DataStoreRunner(
private val dataStore: DataStore<Preferences>
) : FireAndForgetRunner() {
override fun checkEnabled(fireAndForget: FireAndForget): Boolean {
// Your DataStore implementation
}
override fun disable(fireAndForget: FireAndForget) {
// Your DataStore implementation
}
override fun reset(fireAndForget: FireAndForget) {
// Your DataStore implementation
}
override fun getCounter(counterFireAndForget: CounterFireAndForget): Int? {
// Your DataStore implementation for counter
}
override fun setCounter(counterFireAndForget: CounterFireAndForget, value: Int) {
// Your DataStore implementation for counter
}
}fun showAppContent() {
val runner = SettingsFireAndForgetRunner(Settings())
val onboarding = OnboardingFlag(runner)
if (onboarding.isEnabled()) {
// This will only show once
showOnboardingScreen(
onComplete = {
onboarding.disable() // Mark as completed
}
)
} else {
showMainScreen()
}
}abstract class FireAndForget(
val fireAndForgetRunner: FireAndForgetRunner,
val name: String,
val defaultValue: Boolean = true,
val autoDisable: Boolean = false,
)fireAndForgetRunner: The runner implementation that handles state persistencename: Unique identifier for this flag (used as storage key)defaultValue: Initial state (default: true = enabled)autoDisable: When true, automatically disables the flag on first call to isEnabled() (default: false)isEnabled(): Boolean - Returns true if the code should executedisable() - Marks the flag as executed (disables it)reset() - Resets the flag back to defaultValue (allows re-execution)abstract class CounterFireAndForget(
fireAndForgetRunner: FireAndForgetRunner,
name: String,
val counter: Int,
) : FireAndForget(...)fireAndForgetRunner: The runner implementation that handles state persistencename: Unique identifier for this flag (used as storage key)counter: Number of times isEnabled() will return true before returning false
isEnabled(): Boolean - Returns true if counter > 0, decrements counter on each callreset() - Resets the counter back to initial valueval feature = CounterFireAndForget(runner, "feature", counter = 3)
feature.isEnabled() // Call 1: true (counter: 3 β 2)
feature.isEnabled() // Call 2: true (counter: 2 β 1)
feature.isEnabled() // Call 3: true (counter: 1 β 0)
feature.isEnabled() // Call 4+: false (counter: 0)
feature.reset() // Resets counter to 3abstract class FireAndForgetRunner {
fun isEnabled(fireAndForget: FireAndForget): Boolean
protected abstract fun checkEnabled(fireAndForget: FireAndForget): Boolean
abstract fun disable(fireAndForget: FireAndForget)
abstract fun reset(fireAndForget: FireAndForget)
// Counter support for CounterFireAndForget
fun isEnabledCounter(counterFireAndForget: CounterFireAndForget): Boolean
protected abstract fun getCounter(counterFireAndForget: CounterFireAndForget): Int?
protected abstract fun setCounter(counterFireAndForget: CounterFireAndForget, value: Int)
fun resetCounter(counterFireAndForget: CounterFireAndForget)
}Implementation Note: When creating a custom runner, you must override checkEnabled() instead of isEnabled(). The isEnabled() method is final and handles the autoDisable logic internally, ensuring it cannot be bypassed by runner implementations. You must also implement getCounter() and setCounter() to support CounterFireAndForget.
class WelcomeMessage(runner: FireAndForgetRunner) : FireAndForget(
fireAndForgetRunner = runner,
name = "welcome_message"
)
fun showHomeScreen() {
val runner = SettingsFireAndForgetRunner(Settings())
val welcomeFlag = WelcomeMessage(runner)
if (welcomeFlag.isEnabled()) {
showWelcomeDialog(
onDismiss = { welcomeFlag.disable() }
)
}
}class NewFeatureAnnouncement(runner: FireAndForgetRunner) : FireAndForget(
fireAndForgetRunner = runner,
name = "feature_announcement_v2"
)
fun showMainScreen() {
val runner = SettingsFireAndForgetRunner(Settings())
val announcement = NewFeatureAnnouncement(runner)
if (announcement.isEnabled()) {
// Show announcement
displayMessage("Check out our new feature!")
announcement.disable()
}
}class AppTutorial(runner: FireAndForgetRunner) : FireAndForget(
fireAndForgetRunner = runner,
name = "app_tutorial"
)
fun handleRestartTutorial() {
val runner = SettingsFireAndForgetRunner(Settings())
val tutorial = AppTutorial(runner)
// Allow tutorial to run again
tutorial.reset()
navigateToTutorial()
}fun showApp() {
val runner = SettingsFireAndForgetRunner(Settings())
// Multiple flags can share the same runner
val onboarding = OnboardingFlag(runner)
val tutorial = TutorialFlag(runner)
val featureAnnouncement = FeatureAnnouncementFlag(runner)
when {
onboarding.isEnabled() -> showOnboardingScreen { onboarding.disable() }
tutorial.isEnabled() -> showTutorialScreen { tutorial.disable() }
else -> showMainScreen()
}
}class FirstRunSetup(runner: FireAndForgetRunner) : FireAndForget(
fireAndForgetRunner = runner,
name = "first_run_setup"
)
fun initializeApp() {
val runner = SettingsFireAndForgetRunner(Settings())
val setup = FirstRunSetup(runner)
if (setup.isEnabled()) {
// Perform first-run initialization
initializeDatabase()
downloadInitialData()
setup.disable()
}
}Use autoDisable = true to automatically disable the flag on first access without manually calling disable():
class QuickTip(runner: FireAndForgetRunner) : FireAndForget(
fireAndForgetRunner = runner,
name = "quick_tip",
autoDisable = true // Automatically disables after first isEnabled() call
)
fun showScreen() {
val runner = SettingsFireAndForgetRunner(Settings())
val quickTip = QuickTip(runner)
// First call: returns true and automatically disables
if (quickTip.isEnabled()) {
showTooltip("Here's a quick tip!")
// No need to call quickTip.disable()
}
// Subsequent calls: returns false
quickTip.isEnabled() // false
}This is perfect for fire-and-forget operations where you don't have a natural completion callback to call disable(). The flag automatically marks itself as executed when accessed for the first time.
Use CounterFireAndForget to execute code a specific number of times before disabling:
import com.alorma.fireandforget.CounterFireAndForget
class LimitedPromo(runner: FireAndForgetRunner, times: Int = 3) : CounterFireAndForget(
fireAndForgetRunner = runner,
name = "limited_promo",
counter = times // Will return true 3 times, then false
)
fun showScreen() {
val runner = SettingsFireAndForgetRunner(Settings())
val promo = LimitedPromo(runner, times = 3)
// First 3 calls: returns true and decrements counter
if (promo.isEnabled()) {
showPromoBanner("Special offer!")
}
// After 3 calls: returns false
// Counter state persists across app restarts
}isEnabled() calltrue while counter > 0false when counter reaches 0reset() restores counter to initial valuePerfect for:
To show a feature only AFTER it's been accessed N times (inverse logic), use !isEnabled():
class ShowAfterVisits(runner: FireAndForgetRunner, visits: Int = 3) : CounterFireAndForget(
fireAndForgetRunner = runner,
name = "show_after_visits",
counter = visits
)
fun showScreen() {
val runner = SettingsFireAndForgetRunner(Settings())
val showAfterVisits = ShowAfterVisits(runner, visits = 3)
// Track visits silently - returns true for first 3 visits, false after
val stillCounting = showAfterVisits.isEnabled()
// Show feature ONLY after counter reaches 0 (inverse logic)
if (!stillCounting) {
showAdvancedFeature("You've visited 3 times! Here's an advanced feature.")
}
// Visit 1: stillCounting = true β feature NOT shown
// Visit 2: stillCounting = true β feature NOT shown
// Visit 3: stillCounting = true β feature NOT shown
// Visit 4+: stillCounting = false β feature SHOWN
}Alternative: Track and Show Once After N Visits
class UnlockAfterUses(runner: FireAndForgetRunner, requiredUses: Int = 3) : CounterFireAndForget(
fireAndForgetRunner = runner,
name = "unlock_tracker",
counter = requiredUses
)
class FeatureUnlocked(runner: FireAndForgetRunner) : FireAndForget(
fireAndForgetRunner = runner,
name = "feature_unlocked",
autoDisable = true // Show once, then auto-disable
)
fun showScreen() {
val runner = SettingsFireAndForgetRunner(Settings())
val unlockTracker = UnlockAfterUses(runner, requiredUses = 3)
val featureUnlocked = FeatureUnlocked(runner)
// Track usage silently
val stillTracking = unlockTracker.isEnabled()
// Show unlock message once when counter reaches 0
if (!stillTracking && featureUnlocked.isEnabled()) {
showUnlockMessage("π Premium feature unlocked after 3 uses!")
}
}Perfect for:
This repository contains:
# Build core library
./gradlew :core:build
# Build multiplatform-settings runner
./gradlew :multiplatform-settings:build
# Build everything
./gradlew build# Android sample
./gradlew :samples:androidApp:assembleDebug
# Desktop sample
./gradlew :samples:desktopApp:run# Run all tests across all platforms
./gradlew allTests
# Run platform-specific tests
./gradlew jvmTest
./gradlew jsTest
./gradlew iosSimulatorArm64TestContributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.
A lightweight Kotlin Multiplatform library that helps you execute code once on first access, with flexible state persistence options. Use it in your Kotlin Multiplatform projects or natively in Android, iOS, JVM, or JavaScript applications. Now includes CounterFireAndForget for executing code a fixed number of times!
FireAndForget is a simple yet powerful utility for managing one-time operations in your applications. Whether you're building a Kotlin Multiplatform project or a native Android/iOS/JVM/JS app, FireAndForget provides a consistent API for one-time executions. It's perfect for scenarios like:
CounterFireAndForget)CounterFireAndForget
Add the dependency to your commonMain source set:
kotlin {
sourceSets {
commonMain.dependencies {
// Core library - required
implementation("com.alorma.fireandforget:core:$version")
// Optional: multiplatform-settings implementation
implementation("com.alorma.fireandforget:multiplatform-settings:$version")
}
}
}Add the dependency to your app's build.gradle.kts:
dependencies {
// Core library - required
implementation("com.alorma.fireandforget:core:$version")
// Optional: multiplatform-settings implementation
implementation("com.alorma.fireandforget:multiplatform-settings:$version")
}The library can be consumed as a Kotlin/Native framework in your iOS project. Add it to your shared Kotlin module and export it to iOS.
dependencies {
implementation("com.alorma.fireandforget:core:$version")
implementation("com.alorma.fireandforget:multiplatform-settings:$version")
}Check the latest version on Maven Central.
FireAndForget uses a Runner Pattern that separates the flag logic from state persistence:
FireAndForget (Abstract Class)
FireAndForgetRunner implementation for state persistencename to identify its stateCounterFireAndForget (Abstract Class, extends FireAndForget)
counter parameter for the number of allowed executionsisEnabled() callFireAndForgetRunner (Abstract Class)
isEnabled(): Check if the flag should executedisable(): Mark the flag as executedreset(): Reset the flag to allow re-executionCounterFireAndForget:
isEnabledCounter(): Check and decrement countergetCounter(): Retrieve current counter valuesetCounter(): Store counter valueresetCounter(): Reset counter to initial valueThis pattern allows you to choose or create your own state persistence strategy.
First, create a concrete class that extends FireAndForget:
class OnboardingFlag(
runner: FireAndForgetRunner,
) : FireAndForget(
fireAndForgetRunner = runner,
name = "user_onboarding",
defaultValue = true // Default: enabled (will execute)
)The library provides a ready-to-use implementation using russhwolf/multiplatform-settings:
import com.alorma.fireandforget.multiplatform.settings.SettingsFireAndForgetRunner
import com.russhwolf.settings.Settings
val settings = Settings()
val runner = SettingsFireAndForgetRunner(settings)
val onboardingFlag = OnboardingFlag(runner)This persists state across app restarts using platform-specific storage:
For temporary state that doesn't need to persist:
class InMemoryRunner : FireAndForgetRunner() {
private val map = mutableMapOf<String, Boolean>()
private val counterMap = mutableMapOf<String, Int>()
override fun checkEnabled(fireAndForget: FireAndForget): Boolean {
return map[fireAndForget.name] ?: fireAndForget.defaultValue
}
override fun disable(fireAndForget: FireAndForget) {
map[fireAndForget.name] = false
}
override fun reset(fireAndForget: FireAndForget) {
map.remove(fireAndForget.name)
}
override fun getCounter(counterFireAndForget: CounterFireAndForget): Int? {
return counterMap[counterFireAndForget.name]
}
override fun setCounter(counterFireAndForget: CounterFireAndForget, value: Int) {
counterMap[counterFireAndForget.name] = value
}
}Implement FireAndForgetRunner with your preferred storage solution (Room, DataStore, SQLDelight, etc.):
class DataStoreRunner(
private val dataStore: DataStore<Preferences>
) : FireAndForgetRunner() {
override fun checkEnabled(fireAndForget: FireAndForget): Boolean {
// Your DataStore implementation
}
override fun disable(fireAndForget: FireAndForget) {
// Your DataStore implementation
}
override fun reset(fireAndForget: FireAndForget) {
// Your DataStore implementation
}
override fun getCounter(counterFireAndForget: CounterFireAndForget): Int? {
// Your DataStore implementation for counter
}
override fun setCounter(counterFireAndForget: CounterFireAndForget, value: Int) {
// Your DataStore implementation for counter
}
}fun showAppContent() {
val runner = SettingsFireAndForgetRunner(Settings())
val onboarding = OnboardingFlag(runner)
if (onboarding.isEnabled()) {
// This will only show once
showOnboardingScreen(
onComplete = {
onboarding.disable() // Mark as completed
}
)
} else {
showMainScreen()
}
}abstract class FireAndForget(
val fireAndForgetRunner: FireAndForgetRunner,
val name: String,
val defaultValue: Boolean = true,
val autoDisable: Boolean = false,
)fireAndForgetRunner: The runner implementation that handles state persistencename: Unique identifier for this flag (used as storage key)defaultValue: Initial state (default: true = enabled)autoDisable: When true, automatically disables the flag on first call to isEnabled() (default: false)isEnabled(): Boolean - Returns true if the code should executedisable() - Marks the flag as executed (disables it)reset() - Resets the flag back to defaultValue (allows re-execution)abstract class CounterFireAndForget(
fireAndForgetRunner: FireAndForgetRunner,
name: String,
val counter: Int,
) : FireAndForget(...)fireAndForgetRunner: The runner implementation that handles state persistencename: Unique identifier for this flag (used as storage key)counter: Number of times isEnabled() will return true before returning false
isEnabled(): Boolean - Returns true if counter > 0, decrements counter on each callreset() - Resets the counter back to initial valueval feature = CounterFireAndForget(runner, "feature", counter = 3)
feature.isEnabled() // Call 1: true (counter: 3 β 2)
feature.isEnabled() // Call 2: true (counter: 2 β 1)
feature.isEnabled() // Call 3: true (counter: 1 β 0)
feature.isEnabled() // Call 4+: false (counter: 0)
feature.reset() // Resets counter to 3abstract class FireAndForgetRunner {
fun isEnabled(fireAndForget: FireAndForget): Boolean
protected abstract fun checkEnabled(fireAndForget: FireAndForget): Boolean
abstract fun disable(fireAndForget: FireAndForget)
abstract fun reset(fireAndForget: FireAndForget)
// Counter support for CounterFireAndForget
fun isEnabledCounter(counterFireAndForget: CounterFireAndForget): Boolean
protected abstract fun getCounter(counterFireAndForget: CounterFireAndForget): Int?
protected abstract fun setCounter(counterFireAndForget: CounterFireAndForget, value: Int)
fun resetCounter(counterFireAndForget: CounterFireAndForget)
}Implementation Note: When creating a custom runner, you must override checkEnabled() instead of isEnabled(). The isEnabled() method is final and handles the autoDisable logic internally, ensuring it cannot be bypassed by runner implementations. You must also implement getCounter() and setCounter() to support CounterFireAndForget.
class WelcomeMessage(runner: FireAndForgetRunner) : FireAndForget(
fireAndForgetRunner = runner,
name = "welcome_message"
)
fun showHomeScreen() {
val runner = SettingsFireAndForgetRunner(Settings())
val welcomeFlag = WelcomeMessage(runner)
if (welcomeFlag.isEnabled()) {
showWelcomeDialog(
onDismiss = { welcomeFlag.disable() }
)
}
}class NewFeatureAnnouncement(runner: FireAndForgetRunner) : FireAndForget(
fireAndForgetRunner = runner,
name = "feature_announcement_v2"
)
fun showMainScreen() {
val runner = SettingsFireAndForgetRunner(Settings())
val announcement = NewFeatureAnnouncement(runner)
if (announcement.isEnabled()) {
// Show announcement
displayMessage("Check out our new feature!")
announcement.disable()
}
}class AppTutorial(runner: FireAndForgetRunner) : FireAndForget(
fireAndForgetRunner = runner,
name = "app_tutorial"
)
fun handleRestartTutorial() {
val runner = SettingsFireAndForgetRunner(Settings())
val tutorial = AppTutorial(runner)
// Allow tutorial to run again
tutorial.reset()
navigateToTutorial()
}fun showApp() {
val runner = SettingsFireAndForgetRunner(Settings())
// Multiple flags can share the same runner
val onboarding = OnboardingFlag(runner)
val tutorial = TutorialFlag(runner)
val featureAnnouncement = FeatureAnnouncementFlag(runner)
when {
onboarding.isEnabled() -> showOnboardingScreen { onboarding.disable() }
tutorial.isEnabled() -> showTutorialScreen { tutorial.disable() }
else -> showMainScreen()
}
}class FirstRunSetup(runner: FireAndForgetRunner) : FireAndForget(
fireAndForgetRunner = runner,
name = "first_run_setup"
)
fun initializeApp() {
val runner = SettingsFireAndForgetRunner(Settings())
val setup = FirstRunSetup(runner)
if (setup.isEnabled()) {
// Perform first-run initialization
initializeDatabase()
downloadInitialData()
setup.disable()
}
}Use autoDisable = true to automatically disable the flag on first access without manually calling disable():
class QuickTip(runner: FireAndForgetRunner) : FireAndForget(
fireAndForgetRunner = runner,
name = "quick_tip",
autoDisable = true // Automatically disables after first isEnabled() call
)
fun showScreen() {
val runner = SettingsFireAndForgetRunner(Settings())
val quickTip = QuickTip(runner)
// First call: returns true and automatically disables
if (quickTip.isEnabled()) {
showTooltip("Here's a quick tip!")
// No need to call quickTip.disable()
}
// Subsequent calls: returns false
quickTip.isEnabled() // false
}This is perfect for fire-and-forget operations where you don't have a natural completion callback to call disable(). The flag automatically marks itself as executed when accessed for the first time.
Use CounterFireAndForget to execute code a specific number of times before disabling:
import com.alorma.fireandforget.CounterFireAndForget
class LimitedPromo(runner: FireAndForgetRunner, times: Int = 3) : CounterFireAndForget(
fireAndForgetRunner = runner,
name = "limited_promo",
counter = times // Will return true 3 times, then false
)
fun showScreen() {
val runner = SettingsFireAndForgetRunner(Settings())
val promo = LimitedPromo(runner, times = 3)
// First 3 calls: returns true and decrements counter
if (promo.isEnabled()) {
showPromoBanner("Special offer!")
}
// After 3 calls: returns false
// Counter state persists across app restarts
}isEnabled() calltrue while counter > 0false when counter reaches 0reset() restores counter to initial valuePerfect for:
To show a feature only AFTER it's been accessed N times (inverse logic), use !isEnabled():
class ShowAfterVisits(runner: FireAndForgetRunner, visits: Int = 3) : CounterFireAndForget(
fireAndForgetRunner = runner,
name = "show_after_visits",
counter = visits
)
fun showScreen() {
val runner = SettingsFireAndForgetRunner(Settings())
val showAfterVisits = ShowAfterVisits(runner, visits = 3)
// Track visits silently - returns true for first 3 visits, false after
val stillCounting = showAfterVisits.isEnabled()
// Show feature ONLY after counter reaches 0 (inverse logic)
if (!stillCounting) {
showAdvancedFeature("You've visited 3 times! Here's an advanced feature.")
}
// Visit 1: stillCounting = true β feature NOT shown
// Visit 2: stillCounting = true β feature NOT shown
// Visit 3: stillCounting = true β feature NOT shown
// Visit 4+: stillCounting = false β feature SHOWN
}Alternative: Track and Show Once After N Visits
class UnlockAfterUses(runner: FireAndForgetRunner, requiredUses: Int = 3) : CounterFireAndForget(
fireAndForgetRunner = runner,
name = "unlock_tracker",
counter = requiredUses
)
class FeatureUnlocked(runner: FireAndForgetRunner) : FireAndForget(
fireAndForgetRunner = runner,
name = "feature_unlocked",
autoDisable = true // Show once, then auto-disable
)
fun showScreen() {
val runner = SettingsFireAndForgetRunner(Settings())
val unlockTracker = UnlockAfterUses(runner, requiredUses = 3)
val featureUnlocked = FeatureUnlocked(runner)
// Track usage silently
val stillTracking = unlockTracker.isEnabled()
// Show unlock message once when counter reaches 0
if (!stillTracking && featureUnlocked.isEnabled()) {
showUnlockMessage("π Premium feature unlocked after 3 uses!")
}
}Perfect for:
This repository contains:
# Build core library
./gradlew :core:build
# Build multiplatform-settings runner
./gradlew :multiplatform-settings:build
# Build everything
./gradlew build# Android sample
./gradlew :samples:androidApp:assembleDebug
# Desktop sample
./gradlew :samples:desktopApp:run# Run all tests across all platforms
./gradlew allTests
# Run platform-specific tests
./gradlew jvmTest
./gradlew jsTest
./gradlew iosSimulatorArm64TestContributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.