Services & Foreground Service
A Service is an application component that can run in the background without a user interface. Unlike an Activity, a Service has no screen of its own – it exists purely to do work: syncing data, playing audio, or reacting to events while the user is doing something else, or not looking at the app at all.
In RecipeBox, a Service is the natural place to put logic like "upload every recipe photo that changed since the last sync" or "push locally-added recipes to the backend" – work that should keep going even if the user has switched to their home screen.
Three Flavors of Service
Android doesn't have just one kind of Service – the right choice depends on how visible and how long-running the work is.
| Flavor | What it's for | User visibility | Modern restrictions |
|---|---|---|---|
| Service (plain/background) | Short background chores with no UI tie-in | Invisible to the user | Heavily restricted on modern Android – the OS may stop it soon after the app leaves the foreground |
| Foreground Service | Ongoing work the user should know about (sync, playback, tracking) | Must show a persistent notification while running | Allowed to run longer, but must call startForeground() promptly and declare a foreground service type |
| Bound Service | Components that need to talk to the Service directly (method calls, callbacks) | No notification required by itself | Lives only as long as at least one client stays bound via IBinder |
Foreground Service: RecipeSyncService
Suppose RecipeBox needs to push newly-added recipes to a backend, even while the app is in the background. Because this is ongoing work the user should be aware of, we build it as a Foreground Service that shows a persistent "Syncing recipes..." notification – exactly the kind of notification channel covered in Notifications & Channels.
// RecipeSyncService.java
public class RecipeSyncService extends Service {
public static final String CHANNEL_ID = "recipe_sync_channel";
private static final int NOTIF_ID = 501;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("RecipeBox")
.setContentText("Syncing recipes...")
.setSmallIcon(R.drawable.ic_sync)
.setOngoing(true)
.build();
// Must be called within a few seconds of starting the service
startForeground(NOTIF_ID, notification);
syncRecipesInBackground();
// If the system kills the service, do not automatically recreate it
return START_NOT_STICKY;
}
private void syncRecipesInBackground() {
new Thread(() -> {
List<Recipe> pending = RecipeRepository.getInstance(this).getUnsyncedRecipes();
for (Recipe recipe : pending) {
RecipeApiClient.upload(recipe);
}
stopSelf(); // work is done, stop the foreground service cleanly
}).start();
}
@Override
public IBinder onBind(Intent intent) {
return null; // not a bound service
}
@Override
public void onDestroy() {
super.onDestroy();
// release any resources, listeners, etc. here
}
}
- onStartCommand() runs every time the Service is started via startService()/startForegroundService().
- startForeground() must be called quickly – it both shows the notification and tells the OS "treat this process as foreground-priority".
- onBind() returns null because nothing needs to bind to this Service directly.
- onDestroy() is your cleanup hook, called once the Service is stopped.
Starting the Foreground Service
From an Activity or Fragment, you never call startForeground() yourself – that happens inside the Service. What you call is ContextCompat.startForegroundService(), which correctly starts a foreground-eligible service on every supported Android version:
// Kick off the sync, e.g. after the user adds a new recipe
Intent intent = new Intent(this, RecipeSyncService.class);
ContextCompat.startForegroundService(this, intent);
<service
android:name=".RecipeSyncService"
android:foregroundServiceType="dataSync"
android:exported="false" />
Stopping the Service Cleanly
A Service stops in one of two ways: it stops itself once its work is finished, or something external stops it. Calling stopSelf() from inside the Service (as done above, right after the sync loop finishes) removes the notification and lets the process return to normal priority.
// From outside, e.g. if the user cancels sync manually
Intent intent = new Intent(this, RecipeSyncService.class);
stopService(intent);
Bound Service (Brief Look)
If a component needs to call methods directly on a running Service – for example, a settings screen that wants to ask the sync service "how many recipes are left to upload?" – you'd implement onBind() to return a real IBinder instead of null, and the caller would use bindService()/unbindService() instead of startService(). RecipeBox's sync doesn't need this level of direct interaction, so RecipeSyncService stays a plain (unbound) foreground service.
Declaring the Service in the Manifest
Every Service, like every Activity, must be declared in AndroidManifest.xml before Android will let you start it:
<application ... >
<service
android:name=".RecipeSyncService"
android:foregroundServiceType="dataSync"
android:exported="false" />
</application>
Troubleshooting
Service crashes with "startForegroundService() did not call startForeground()"? → Call startForeground() as the very first thing in onStartCommand(), not after a long background task.
Notification never disappears? → Make sure stopSelf() or stopService() actually runs; a crashed background thread can leave the Service alive.
Service seems to die randomly on newer phones? → That is expected for plain background Services; if the work matters to the user, promote it to a Foreground Service as shown above.