Nowadays, users have many installed applications. It means that users often interact with many applications during a short amount of time. For example, when the user interacts with an application, receives a notification, then goes to another app.
However, as being developers, we would like to know when our application moved to the background. It’s a very popular use case for analytics. Different analytics solutions define sessions in different ways, some solutions allow developers to define “session” meaning. Let’s create a simple analytics abstraction which informs us about the duration of the session, which will be calculated in the following way: when we start the application, the session is also started and when the application moves to the background, our session is finished. Finally, we would like to display this information in logs.
The ProcessLifecycleOwner
can help us get information about changing the state of the application.
So, let's start with adding dependencies for lifecycle framework:
dependencies {
...
implementation 'androidx.lifecycle:lifecycle-process:2.6.1'
}
Let's start with creating an ApplicationObserver
class which helps us to handle the changing state of the application and configure our analytics solution in a proper way. So we want to know when our application moved to the background and to the foreground. We can use lifecycle event for it:
- when application moved to background
ON_STOP
event will be triggered; - when application moved to foreground
ON_START
event will be triggered.
We can create an ApplicationObserver
class which simplifies defining sessions for the analytics.
class ApplicationObserver(val analytics: Analytics) : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
}
override fun onStop(owner: LifecycleOwner) {
}
}
The next step is attaching the ApplicationObserver
to our application. We should register this observer in the application class. In my case, it's a MapNotesApp class. So, we should register it inside onCreate method. We can use the following code for initialization of ProcessLifecycleOwner
and registering ApplicationObserver
component.
class MapNotesApp : Application() {
override fun onCreate() {
super.onCreate()
...
val analytics = Analytics()
analytics.addReporter(LogReporter())
ProcessLifecycleOwner
.get()
.lifecycle
.addObserver(ApplicationObserver(analytics))
}
...
}
At the moment we can move to creating an abstraction for the analytics. I suggest to create a simple solution which can be extended, but even this implementation will be enough for demo purpose.
The Analytics
class allows us to collect session information.
class Analytics {
private var startSessionTimestamp: Long = -1
private val reporters = mutableListOf<AnalyticsReporter>()
fun addReporter(reporter: AnalyticsReporter) {
reporters.add(reporter)
}
fun startSession() {
startSessionTimestamp = Date().time
}
fun stopSession() {
reportSession()
sendAllEvents()
startSessionTimestamp = -1
}
private fun reportSession() {
reporters.forEach {reporter ->
val currentTime = Date().time
// we should check if session was started and stopped correctly
val sessionTime = (currentTime - startSessionTimestamp) / 1000
reporter.report("Session time: $sessionTime sec" )
}
}
private fun sendAllEvents() {
reporters.forEach {reporter ->
reporter.sendAllEvents()
}
}
}
The AnalyticsReporter
interface is an abstraction for all reporters, like LogReporter.
interface AnalyticsReporter {
fun report(event: String)
fun sendAllEvents()
}
The LogReposter
is implementation for the AnalyticsReporter
interface. It displays all logs in the console.
class LogReporter : AnalyticsReporter {
private val events = mutableListOf<String>()
override fun report(event: String) {
events.add(event)
}
override fun sendAllEvents() {
events.forEach { event ->
Log.d(this.javaClass.simpleName, event)
}
events.clear()
}
}
Finally, we can update the ApplicationObserver
class for starting and stopping the session.
class ApplicationObserver(val analytics: Analytics) : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
analytics.startSession()
}
override fun onStop(owner: LifecycleOwner) {
super.onStop(owner)
analytics.stopSession()
}
}
Afterwards, we can run our application and check the log. When the application moves to the foreground, our session starts, but when the application moves to the background, we finish our session with small delay and display this information in logs.
com.alex.mapnotes D/LogAnalyticsReporter: Session time: 28 sec
The main reason for this delay is the internal implementation of ProcessLifecycleOwner
and we will talk about it in a moment.
So, let’s move between different screens because the duration of our session is independent on the current activity. As you can see, we can easily handle case when the application moves from foreground and background and vice versa. Analytics it just one of the example when we need this information.
Let's check how ProcessLifecycleOwner
works under the hood. Everything starts when we add the "lifecycle-process" dependency to the project. After building it, the ProcessLifecycleInitializer
will be added to AndroidManifest.xml as a provider which allows the framework to collect the information about changing states of the activities. The main helper here is ReportFragment, which sends information about changes in the lifecycle of each activity to LifecycleRegistry class. That is a hub for collecting information about the lifecycle and it can notify an interested observer about changes in the lifecycle of LifecycleOwners. However, the framework has one trick that is a delay and it is used for verification that no new activity was created, and this delay is equal to 700 milliseconds.