How to Create an Android Service in Kotlin: A Step-by-Step Guide

How to Create an Android Service in Kotlin: A Step-by-Step Guide


In this beginner-friendly guide, you’ll learn how to create an Android service in Kotlin the modern way. We’ll cover what an Android Service is, when you should (and shouldn’t) use one, and walk through step-by-step examples: a simple started service, a foreground service with a persistent notification, and a bound service you can call from your UI. We’ll use Kotlin, coroutines, and a small Jetpack Compose screen to start, stop, and bind to services. By the end, you’ll understand the service lifecycle in Android Kotlin and how to build safe, compliant services for current Android versions.

What is an Android Service?

How to create an Android service in Kotlin: foreground service with persistent media-style notification
Foreground service concept with a persistent notification and media controls.

An Android Service is an application component designed to perform long-running operations in the background without a UI. Services are useful when work must continue even if your app’s activity goes away. In modern Android (Android 12–15), services are mainly for:

  • User-perceptible, ongoing work that must run right now (foreground services), like media playback, turn-by-turn navigation, live uploads, or active device connections.
  • Long-lived work that your UI binds to (bound services) to call methods or receive updates.

If your work is deferrable, requires constraints (like Wi‑Fi), should survive process death, or needs guaranteed retries, prefer WorkManager over a plain service. This is the recommended approach for most background tasks.

When should you use a Service in 2026?

  • Use a foreground service (FGS) when work is user-initiated and user-perceptible right now and must keep running even if the user leaves your app.
  • Use a bound service when your activity/fragment needs to talk to a long-lived component (for example, a download manager you control).
  • Use a started service only for short tasks while your app stays in the foreground. If the app goes to background, starting a background service is restricted on modern Android.
  • Use WorkManager for scheduled, deferrable, or guaranteed work (recommended for most background tasks).

Service types in Android, explained for beginners

How to create an Android service in Kotlin: bound service communication between activity and background service
Bound service concept showing two-way communication with an activity.

Started vs bound vs foreground services

  • Started service: You call startService() (or ContextCompat.startForegroundService()). It runs until it calls stopSelf() or you call stopService(). On modern Android, you should keep these short and usually only while the app is in the foreground.
  • Bound service: Your UI binds with bindService() and gets a Binder object to call service methods. The service lives as long as one or more clients remain bound.
  • Foreground service (FGS): A started service that shows a persistent notification and is intended for ongoing, user-perceptible work. On Android 14+, you must declare a foregroundServiceType and often a matching FOREGROUND_SERVICE_* permission.

Quick visual comparison: services vs WorkManager

Component Use when Start/Bind APIs Notification? Lifetime Beginner tip
Started Service Short work while app is foreground; no UI callbacks needed startService(), stop with stopSelf()/stopService() No Until stopped; avoid in background on Android 12+ Keep work short; prefer WorkManager if deferrable
Foreground Service (FGS) User-perceptible, ongoing work that must run now ContextCompat.startForegroundService() then ServiceCompat.startForeground(..., type) Yes (ongoing) Runs while foreground; stop when work ends; some types time out on Android 15 Declare correct foregroundServiceType and matching permission on Android 14+
Bound Service UI needs a long-lived component to call directly bindService()/unbindService(); use Binder No (unless also foreground) Lives while at least one client is bound Unbind in UI lifecycle to avoid leaks
WorkManager (not a Service) Deferrable, guaranteed work with constraints/retries WorkManager.enqueue(WorkRequest) No (system may show) Managed by system; can survive restarts Default choice for most background tasks

Before you start

  • Use the latest Android Gradle Plugin and target the latest stable Android SDK.
  • Add Kotlin coroutines and Jetpack libraries (core-ktx, activity-compose, lifecycle if you need it).
  • Understand that Android 12+ restricts starting foreground services from the background. Always start FGS from user actions when your app is visible, or use documented exemptions only.

How to create an Android service in Kotlin: step-by-step

1) Add permissions and declare services in AndroidManifest.xml

For a foreground service on Android 14+, declare the proper foregroundServiceType and its matching permission. If you show notifications on Android 13+, request POST_NOTIFICATIONS at runtime. Below is an example for a data sync foreground service plus started and bound services.

XML
<manifest ...>

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Declare the specific FGS permission that matches your type on Android 14+ -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

<application ...>

<service
android:name=".services.ExampleStartedService"
android:exported="false" />

<service
android:name=".services.ExampleForegroundService"
android:exported="false"
android:foregroundServiceType="dataSync" />

<service
android:name=".services.ExampleBoundService"
android:exported="false" />

</application>

</manifest>

Notes:

  • Pick the correct type for your FGS, such as dataSync, mediaPlayback, location, camera, microphone, or mediaProcessing. Android 14+ requires you to declare it and pass the same type when starting foreground.
  • On Android 13+, most notifications require POST_NOTIFICATIONS runtime permission (some exemptions exist like media playback/calls). You can start an FGS without it, but your notification may be restricted if permission isn’t granted.

2) Create a simple started background service in Kotlin

This started service performs a short task on a background coroutine and stops itself when done. Use this only while your app is in the foreground.

Kotlin
Code
class="cd-package">package com.example.app.services

class="cd-package">import android.app.Service
class="cd-package">import android.content.Intent
class="cd-package">import android.os.IBinder
class="cd-package">import android.util.Log
class="cd-package">import kotlinx.coroutines.*

class ExampleStartedService : Service() {

    class="cd-keyword cd-access">private val job = SupervisorJob()
    class="cd-keyword cd-access">private val scope = CoroutineScope(Dispatchers.IO + job)

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        scope.launch {
            try {
                for (i in 1..5) {
                    Log.d(TAG, "Working... step $i")
                    delay(1000)
                }
            } finally {
                // Stop this specific start request instance
                stopSelf(startId)
            }
        }
        return START_NOT_STICKY
    }

    override fun onBind(intent: Intent?): IBinder? = null

    override fun onDestroy() {
        job.cancel()
        super.onDestroy()
    }

    companion object {
        class="cd-keyword cd-access">private const val TAG = "ExampleStartedService"
    }
}

3) Create a foreground service with notification in Kotlin

A foreground service must post an ongoing notification and specify its service type. Below is a minimal data sync example that shows progress and stops itself when finished. Replace R.drawable.ic_sync with a valid small icon in your project (vector asset recommended).

Kotlin
Code
class="cd-package">package com.example.app.services

class="cd-package">import android.app.Notification
class="cd-package">import android.app.NotificationChannel
class="cd-package">import android.app.NotificationManager
class="cd-package">import android.app.PendingIntent
class="cd-package">import android.app.Service
class="cd-package">import android.content.Intent
class="cd-package">import android.content.pm.ServiceInfo
class="cd-package">import android.os.Build
class="cd-package">import android.os.IBinder
class="cd-package">import androidx.annotation.RequiresApi
class="cd-package">import androidx.core.app.NotificationCompat
class="cd-package">import androidx.core.app.NotificationManagerCompat
class="cd-package">import androidx.core.app.ServiceCompat
class="cd-package">import com.example.app.MainActivity
class="cd-package">import com.example.app.R
class="cd-package">import kotlinx.coroutines.*

class ExampleForegroundService : Service() {

    class="cd-keyword cd-access">private val job = SupervisorJob()
    class="cd-keyword cd-access">private val scope = CoroutineScope(Dispatchers.IO + job)

    override fun onCreate() {
        super.onCreate()
        createNotificationChannel()
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val notifId = NOTIFICATION_ID
        val notification = buildNotification(progress = 0, content = "Sync starting…")
        // Pass the same type you declared in the manifest (Android 14+ requirement)
        ServiceCompat.startForeground(
            this,
            notifId,
            notification,
            ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
        )

        scope.launch {
            try {
                for (p in 0..100 step 10) {
                    updateNotification(p)
                    delay(700)
                }
            } finally {
                stopForeground(STOP_FOREGROUND_REMOVE)
                stopSelf(startId)
            }
        }

        return START_NOT_STICKY
    }

    class="cd-keyword cd-access">private fun updateNotification(progress: Int) {
        NotificationManagerCompat.from(this)
            .notify(NOTIFICATION_ID, buildNotification(progress, "Syncing… $progress%"))
    }

    class="cd-keyword cd-access">private fun buildNotification(progress: Int, content: String): Notification {
        val openApp = PendingIntent.getActivity(
            this,
            0,
            Intent(this, MainActivity::class.java),
            PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
        )
        val builder = NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("Data sync")
            .setContentText(content)
            .setSmallIcon(R.drawable.ic_sync)
            .setOngoing(true)
            .setOnlyAlertOnce(true)
            .setContentIntent(openApp)

        if (progress in 1..99) {
            builder.setProgress(100, progress, false)
        } else {
            builder.setProgress(0, 0, false)
        }
        return builder.build()
    }

    class="cd-keyword cd-access">private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                CHANNEL_ID,
                "Sync",
                NotificationManager.IMPORTANCE_LOW
            )
            val nm = getSystemService(NotificationManager::class.java)
            nm.createNotificationChannel(channel)
        }
    }

    // Android 15 (API 35) can time out certain FGS types like dataSync/mediaProcessing
    class="cd-annotation">@RequiresApi(35)
    override fun onTimeout(timeoutType: Int) {
        // Stop promptly if the system times out your FGS
        stopForeground(STOP_FOREGROUND_REMOVE)
        stopSelf()
    }

    override fun onBind(intent: Intent?): IBinder? = null

    override fun onDestroy() {
        job.cancel()
        super.onDestroy()
    }

    companion object {
        class="cd-keyword cd-access">private const val CHANNEL_ID = "sync_channel"
        class="cd-keyword cd-access">private const val NOTIFICATION_ID = 42
    }
}

Foreground service launch flow (Android 12–15)

1) User taps a UI control to start work (must be user-initiated and app visible)
2) (Android 13+) Request POST_NOTIFICATIONS if you will show notifications
3) Call ContextCompat.startForegroundService(Intent) from the UI thread
4) In onStartCommand: create channel, build ongoing notification
5) Immediately call ServiceCompat.startForeground(..., type) with the SAME foregroundServiceType you declared
6) Do work on a coroutine (Dispatchers.IO), update notification as needed
7) Finish: stopForeground(...) then stopSelf(). (Android 15: also handle onTimeout)
8) Catch ForegroundServiceStartNotAllowedException if background-start is blocked

Tip: If your task isn’t user-perceptible right now, use WorkManager instead of an FGS.

4) Create a bound service (optional, for two-way communication)

A bound service gives your UI a Binder so you can call its functions. Here’s a simple example that increments a counter every second while clients are bound.

Kotlin
Code
class="cd-package">package com.example.app.services

class="cd-package">import android.app.Service
class="cd-package">import android.content.Intent
class="cd-package">import android.os.Binder
class="cd-package">import android.os.IBinder
class="cd-package">import android.util.Log
class="cd-package">import kotlinx.coroutines.*

class ExampleBoundService : Service() {

    class="cd-keyword cd-access">private val binder = LocalBinder()
    class="cd-keyword cd-access">private val job = SupervisorJob()
    class="cd-keyword cd-access">private val scope = CoroutineScope(Dispatchers.Default + job)
    class="cd-keyword cd-access">private var counter = 0
    class="cd-keyword cd-access">private var countingJob: Job? = null

    inner class LocalBinder : Binder() {
        fun getService(): ExampleBoundService = thisclass="cd-annotation">@ExampleBoundService
    }

    override fun onBind(intent: Intent?): IBinder = binder

    fun startCounting() {
        if (countingJob?.isActive == true) return
        countingJob = scope.launch {
            while (isActive) {
                delay(1000)
                counter++
                Log.d(TAG, "Counter: $counter")
            }
        }
    }

    fun stopCounting() {
        countingJob?.cancel()
        countingJob = null
    }

    fun getCounter(): Int = counter

    override fun onUnbind(intent: Intent?): Boolean {
        stopCounting()
        return super.onUnbind(intent)
    }

    override fun onDestroy() {
        job.cancel()
        super.onDestroy()
    }

    companion object {
        class="cd-keyword cd-access">private const val TAG = "ExampleBoundService"
    }
}

5) Start, stop, and bind from a Jetpack Compose screen

This Compose screen:

  • Requests the notifications permission on Android 13+.
  • Starts/stops the foreground service.
  • Starts a short started service task.
  • Binds/unbinds to the bound service and shows a live counter.
Kotlin
Code
class="cd-package">package com.example.app.ui

class="cd-package">import android.Manifest
class="cd-package">import android.content.*
class="cd-package">import android.content.pm.PackageManager
class="cd-package">import android.os.Build
class="cd-package">import android.os.IBinder
class="cd-package">import androidx.activity.compose.rememberLauncherForActivityResult
class="cd-package">import androidx.activity.result.contract.ActivityResultContracts
class="cd-package">import androidx.compose.foundation.layout.*
class="cd-package">import androidx.compose.material3.*
class="cd-package">import androidx.compose.runtime.*
class="cd-package">import androidx.compose.ui.Modifier
class="cd-package">import androidx.compose.ui.platform.LocalContext
class="cd-package">import androidx.compose.ui.unit.dp
class="cd-package">import androidx.core.content.ContextCompat
class="cd-package">import com.example.app.services.ExampleBoundService
class="cd-package">import com.example.app.services.ExampleForegroundService
class="cd-package">import com.example.app.services.ExampleStartedService

class="cd-annotation">@Composable
fun ServiceDemoScreen() {
    val context = LocalContext.current

    // Request POST_NOTIFICATIONS on Android 13+ (optional but recommended if you show notifications)
    val notifPermissionGranted = remember { mutableStateOf(true) }
    val notifLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestPermission(),
        onResult = { granted -> notifPermissionGranted.value = granted }
    )
    LaunchedEffect(Unit) {
        if (Build.VERSION.SDK_INT >= 33) {
            val granted = ContextCompat.checkSelfPermission(
                context, Manifest.permission.POST_NOTIFICATIONS
            ) == PackageManager.PERMISSION_GRANTED
            notifPermissionGranted.value = granted
            if (!granted) notifLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
        }
    }

    var isBound by remember { mutableStateOf(false) }
    var boundService by remember { mutableStateOf<ExampleBoundService?>(null) }

    val connection = remember {
        object : ServiceConnection {
            override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
                val binder = service as ExampleBoundService.LocalBinder
                boundService = binder.getService()
                boundService?.startCounting()
            }
            override fun onServiceDisconnected(name: ComponentName?) {
                boundService = null
            }
        }
    }

    Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
        Text("Service demo (Compose)")

        Button(onClick = {
            try {
                // Start FGS from a user action while app is visible
                ContextCompat.startForegroundService(
                    context,
                    Intent(context, ExampleForegroundService::class.java)
                )
            } catch (t: Throwable) {
                // On Android 12+, background-start may throw ForegroundServiceStartNotAllowedException
                // Ensure you start from foreground or use allowed exemptions
            }
        }) {
            Text("Start Foreground Service")
        }

        Button(onClick = {
            context.stopService(Intent(context, ExampleForegroundService::class.java))
        }) {
            Text("Stop Foreground Service")
        }

        Button(onClick = {
            // Short task while app is in foreground
            context.startService(Intent(context, ExampleStartedService::class.java))
        }) {
            Text("Start Started Service")
        }

        Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
            Button(onClick = {
                if (!isBound) {
                    val ok = context.bindService(
                        Intent(context, ExampleBoundService::class.java),
                        connection,
                        Context.BIND_AUTO_CREATE
                    )
                    isBound = ok
                }
            }) { Text(if (isBound) "Already Bound" else "Bind to Bound Service") }

            Button(enabled = isBound, onClick = {
                if (isBound) {
                    boundService?.stopCounting()
                    context.unbindService(connection)
                    boundService = null
                    isBound = false
                }
            }) { Text("Unbind") }
        }

        Text("Bound service counter: ${boundService?.getCounter() ?: 0}")
    }
}

Modern Android rules you must follow

Background-start restrictions (Android 12+)

  • Android 12+ blocks starting a foreground service from the background except for limited exemptions. Start FGS from user actions while your app is visible, and catch ForegroundServiceStartNotAllowedException if needed.
  • If your work isn’t immediately user-perceptible, use WorkManager instead.

Foreground service types and timeouts (Android 14/15)

  • Android 14+: You must declare the correct foregroundServiceType in your manifest and pass the same type to ServiceCompat.startForeground. Some types also require a matching FOREGROUND_SERVICE_* permission in the manifest.
  • Android 15: Certain types (such as dataSync and mediaProcessing) have time-limited quotas per 24 hours. Implement a prompt shutdown path and handle the service timeout by stopping the FGS if the system calls your timeout callback.

Clean up correctly and avoid leaks

  • Always call stopSelf() when your work completes in a started service.
  • In a foreground service, call stopForeground(...) and then stopSelf() when done.
  • Cancel coroutines in onDestroy() to avoid leaks.
  • For bound services, unbind on the UI’s onStop()/onDestroy() (or Compose lifecycle) to prevent leaks.

Pre-launch checklist: How to create an Android service in Kotlin safely

  • Manifest: declare your service(s); for FGS add android:foregroundServiceType and required FOREGROUND_SERVICE_* permission on Android 14+.
  • Notifications: create a channel (O+), set a valid small icon, request POST_NOTIFICATIONS (Android 13+) if you plan to alert.
  • Start rules: start from visible UI; catch ForegroundServiceStartNotAllowedException on Android 12+.
  • Timing: call ServiceCompat.startForeground(...) promptly after startForegroundService.
  • Work thread: run tasks on coroutines (Dispatchers.IO); cancel in onDestroy().
  • Stop path: on completion call stopForeground(...) then stopSelf(); handle onTimeout (API 35+).
  • Bound services: unbind in onStop()/onDestroy() to avoid leaks; never hold Activity context.
  • Testing: verify on Android 12/13/14/15; test denial of notification permission and background-start restrictions.
  • Right tool: if work is deferrable or needs constraints/retries, use WorkManager instead of a Service.

When to use WorkManager instead

For background tasks that can be deferred, need constraints (charging, Wi‑Fi), should survive device restarts, or need guaranteed execution with backoff and retries, use WorkManager. It’s the recommended API for most background work and replaces older patterns like IntentService and JobIntentService (which you should not use in new code).

Output / Result

After implementing the code above and running the app:

  • Tapping “Start Foreground Service” shows an ongoing “Data sync” notification with a progress bar. After it reaches 100%, the notification disappears and the service stops.
  • Tapping “Start Started Service” logs five “Working…” messages (one per second) in Logcat, then the service stops itself.
  • Binding to the bound service starts a counter that increments every second. The Compose UI displays the current count. Unbinding stops the counter and releases the service.

Quick tips for success

  • Use coroutines on Dispatchers.IO for work inside services, and cancel in onDestroy().
  • Declare proper foreground service types and permissions on Android 14+.
  • Handle Android 12+ foreground service background-start restrictions; start from visible UI.
  • Request POST_NOTIFICATIONS on Android 13+ if your notification needs to alert the user.
  • Prefer WorkManager for most background tasks that are not user-perceptible right now.

FAQ: Android service tutorial for beginners

What is an Android service and when should I use one?

An Android Service runs work outside your UI. Use a foreground service for user-perceptible, ongoing work that must run immediately (media, navigation, active uploads). Use a bound service to give your UI an API for long-lived work. For deferrable or guaranteed background tasks, prefer WorkManager.

How do I create my first background service in Kotlin?

Create a Service subclass, start coroutines for your work on Dispatchers.IO, and call stopSelf() when done. If the task must keep running and be visible to the user, create a foreground service instead and post an ongoing notification.

What is the difference between started and bound services in Android?

  • Started: Launched with startService() and runs until stopped. No direct API back to the UI.
  • Bound: UI binds with bindService() and gets a Binder to call functions directly. The service lives while clients are bound.

How do I start and stop a service in Kotlin?

  • Started service: context.startService(Intent(...)), then stop with context.stopService(Intent(...)) or stopSelf() inside the service.
  • Foreground service: ContextCompat.startForegroundService(...) then ServiceCompat.startForeground(...) to post the notification within a few seconds. Stop with stopForeground(...) and stopSelf().
  • Bound service: bindService(...) to connect and unbindService(...) to disconnect.

Do foreground services require a persistent notification in Android?

Yes. Every foreground service must display an ongoing notification as long as it’s running in the foreground. On Android 13+, showing notifications generally requires the POST_NOTIFICATIONS runtime permission (with limited exemptions like media playback/calls). On Android 14+, you must also declare the correct service type and matching permissions.

Wrap-up

Now you know how to create an Android service in Kotlin using modern best practices: started, bound, and foreground services with notifications. Remember: use a foreground service only for user-perceptible, ongoing work; otherwise, prefer WorkManager. Following these patterns will keep your app reliable and compliant on Android 12 to 15.

Sources / Further reading

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted