DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Android AlarmManager & Build Variants — Scheduling & Environment Guide

Android AlarmManager & Build Variants — Scheduling & Environment Guide

Building reliable Android apps requires two critical skills: scheduling tasks correctly and managing environment-specific configurations. This guide covers both essentials with production-ready patterns.

Part 1: AlarmManager Fundamentals

AlarmManager vs WorkManager

When should you use each?

Feature AlarmManager WorkManager
Timing Exact time Flexible, best-effort
Persistence Survives reboot Survives reboot
Battery Can impact (exact alarms) Optimized for battery
Min interval No minimum 15min recommended
Use case Clock apps, precise timers Periodic tasks, syncs

Recommendation:

  • Use AlarmManager for: time-critical alarms (alarms, timers, notifications at exact times)
  • Use WorkManager for: periodic background work (syncs, logs, periodic checks)

The Core Pattern: ExactAndAllowWhileIdle

For reliable notifications/alarms, use setExactAndAllowWhileIdle():

import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import java.util.Calendar

fun scheduleExactAlarm(context: Context, hourOfDay: Int, minute: Int) {
    val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager

    val intent = Intent(context, AlarmReceiver::class.java).apply {
        action = "com.example.ALARM_ACTION"
    }

    // FLAG_IMMUTABLE is required on API 31+
    val pendingIntent = PendingIntent.getBroadcast(
        context,
        0,
        intent,
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
    )

    val calendar = Calendar.getInstance().apply {
        set(Calendar.HOUR_OF_DAY, hourOfDay)
        set(Calendar.MINUTE, minute)
        set(Calendar.SECOND, 0)
    }

    // If time has already passed today, schedule for tomorrow
    if (calendar.timeInMillis <= System.currentTimeMillis()) {
        calendar.add(Calendar.DAY_OF_MONTH, 1)
    }

    // API 31+ requires SCHEDULE_EXACT_ALARM permission
    alarmManager.setExactAndAllowWhileIdle(
        AlarmManager.RTC_WAKEUP,
        calendar.timeInMillis,
        pendingIntent
    )
}
Enter fullscreen mode Exit fullscreen mode

BroadcastReceiver Implementation

Handle the alarm in a BroadcastReceiver:

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import java.util.concurrent.TimeUnit

class AlarmReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        if (context == null) return

        // Avoid long operations in onReceive (max 10 seconds)
        // Delegate to WorkManager or start a service
        val workRequest = OneTimeWorkRequestBuilder<AlarmWorker>()
            .setInitialDelay(0, TimeUnit.SECONDS)
            .build()

        WorkManager.getInstance(context).enqueueUniqueWork(
            "alarm_work",
            androidx.work.ExistingWorkPolicy.KEEP,
            workRequest
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

PendingIntent.FLAG_IMMUTABLE (API 31+)

Starting with Android 12, FLAG_IMMUTABLE is required for security:

// ❌ WRONG (deprecated on API 31+)
val pendingIntent = PendingIntent.getBroadcast(
    context, 0, intent,
    PendingIntent.FLAG_UPDATE_CURRENT  // Unsafe
)

// ✅ CORRECT
val pendingIntent = PendingIntent.getBroadcast(
    context, 0, intent,
    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
Enter fullscreen mode Exit fullscreen mode

Daily Repeating Alarms

Schedule an alarm that repeats every 24 hours:

fun scheduleDailyAlarm(context: Context, hourOfDay: Int, minute: Int) {
    val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager

    val intent = Intent(context, AlarmReceiver::class.java)
    val pendingIntent = PendingIntent.getBroadcast(
        context, 1, intent,
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
    )

    val calendar = Calendar.getInstance().apply {
        set(Calendar.HOUR_OF_DAY, hourOfDay)
        set(Calendar.MINUTE, minute)
    }

    if (calendar.timeInMillis <= System.currentTimeMillis()) {
        calendar.add(Calendar.DAY_OF_MONTH, 1)
    }

    // Repeating alarm every 24 hours
    alarmManager.setExactAndAllowWhileIdle(
        AlarmManager.RTC_WAKEUP,
        calendar.timeInMillis,
        pendingIntent
    )

    // Re-schedule after firing (in AlarmReceiver)
    val nextTime = Calendar.getInstance().apply {
        add(Calendar.DAY_OF_MONTH, 1)
    }
    alarmManager.setExactAndAllowWhileIdle(
        AlarmManager.RTC_WAKEUP,
        nextTime.timeInMillis,
        pendingIntent
    )
}
Enter fullscreen mode Exit fullscreen mode

Boot Completed Restore

Alarms are cleared on device reboot. Restore them in a BOOT_COMPLETED receiver:

class BootCompletedReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        if (intent?.action == Intent.ACTION_BOOT_COMPLETED && context != null) {
            // Restore all scheduled alarms from SharedPreferences or database
            val prefs = context.getSharedPreferences("alarms", Context.MODE_PRIVATE)
            val alarmHour = prefs.getInt("alarm_hour", 8)
            val alarmMinute = prefs.getInt("alarm_minute", 0)

            scheduleExactAlarm(context, alarmHour, alarmMinute)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Add to AndroidManifest.xml:

<receiver android:name=".BootCompletedReceiver"
    android:exported="false">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>
Enter fullscreen mode Exit fullscreen mode

Required Permissions

Add to AndroidManifest.xml:

<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
Enter fullscreen mode Exit fullscreen mode

Part 2: Build Variants for Environment Management

Android's build system lets you create multiple app variants from a single codebase—perfect for debug/staging/production environments.

Build Types vs Product Flavors

Build Types (debug/release/staging):

  • Control compilation & optimization
  • One per build (mutually exclusive)
  • Set app signing, debuggability, minification

Product Flavors (free/premium):

  • Represent different product variants
  • Multiple can combine with build types
  • Control features, branding, API endpoints

Basic Setup: buildTypes

In build.gradle.kts:

android {
    compileSdk = 35

    buildTypes {
        debug {
            isDebuggable = true
            applicationIdSuffix = ".debug"
            versionNameSuffix = "-DEBUG"
            buildConfigField("String", "API_URL", ""https://api-dev.example.com"")
            buildConfigField("Boolean", "ENABLE_LOGS", "true")
        }

        release {
            isDebuggable = false
            isMinifyEnabled = true
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
            buildConfigField("String", "API_URL", ""https://api.example.com"")
            buildConfigField("Boolean", "ENABLE_LOGS", "false")
        }

        create("staging") {
            initWith(buildTypes.getByName("release"))
            isDebuggable = true
            applicationIdSuffix = ".staging"
            versionNameSuffix = "-STAGING"
            buildConfigField("String", "API_URL", ""https://api-staging.example.com"")
            buildConfigField("Boolean", "ENABLE_LOGS", "true")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Use in code:

import com.example.BuildConfig

class ApiClient {
    companion object {
        fun getBaseUrl() = BuildConfig.API_URL
        fun shouldEnableLogs() = BuildConfig.ENABLE_LOGS
    }
}
Enter fullscreen mode Exit fullscreen mode

Product Flavors: free/premium

Add flavors for feature differentiation:

android {
    flavorDimensions = listOf("tier")

    productFlavors {
        create("free") {
            dimension = "tier"
            applicationIdSuffix = ".free"
            buildConfigField("Boolean", "IS_PREMIUM", "false")
            buildConfigField("Int", "MAX_ITEMS", "10")
        }

        create("premium") {
            dimension = "tier"
            applicationIdSuffix = ".premium"
            buildConfigField("Boolean", "IS_PREMIUM", "true")
            buildConfigField("Int", "MAX_ITEMS", "999")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Available variants: freeDebug, freeRelease, premiumDebug, premiumRelease, etc.

Per-Variant Source Sets

Customize code/resources by variant:

app/src/
├── main/
├── debug/
│   ├── kotlin/com/example/DebugUtil.kt
│   └── res/values/strings.xml
├── free/
│   ├── kotlin/com/example/Feature.kt  (free implementation)
│   └── res/drawable/logo.webp
├── premium/
│   ├── kotlin/com/example/Feature.kt  (premium implementation)
│   └── res/drawable/logo_premium.webp
└── freeDebug/
    └── res/values/strings.xml  (free+debug only)
Enter fullscreen mode Exit fullscreen mode

Gradle automatically includes the right files when building ./gradlew buildFreeDebug.

buildConfigField for Dynamic Config

Centralize environment settings without hardcoding:

buildTypes {
    debug {
        buildConfigField("String", "LOG_LEVEL", "\"DEBUG\"")
        buildConfigField("Long", "CACHE_TTL_MS", "60000")  // 1 minute
        buildConfigField("String[]", "ALLOWED_HOSTS",
            "new String[]{"localhost", "127.0.0.1"}")
    }
    release {
        buildConfigField("String", "LOG_LEVEL", "\"WARN\"")
        buildConfigField("Long", "CACHE_TTL_MS", "3600000")  // 1 hour
        buildConfigField("String[]", "ALLOWED_HOSTS",
            "new String[]{"api.example.com"}")
    }
}
Enter fullscreen mode Exit fullscreen mode

Access in code:

Log.i("Config", "Cache TTL: ${BuildConfig.CACHE_TTL_MS}ms")
Log.i("Config", "Log level: ${BuildConfig.LOG_LEVEL}")
Enter fullscreen mode Exit fullscreen mode

Complete Example: Multi-Environment App

// build.gradle.kts
android {
    buildTypes {
        debug {
            applicationIdSuffix = ".debug"
            buildConfigField("String", "API_BASE", ""http://10.0.2.2:3000"")  // emulator
        }
        create("staging") {
            buildConfigField("String", "API_BASE", ""https://staging-api.example.com"")
        }
        release {
            buildConfigField("String", "API_BASE", ""https://api.example.com"")
        }
    }

    productFlavors {
        create("internal") {
            applicationIdSuffix = ".internal"
            buildConfigField("Boolean", "INTERNAL_BUILD", "true")
        }
        create("public") {
            buildConfigField("Boolean", "INTERNAL_BUILD", "false")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Build commands:

# Debug build
./gradlew buildDebug

# Staging build (closest to production)
./gradlew buildStaging

# Release APK for production
./gradlew buildRelease

# Specific flavor + type
./gradlew buildInternalDebug
./gradlew buildPublicRelease
Enter fullscreen mode Exit fullscreen mode

Combining Alarms with Build Variants

A production pattern: use different alarm schedules per environment.

fun scheduleAppAlarms(context: Context) {
    val alarmHour = if (BuildConfig.DEBUG) {
        7  // earlier in development
    } else {
        8  // standard for users
    }

    val alarmMinute = if (BuildConfig.INTERNAL_BUILD) {
        0  // exact time for internal testing
    } else {
        randomMinute()  // stagger for production
    }

    scheduleExactAlarm(context, alarmHour, alarmMinute)
}
Enter fullscreen mode Exit fullscreen mode

Summary

  • AlarmManager: Use setExactAndAllowWhileIdle() with FLAG_IMMUTABLE for reliable notifications
  • BroadcastReceiver: Keep it lightweight, delegate work to WorkManager
  • Boot Restore: Persist alarm settings and restore on device reboot
  • Build Variants: Manage debug/release/staging configs in buildTypes and buildConfigField
  • Product Flavors: Create free/premium app variants from one codebase
  • Source Sets: Customize code & resources per variant automatically

Combining precise scheduling with environment-aware configurations gives you a robust, maintainable Android app.


8 Android App Templates → https://myougatheax.gumroad.com

Top comments (0)