Skip to content

Commit

Permalink
Attach lifecycle listeners internally in initialize method (#131)
Browse files Browse the repository at this point in the history
* Internalize the lifecycle listener attachment.
* Mark the public property to be deprecated and replace with a no-op implementation to avoid duplicate listeners.
* Update readme instructions, migration guide.

---------

Co-authored-by: Evan Masseau <>
  • Loading branch information
evan-masseau authored Feb 20, 2024
1 parent 1500879 commit e9598b0
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 22 deletions.
11 changes: 11 additions & 0 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@
This document provides guidance on how to migrate from the old version of the SDK to a newer version.
It will be updated as new versions are released including deprecations or breaking changes.

## 2.1.0 Deprecations
#### Deprecated `Klaviyo.lifecycleCallbacks`
In an effort to reduce setup code required to integrate the Klaviyo Android SDK, we have deprecated the public property
`Klaviyo.lifecycleCallbacks` and will now register for lifecycle callbacks automatically upon `initialize`.
It is no longer required to have this line in your `Application.onCreate()` method:
```kotlin
registerActivityLifecycleCallbacks(Klaviyo.lifecycleCallbacks)
```
For version 2.1.x, `Klaviyo.lifecycleCallbacks` has been replaced with a no-op implementation to avoid duplicative
listeners, and will be removed altogether in the next major release.

## 2.0.0 Breaking Changes
#### Removed `EventType` in favor of `EventMetric`.
The reasoning is explained below, see [1.4.0 Deprecations](#140-deprecations) for details and code samples.
Expand Down
17 changes: 6 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,12 @@ send them timely push notifications via [FCM (Firebase Cloud Messaging)](https:/
The SDK must be initialized with the short alphanumeric
[public API key](https://help.klaviyo.com/hc/en-us/articles/115005062267#difference-between-public-and-private-api-keys1)
for your Klaviyo account, also known as your Site ID. We require access to the `applicationContext` so the
SDK can be responsive to changes in network conditions and persist data via
`SharedPreferences`. You must also register the Klaviyo SDK for activity lifecycle
callbacks per the example code, so we can gracefully manage background processes.
SDK can be responsive to changes in application state and network conditions, and access `SharedPreferences` to
persist data. Upon initialize, the SDK registers listeners for your application's activity lifecycle callbacks,
to gracefully manage background processes.

`Klaviyo.initialize()` **must** be called before any other SDK methods can be invoked. We recommend initializing from
the earliest point in your application code, such as the `Application.onCreate()` method.

```kotlin
// Application subclass
Expand All @@ -90,18 +93,10 @@ class YourApplication : Application() {

// Initialize is required before invoking any other Klaviyo SDK functionality
Klaviyo.initialize("KLAVIYO_PUBLIC_API_KEY", applicationContext)
// Required for the SDK to properly respond to lifecycle changes such as app backgrounding
registerActivityLifecycleCallbacks(Klaviyo.lifecycleCallbacks)
}
}
```

`Klaviyo.initialize()` **must** be called before any other SDK methods can be invoked.
Because we require lifecycle callbacks, it is necessary to subclass
[`Application`](https://developer.android.com/reference/android/app/Application)
to initialize and register callbacks in `Application.onCreate`.
## Profile Identification
The SDK provides methods to identify profiles via the
[Create Client Profile API](https://developers.klaviyo.com/en/reference/create_client_profile).
Expand Down
23 changes: 17 additions & 6 deletions sdk/analytics/src/main/java/com/klaviyo/analytics/Klaviyo.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.klaviyo.analytics

import android.app.Application
import android.app.Application.ActivityLifecycleCallbacks
import android.content.Context
import android.content.Intent
Expand All @@ -12,6 +13,8 @@ import com.klaviyo.analytics.networking.ApiClient
import com.klaviyo.analytics.networking.KlaviyoApiClient
import com.klaviyo.core.Registry
import com.klaviyo.core.config.Config
import com.klaviyo.core.config.LifecycleException
import com.klaviyo.core.lifecycle.NoOpLifecycleCallbacks
import com.klaviyo.core.safeApply
import com.klaviyo.core.safeCall

Expand All @@ -22,12 +25,14 @@ import com.klaviyo.core.safeCall
*/
object Klaviyo {

/**
* Klaviyo lifecycle monitor which must be attached by the parent application
* so that the SDK can respond to environment changes such as internet
* availability and application termination
*/
val lifecycleCallbacks: ActivityLifecycleCallbacks get() = Registry.lifecycleCallbacks
@Deprecated(
"""
Lifecycle callbacks are now handled internally by Klaviyo.initialize.
This property will be removed in the next major version.
""",
ReplaceWith("", "")
)
val lifecycleCallbacks: ActivityLifecycleCallbacks get() = NoOpLifecycleCallbacks

private val profileOperationQueue = ProfileOperationQueue()

Expand All @@ -50,6 +55,12 @@ object Klaviyo {
.applicationContext(applicationContext)
.build()
)

val application = applicationContext.applicationContext as? Application
application?.apply {
unregisterActivityLifecycleCallbacks(Registry.lifecycleCallbacks)
registerActivityLifecycleCallbacks(Registry.lifecycleCallbacks)
} ?: throw LifecycleException()
}

/**
Expand Down
16 changes: 13 additions & 3 deletions sdk/analytics/src/test/java/com/klaviyo/analytics/KlaviyoTest.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.klaviyo.analytics

import android.app.Application
import android.app.Application.ActivityLifecycleCallbacks
import android.content.Intent
import android.os.Bundle
Expand Down Expand Up @@ -95,30 +96,39 @@ internal class KlaviyoTest : BaseTest() {
}

@Test
fun `Klaviyo initializes properly creates new config service`() {
fun `initialize properly creates new config service and attaches lifecycle listeners`() {
val builderMock = mockk<Config.Builder>()
every { Registry.configBuilder } returns builderMock
every { builderMock.apiKey(any()) } returns builderMock
every { builderMock.applicationContext(any()) } returns builderMock
every { builderMock.build() } returns configMock

val mockApplication = mockk<Application>()
every { contextMock.applicationContext } returns mockApplication.also {
every { it.unregisterActivityLifecycleCallbacks(any()) } returns Unit
every { it.registerActivityLifecycleCallbacks(any()) } returns Unit
}

Klaviyo.initialize(
apiKey = API_KEY,
applicationContext = contextMock
)

val expectedListener = Registry.lifecycleCallbacks
verifyAll {
builderMock.apiKey(API_KEY)
builderMock.applicationContext(contextMock)
builderMock.build()
mockApplication.unregisterActivityLifecycleCallbacks(match { it == expectedListener })
mockApplication.registerActivityLifecycleCallbacks(match { it == expectedListener })
}
}

@Test
fun `Klaviyo makes core lifecycle callbacks service available`() {
fun `Klaviyo does not make core lifecycle callbacks service publicly available`() {
val mockLifecycleCallbacks = mockk<ActivityLifecycleCallbacks>()
every { Registry.lifecycleCallbacks } returns mockLifecycleCallbacks
assertEquals(mockLifecycleCallbacks, Klaviyo.lifecycleCallbacks)
assertNotEquals(mockLifecycleCallbacks, Klaviyo.lifecycleCallbacks)
}

private fun verifyProfileDebounced() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import com.klaviyo.core.Registry
import com.klaviyo.core.config.Config
import com.klaviyo.core.config.Log
import com.klaviyo.fixtures.BaseTest
import com.klaviyo.fixtures.Logger
import com.klaviyo.fixtures.LogFixture
import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
Expand All @@ -20,7 +20,7 @@ import org.junit.Test

internal class KlaviyoUninitializedTest {
companion object {
private val logger = spyk(Logger()).apply {
private val logger = spyk(LogFixture()).apply {
every { error(any(), any<Throwable>()) } answers {
println(firstArg<String>())
secondArg<Throwable>().printStackTrace()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ class MissingAPIKey : KlaviyoException("You must declare an API key for the Klav
*/
class MissingContext : KlaviyoException("You must add your application context to the Klaviyo SDK")

/**
* Exception that is thrown when the application context is missing from the config
*/
class LifecycleException : KlaviyoException(
"Failed to attach lifecycle listeners to the application"
)

/**
* Exception to throw when a permission is not declared for the application context
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.klaviyo.core.lifecycle

import android.app.Activity
import android.app.Application
import android.os.Bundle

/**
* A no-op implementation of ActivityLifecycleCallbacks
* to temporarily replace the public property Klaviyo.lifecycleCallbacks
* and prevent duplicate registration of the KlaviyoLifecycleMonitor
* until next major release when we can make the breaking change to remove that public property
*/
object NoOpLifecycleCallbacks : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
override fun onActivityStarted(activity: Activity) {}
override fun onActivityResumed(activity: Activity) {}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {}
}

0 comments on commit e9598b0

Please sign in to comment.