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
)
}
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
)
}
}
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
)
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
)
}
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)
}
}
}
Add to AndroidManifest.xml:
<receiver android:name=".BootCompletedReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
Required Permissions
Add to AndroidManifest.xml:
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
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")
}
}
}
Use in code:
import com.example.BuildConfig
class ApiClient {
companion object {
fun getBaseUrl() = BuildConfig.API_URL
fun shouldEnableLogs() = BuildConfig.ENABLE_LOGS
}
}
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")
}
}
}
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)
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"}")
}
}
Access in code:
Log.i("Config", "Cache TTL: ${BuildConfig.CACHE_TTL_MS}ms")
Log.i("Config", "Log level: ${BuildConfig.LOG_LEVEL}")
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")
}
}
}
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
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)
}
Summary
-
AlarmManager: Use
setExactAndAllowWhileIdle()withFLAG_IMMUTABLEfor 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
buildTypesandbuildConfigField - 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)