FireAndForget
1.3.0indexedExecute 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.
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)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:
This 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:
Implement FireAndForgetRunner with your preferred storage solution (Room, DataStore, SQLDelight, etc.):
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: = enabled)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 falseisEnabled(): 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 3
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 .
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())
quickTip = QuickTip(runner)
(quickTip.isEnabled()) {
showTooltip()
}
quickTip.isEnabled()
}
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
)
{
runner = SettingsFireAndForgetRunner(Settings())
promo = LimitedPromo(runner, times = )
(promo.isEnabled()) {
showPromoBanner()
}
}
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 (runner: FireAndForgetRunner, visits: = ) : CounterFireAndForget(
fireAndForgetRunner = runner,
name = ,
counter = visits
)
{
runner = SettingsFireAndForgetRunner(Settings())
showAfterVisits = ShowAfterVisits(runner, visits = )
stillCounting = showAfterVisits.isEnabled()
(!stillCounting) {
showAdvancedFeature()
}
}
Alternative: Track and Show Once After N Visits
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 iosSimulatorArm64Test
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.
CounterFireAndForgetFireAndForget (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 valueclass 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
}
}
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
}
}
trueautoDisable: When true, automatically disables the flag on first call to isEnabled() (default: false)abstract 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)
}
CounterFireAndForgetclass 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!")
}
}
Surfaced from shared tags and platforms — no rankings paid for.