The Android lifecycle is important in managing the lifespan of various Android components, such as Activities, Fragments, Views, and Services, from their creation to their destruction. Understanding the Android lifecycle is key to correctly initializing objects and releasing resources at the appropriate time, ensuring proper memory management.
Android provides dedicated callback methods like onCreate()
, onStart()
, and onDestroy()
that are triggered at specific points in a component’s lifecycle. These methods allow developers to manage objects and resources effectively, handling tasks like initialization, behavior changes depending on app state (such as background processes or configuration changes), and releasing unused memory to optimize app performance.
As the Android ecosystem evolves, so do the development practices. Over time, many developers have shifted from Java to Kotlin, adopting more Kotlin-based tech stacks like Coroutines and Flow for handling asynchronous operations. However, the traditional Android components were initially built around callback systems, which can make seamless handling of coroutine lifecycles challenging.
To address this, AndroidX libraries introduced androidx-activity and androidx-fragment extensions, which implement LifecycleOwner and provide lifecycle-aware APIs like lifecycleScope and repeatOnLifecycle(). These APIs allow developers to safely manage lifecycle-aware objects or coroutine scopes according to the Android lifecycle, ensuring coroutines are launched and canceled at the right time. Additionally, AndroidX includes a lifecycle library for Services, specifically LifecycleService, to handle coroutine scopes conveniently in Service components.
In this article, you’ll explore LifecycleService and how you can apply similar approaches to manage the resource lifecycle in your FirebaseMessagingService, leveraging Kotlin coroutines for more efficient, modern code practices.
Understanding the Service Lifecycle
Services in Android have key callback methods that handle important aspects of their lifecycle and, if needed, allow other components to bind to the service. Here are the main callback methods you should override:
- onStartCommand(): Called when another component (e.g., an activity) starts the service using
startService()
. The service can run indefinitely in the background. It’s your responsibility to stop the service usingstopSelf()
orstopService()
once its work is done. If your service only provides binding, you don’t need to implement this method. - onBind(): Called when another component binds to the service using
bindService()
. This method must return anIBinder
to let clients communicate with the service. If you don’t allow binding, returnnull
. You must always implement this method if binding is required. - onCreate(): Called when the service is first created. Use this for one-time setup before
onStartCommand()
oronBind()
is called. If the service is already running, this method won’t be invoked again. - onDestroy(): Called when the service is no longer in use and is about to be destroyed. Implement this method to clean up resources (threads, listeners, receivers). This is the last callback the service receives.
These methods ensure that your service runs smoothly and properly handles cleanup when it’s no longer needed. So you can imagine the lifecycle instance of the LifecycleService provided by LifecycleOwner
should align with the stages outlined in the illustration below:
Since services often run in the background, failing to properly cancel coroutine tasks and release memory can lead to long-running background processes that occupy memory unnecessarily and are difficult to debug. Let’s explore how to manage this effectively using LifecycleService.
LifecycleService
The LifecycleService is provided by the lifecycle-service library, which is a Service that simply extends the LifecycleOwner
so that you can manage your coroutine scope efficiently. To start using LifecycleService, add the following dependency to your Gradle file:
dependencies { | |
implementation("androidx.lifecycle:lifecycle-service:2.8.5") | |
} |
Now, you can create your own service by extending LifecycleService
, allowing you to take advantage of lifecycleScope
. This will automatically cancel any launched coroutine jobs safely as the lifecycle progresses, as demonstrated below:
class MyService: LifecycleService() { | |
override fun onCreate() { | |
super.onCreate() | |
lifecycleScope.launch { | |
// .. | |
} | |
} | |
} |
Job Offers
It’s important to note that you don’t need to use the Lifecycle.repeatOnLifecycle
API in services. This API was primarily designed to enable safer Flow
collection in the UI layer of Android apps. Its restartable behavior is tailored for view-based components like Activities and Fragments, ensuring that processing occurs only when the UI is visible. Since services operate in the background and don’t have a UI, repeatOnLifecycle
isn’t necessary in this context.
FirebaseMessagingService
In modern mobile services and startups, increasing user engagement by delivering timely, relevant information is crucial. Most applications achieve this by leveraging push notifications to keep users informed at the right moment.
Firebase offers a cloud messaging service called Firebase Cloud Messaging (FCM), which allows you to seamlessly implement push notifications. The official documentation provides guidance on creating your own service by extending the FirebaseMessagingService, as demonstrated in the code below:
class MyService: FirebaseMessagingService() { | |
override fun onNewToken(token: String) { | |
super.onNewToken(token) | |
// If you want to send messages to this application instance or | |
// manage this apps subscriptions on the server side, send the | |
// FCM registration token to your app server. | |
sendRegistrationToServer(token) | |
} | |
} |
You may have encountered a situation where you need to register a user token with your backend server. In such cases, you should override the onNewToken
method and ensure that the required tasks are executed on a background thread. If you’re using coroutines for asynchronous tasks, it’s crucial to manage the coroutine scope’s lifecycle properly, ensuring jobs are canceled correctly in line with the service’s lifecycle.
If you examine the implementation of FirebaseMessagingService
in the open-source Firebase project on GitHub, you’ll notice it extends Service
rather than LifecycleService
. As a result, you’ll need to manually manage your coroutine scope to ensure it aligns with the service’s lifecycle.
Firebase Messaging Lifecycle KTX
To address the lifecycle management issues mentioned earlier, you can leverage the firebase-android-ktx library. The firebase-android-ktx
library offers Kotlin and Jetpack Compose-friendly Firebase extensions, helping you focus on business logic. While it includes extensions for Realtime Database, this article will focus on the Firebase Messaging Lifecycle KTX library.
The Firebase Messaging Lifecycle KTX extension enables you to implement a lifecycle-aware FirebaseMessagingService
, allowing you to manage and cancel coroutine scopes according to the service’s lifecycle. This is especially useful when refreshing and sending a token to the server in the onNewToken
method, ensuring the coroutine scope is properly handled and preventing memory leaks by stopping tasks once the service is no longer active. To use this library, you should override
To use this library, add the dependency below to your module’s build.gradle.kts
file:
dependencies { | |
implementation("com.github.skydoves:firebase-messaging-lifecycle-ktx:0.2.0") | |
} |
Now, you can simply use LifecycleAwareFirebaseMessagingService
instead of FirebaseMessagingService
. This lifecycle-aware version is designed to manage tasks in alignment with the service’s lifecycle. For example, in the onNewToken
method, you can send a token to your backend using the lifecycleScope.launch
function. This ensures that the coroutine scope is automatically canceled when the service’s lifecycle changes, preventing any unintended background tasks from continuing to run.
class AppFirebaseMessagingService : LifecycleAwareFirebaseMessagingService() { | |
override fun onNewToken(token: String) { | |
super.onNewToken(token) | |
lifecycleScope.launch { | |
// send the token to the server | |
.. | |
} | |
} | |
override fun onMessageReceived(message: RemoteMessage) { | |
super.onMessageReceived(message) | |
Log.d(APP_LOG_TAG, "FCMService#onMessageRec onMessageReceived: ${message.data}") | |
} | |
override fun onDestroy() { | |
super.onDestroy() | |
Log.d(APP_LOG_TAG, "FCMService#onDestroy onDestroy") | |
} | |
} |
As shown in the example above, LifecycleAwareFirebaseMessagingService
provides a lifecycleScope
, ensuring that any coroutine tasks launched within this scope are automatically canceled when the service’s lifecycle reaches the onDestroy
state. This helps prevent resource leaks and ensures proper task management.
Conclusion
In this article, you’ve explored the lifecycles of Service
and LifecycleService
, focusing on efficiently managing tasks, especially when running coroutine jobs. We also discussed how to apply a similar approach to FirebaseMessagingService
. Properly canceling tasks that no longer need to run is crucial for preventing memory leaks and avoiding unnecessary memory usage in Android components.
If you have any questions or feedback on this article, you can find the author on Twitter @github_skydoves or GitHub. If you’d like to stay updated with the latest information through articles and references, tips with code samples that demonstrate best practices, and news about the overall Android/Kotlin ecosystem, check out ‘Learn Kotlin and Android With Dove Letter’.
As always, happy coding!
— Jaewoong
This article is previously published on proandroiddev.com