> ## Documentation Index
> Fetch the complete documentation index at: https://docs.growthbook.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Kotlin (Android)

> Install the GrowthBook Kotlin SDK to evaluate feature flags and run A/B tests in Android apps using Android SDK 21 and above.

This SDK supports both Java and Kotlin Android apps using Android SDK 21 and above.

> [!NOTE]
>
> **Server/Backend Development?**
>
> If you're building JVM backend applications (servers, CLIs, workers), see the [Kotlin (JVM)](https://docs.growthbook.io/lib/kotlin-jvm.md) documentation instead, which provides JVM-optimized guidance without Android-specific dependencies.

kotlin

Kotlin SDK Resources

Get help on Slack

## Installation

```kotlin Gradle (Kotlin DSL) theme={null}
repositories {
    mavenCentral()
}

dependencies {
    implementation("io.growthbook.sdk:GrowthBook:7.1.0")

    // Choose ONE network dispatcher:
    implementation("io.growthbook.sdk:NetworkDispatcherKtor:1.0.12")
    // OR
    implementation("io.growthbook.sdk:NetworkDispatcherOkHttp:1.0.7")

    // Optional: JSON serialization helpers
    implementation("io.growthbook.sdk:GrowthBookKotlinxSerialization:1.0.0")
}
```

```groovy Gradle (Groovy DSL) theme={null}
repositories {
    mavenCentral()
}

dependencies {
    implementation 'io.growthbook.sdk:GrowthBook:7.1.0'

    // Choose ONE network dispatcher:
    implementation 'io.growthbook.sdk:NetworkDispatcherKtor:1.0.12'
    // OR
    implementation 'io.growthbook.sdk:NetworkDispatcherOkHttp:1.0.7'

    // Optional: JSON serialization helpers
    implementation 'io.growthbook.sdk:GrowthBookKotlinxSerialization:1.0.0'
}
```

### Network Dispatchers

The SDK requires a network dispatcher for fetching feature definitions:

* **`GBNetworkDispatcherKtor`** - Ktor-based implementation (Android-friendly default)
* **`GBNetworkDispatcherOkHttp`** - OkHttp-based implementation (recommended for most Android apps)

## Quick Start

To create a GrowthBook SDK instance, use `GBSDKBuilder`. The SDK uses coroutines, so initialize it from a coroutine scope.

```kotlin theme={null}
import com.sdk.growthbook.GBSDKBuilder
import com.sdk.growthbook.GrowthBookSDK
import com.sdk.growthbook.model.toGbBoolean
import com.sdk.growthbook.model.toGbString
import com.sdk.growthbook.network.GBNetworkDispatcherKtor
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {
    private lateinit var growthBook: GrowthBookSDK

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // User attributes for targeting and assigning users to experiment variations
        val attributes = mapOf(
            "id" to "user-123".toGbString(),
            "deviceType" to "Android".toGbString(),
            "appVersion" to "1.2.0".toGbString(),
            "isPremium" to true.toGbBoolean()
        )

        lifecycleScope.launch {
            growthBook = GBSDKBuilder(
                // Fetch and cache feature definitions from GrowthBook API
                apiKey = "sdk_abc123",
                apiHost = "https://cdn.growthbook.io/",
                attributes = attributes,
                trackingCallback = { experiment, result ->
                    // Track experiment views in your analytics system
                    analytics.track("experiment_viewed", mapOf(
                        "experiment_id" to experiment.key,
                        "variation_id" to result.variationId
                    ))
                },
                networkDispatcher = GBNetworkDispatcherKtor(),
                // Optional: encryption key if using encrypted features
                encryptionKey = "your_encryption_key_here"
            ).initialize()
        }
    }
}
```

### Configuration Options

The `GBSDKBuilder` accepts several configuration options:

* **apiKey** (`String`, required) - Your GrowthBook API key
* **apiHost** (`String`, required) - API host URL (typically `https://cdn.growthbook.io/`)
* **streamingHost** (`String`, optional) - Streaming host URL for SSE updates
* **attributes** (`Map<String, GBValue>`) - User attributes for targeting
* **trackingCallback** (`(GBExperiment, GBExperimentResult) -> Unit`) - Analytics tracking callback
* **networkDispatcher** (`NetworkDispatcher`, required) - Network implementation
* **encryptionKey** (`String`, optional) - Decryption key for encrypted features
* **cachingEnabled** (`Boolean`, default: true) - Enable/disable local feature caching
* **remoteEval** (`Boolean`, default: false) - Enable remote feature evaluation
* **enableLogging** (`Boolean`, default: false) - Print debug logs to stdout

The following options are set via builder methods rather than constructor parameters:

* `.setEnabled(Boolean)` - Enable/disable all experiments (default: true)
* `.setQAMode(Boolean)` - Disable randomization for QA testing (default: false)
* `.setForcedVariations(Map<String, Int>)` - Force specific variations for QA
* `.setRefreshHandler(GBCacheRefreshHandler)` - Callback fired when features are refreshed
* `.setCacheMaxAge(Long)` - Freshness window for cached features, in milliseconds (default: unset)
* `.setStickyBucketService(GBStickyBucketService)` - Enable sticky bucketing
* `.setFeatureUsageCallback(GBFeatureUsageCallback)` - Callback fired on every feature evaluation

### Updating User Attributes

You can update user attributes at any time using `setAttributes()`. This completely replaces the attributes object:

```kotlin theme={null}
// Update attributes when user logs in
growthBook.setAttributes(mapOf(
    "id" to userId.toGbString(),
    "isPremium" to true.toGbBoolean(),
    "country" to "US".toGbString(),
))
```

> [!WARNING]
>
> **Attribute Changes**
>
> Be aware that changing attributes may change the assigned feature values, which can be disorienting to users if not handled carefully.

### Feature Refresh Handler

To access features as soon as they're loaded from the backend, use `setRefreshHandler()`:

```kotlin theme={null}
class MainActivity : AppCompatActivity() {
    private var growthBookSDK: GrowthBookSDK? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleScope.launch {
            val builder = GBSDKBuilder(
                apiKey = "sdk_abc123",
                apiHost = "https://cdn.growthbook.io/",
                attributes = mapOf("id" to userId.toGbString()),
                networkDispatcher = GBNetworkDispatcherKtor(),
                trackingCallback = { _, _ -> }
            )

            builder.setRefreshHandler { isRefreshed, error ->
                if (isRefreshed) {
                    // Features are now loaded and ready to use
                    val feature = growthBookSDK?.feature("new-checkout-flow")
                    if (feature?.on == true) {
                        // Show new checkout flow
                    }
                } else {
                    Log.e("GrowthBook", "Failed to refresh features", error)
                }
            }

            growthBookSDK = builder.initialize()
        }
    }
}
```

## Evaluating Features

### Feature Result

The `feature()` method takes a feature key and returns a `GBFeatureResult` object with the following properties:

* **gbValue** (`GBValue`) - The assigned value of the feature (typed wrapper)
* **on** (`Boolean`) - The value cast to a boolean
* **off** (`Boolean`) - The value cast to a boolean and then negated
* **source** (`GBFeatureSource`) - Why the value was assigned: `unknownFeature`, `defaultValue`, `force`, `experiment`, `prerequisite`, `cyclicPrerequisite`, or `override`

When the source is `experiment`, there are additional properties:

* **experiment** (`GBExperiment`) - The experiment configuration
* **experimentResult** (`GBExperimentResult`) - The experiment evaluation result

### Basic Usage

```kotlin theme={null}
// Boolean feature flag
val newCheckoutEnabled = growthBook.feature("new-checkout-flow").on
if (newCheckoutEnabled) {
    showNewCheckout()
} else {
    showLegacyCheckout()
}

// String feature
val buttonColor = growthBook.feature("button-color").gbValue as? GBString

// Numeric feature
val maxRetries = growthBook.feature("max-retries").gbValue as? GBNumber

// Complex JSON feature
val config = growthBook.feature("service-config").gbValue as? GBJson
```

### Working with GBValue

Starting with version 2.0.0, feature values use the `GBValue` type for better type safety:

```kotlin theme={null}
val featureResult = growthBook.feature("my-feature")

// Access the typed value
when (val gbValue = featureResult.gbValue) {
    is GBBoolean -> {
        val boolValue = gbValue.value
        println("Boolean: $boolValue")
    }
    is GBString -> {
        val stringValue = gbValue.value
        println("String: $stringValue")
    }
    is GBNumber -> {
        val numberValue = gbValue.value // Double
        println("Number: $numberValue")
    }
    is GBArray -> {
        println("GbArray: $gbValue")
    }
    is GBJson -> {
        println("JSON: $gbValue")
    }
    is GBNull -> {
        println("Null value")
    }
    is GBValue.Unknown -> {
        println("GBValue.Unknown")
    }
}
```

### Typed Feature Access

For convenience, you can directly get a typed feature value using `featureValue<T>()` (available in version 7.1.0+):

```kotlin theme={null}
// Get feature value with inferred type
val maxRetries: Int? = growthBook.featureValue<Int>("max-retries")
val buttonColor: String? = growthBook.featureValue<String>("button-color")
val isEnabled: Boolean? = growthBook.featureValue<Boolean>("new-feature-enabled")

// Use with default values
val timeout = growthBook.featureValue<Long>("api-timeout") ?: 5000L
```

### Feature Source and Experiments

Check why a feature value was assigned and access experiment details:

```kotlin theme={null}
val feature = growthBook.feature("premium-feature")

println("Feature value: ${feature.gbValue}")
println("Is on: ${feature.on}")
println("Source: ${feature.source}")

// If the feature value came from an experiment
if (feature.source == GBFeatureSource.experiment) {
    val experiment = feature.experiment
    val result = feature.experimentResult

    println("Experiment key: ${experiment?.key}")
    println("Variation ID: ${result?.variationId}")
    println("In experiment: ${result?.inExperiment}")
}
```

## Running Inline Experiments

You can run experiments directly without defining them in the GrowthBook API. This is useful for programmatic experiments:

```kotlin theme={null}
val experiment = GBExperiment(
    key = "button-color-test"
    variations = listOf(
        "blue", "red", "green"
    ).map { it.toGbString() },
    weights = listOf(0.5f, 0.3f, 0.2f),
)

val result = growthBook.run(experiment)

// Get the assigned variation
val color = result.value as GBString // "blue", "red", or "green"
println("Assigned color: $color")

if (result.inExperiment) {
    // User is in the experiment
    applyButtonColor(color)
}
```

### Experiment Configuration

The `GBExperiment` class accepts the following properties:

**Required:**

* **key** (`String`) - The unique identifier for this experiment
* **variations** (`Array<Any>`) - Array of variations to choose between

**Optional:**

* **weights** (`FloatArray`) - Traffic distribution across variations (must sum to 1.0)
* **active** (`Boolean`, default: true) - If false, always return control (first variation)
* **coverage** (`Float`, default: 1.0) - Percentage of users to include (0.0 to 1.0)
* **condition** (`GBCondition`) - Targeting conditions for the experiment
* **namespace** (`GBNamespace`) - Namespace for experiment isolation
* **force** (`Int`) - Force all users to a specific variation index (for QA)
* **hashAttribute** (`String`, default: "id") - User attribute for variation assignment

### Experiment Result

The `GBExperimentResult` object contains:

* **inExperiment** (`Boolean`) - Whether the user is in the experiment
* **variationId** (`Int`) - The index of the assigned variation
* **value** (`Any`) - The value of the assigned variation
* **hashAttribute** (`String`) - The attribute used for hashing
* **hashValue** (`String`) - The value of the hash attribute
* **key** (`String`) - The experiment key
* **bucket** (`Float`) - The hash bucket value (0.0 to 1.0)
* **stickyBucketUsed** (`Boolean`) - Whether sticky bucketing was used

### Advanced Experiment Example

```kotlin theme={null}
// Complex experiment with targeting and custom weights
val pricingExperiment = GBExperiment(
    key = "pricing-test",
    variations = listOf(9.99, 14.99, 19.99)
        .map { it.toGbNumber() },
    hashAttribute = "userId" // Use custom attribute for hashing
).apply {
    weights = listOf(0.5f, 0.3f, 0.2f)
    coverage = 0.8f // Only 80% of users

    // Only target premium users in the US
    condition = JsonObject(mapOf(
        "country" to JsonPrimitive("US"),
        "plan" to JsonPrimitive("premium"),
    ))
}

val result = growthBook.run(pricingExperiment)

if (result.inExperiment) {
    val price = result.value as GBNumber
    displayPrice(price)

    // Track the experiment view
    analytics.track("pricing_experiment_viewed", mapOf(
        "price" to price,
        "variation_id" to result.variationId
    ))
}
```

## Sticky Bucketing

Sticky Bucketing ensures users see consistent experiment variations even when targeting conditions or user attributes change. This prevents jarring user experiences from switching variations mid-experiment.

### How It Works

When sticky bucketing is enabled, the SDK persists experiment assignments in local storage. The next time the user is evaluated for that experiment, they'll get the same variation they saw before.

### Implementation

Implement the `GBStickyBucketService` interface to enable sticky bucketing:

```kotlin theme={null}
import com.sdk.growthbook.stickybucket.GBStickyBucketService
import com.sdk.growthbook.utils.GBStickyAssignmentsDocument
import android.content.SharedPreferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString

class StickyBucketServiceImpl(
    override val coroutineScope: CoroutineScope,
    private val preferences: SharedPreferences,
    private val prefix: String = "gb_sticky_"
) : GBStickyBucketService {

    override suspend fun getAssignments(
        attributeName: String,
        attributeValue: String
    ): GBStickyAssignmentsDocument? {
        val key = "$prefix${attributeName}||$attributeValue"
        val json = preferences.getString(key, null) ?: return null

        return try {
            Json.decodeFromString<GBStickyAssignmentsDocument>(json)
        } catch (e: Exception) {
            null
        }
    }

    override suspend fun saveAssignments(doc: GBStickyAssignmentsDocument) {
        val key = "$prefix${doc.attributeName}||${doc.attributeValue}"
        val json = Json.encodeToString(doc)

        preferences.edit()
            .putString(key, json)
            .apply()
    }

    override suspend fun getAllAssignments(
        attributes: Map<String, String>
    ): Map<String, GBStickyAssignmentsDocument> {
        val docs = mutableMapOf<String, GBStickyAssignmentsDocument>()

        attributes.forEach { (attrName, attrValue) ->
            getAssignments(attrName, attrValue)?.let { doc ->
                val docKey = "${doc.attributeName}||${doc.attributeValue}"
                docs[docKey] = doc
            }
        }

        return docs
    }
}
```

### Using Sticky Bucketing

Pass your sticky bucket service implementation when building the SDK:

```kotlin theme={null}
val preferences = context.getSharedPreferences("growthbook_prefs", Context.MODE_PRIVATE)

val stickyBucketService = StickyBucketServiceImpl(
    coroutineScope = lifecycleScope,
    preferences = preferences
)

val growthBook = GBSDKBuilder(
    apiKey = "sdk_abc123",
    apiHost = "https://cdn.growthbook.io/",
    attributes = mapOf("id" to userId.toGbString()),
    networkDispatcher = GBNetworkDispatcherKtor(),
    trackingCallback = { _, _ -> }
)
    .setStickyBucketService(stickyBucketService)
    .initialize()
```

> [!NOTE]
>
> **Sticky Bucket Documents**
>
> Each sticky bucket document contains:
>
> * `attributeName` - The attribute used to identify the user (e.g., "id", "deviceId")
> * `attributeValue` - The value of that attribute (e.g., "user-123")
> * `assignments` - A map of experiment assignments (e.g., `{"exp1__0": "control"}`)

## Automatic Features Refresh

The GrowthBook SDK supports automatic feature refresh through multiple mechanisms to ensure your app always has the latest feature definitions.

### Server-Sent Events (SSE)

Server-Sent Events provide real-time feature updates without polling. When enabled, the SDK maintains a persistent connection to receive live updates whenever features change.

```kotlin theme={null}
val growthBook = gbSdkBuilder.initialize()
val flow = growthBook.startAutoRefreshFeatures()
flow.launchIn(lifecycleScope)
```

### Manual Refresh

You can manually trigger feature refresh at any time:

```kotlin theme={null}
lifecycleScope.launch {
    growthBook.refreshCache()
    Log.d("GrowthBook", "Feature refresh requested")
}
```

### Cache Configuration

#### Cache freshness (TTL)

By default the SDK refetches features on every fetch. `setCacheMaxAge()` sets a freshness window, in milliseconds: while the cache is younger than that, the network call is skipped and the cached features are served instead.

```kotlin theme={null}
val growthBook = GBSDKBuilder(
    apiKey = "sdk_abc123",
    apiHost = "https://cdn.growthbook.io/",
    attributes = attributes,
    networkDispatcher = GBNetworkDispatcherKtor(),
    trackingCallback = { _, _ -> },
)
    // Consider the cache stale after 10 minutes
    .setCacheMaxAge(10 * 60 * 1000L)
    .initialize()
```

This is a staleness gate evaluated on the next fetch, not a background polling mechanism.

> [!NOTE]
>
> **`refreshCache()` ignores the freshness window**
>
> `growthBook.refreshCache()` is an explicit refresh and always hits the network, even when the cache is still within its max age. Don't drive it from a timer to emulate a TTL — that defeats the freshness window entirely.

#### Disabling the cache

By default, caching is enabled. You can disable it via the `cachingEnabled` constructor parameter:

```kotlin theme={null}
val growthBook = GBSDKBuilder(
    apiKey = "sdk_abc123",
    apiHost = "https://cdn.growthbook.io/",
    attributes = attributes,
    networkDispatcher = GBNetworkDispatcherKtor(),
    trackingCallback = { _, _ -> },
    cachingEnabled = false // disable local cache
).initialize()
```

#### Refresh notifications

To be notified when features are refreshed from the server, use `setRefreshHandler()` on the builder:

```kotlin theme={null}
val builder = GBSDKBuilder(
    apiKey = "sdk_abc123",
    apiHost = "https://cdn.growthbook.io/",
    attributes = attributes,
    networkDispatcher = GBNetworkDispatcherKtor(),
    trackingCallback = { _, _ -> }
)

builder.setRefreshHandler { isRefreshed, error ->
    if (isRefreshed) {
        Log.d("GrowthBook", "Features refreshed successfully")
    } else {
        Log.w("GrowthBook", "Feature refresh failed: ${error?.errorMessage}")
    }
}

val growthBook = builder.initialize()
```

## GrowthBook Event Tracking

Sends experiment view and feature usage events to GrowthBook's [Managed Warehouse](https://docs.growthbook.io/app/managed-warehouse.md) or [Event Forwarder](https://docs.growthbook.io/app/event-forwarder.md).

Requires the GrowthBook Kotlin SDK v7.8.0 or later, with `NetworkDispatcherKtor` v1.2.0+ or `NetworkDispatcherOkHttp` v1.1.0+.

```kotlin theme={null}
import com.sdk.growthbook.GBSDKBuilder
import com.sdk.growthbook.network.GBNetworkDispatcherKtor
import com.sdk.growthbook.plugin.TrackingPluginConfig
import com.sdk.growthbook.plugin.tracking.GrowthBookTrackingPlugin

val networkDispatcher = GBNetworkDispatcherKtor()

val growthBook = GBSDKBuilder(
    apiKey = "YOUR_CLIENT_KEY",
    apiHost = "https://cdn.growthbook.io/",
    attributes = attributes,
    networkDispatcher = networkDispatcher,
    trackingCallback = { _, _ -> },
)
    .setPlugins(
        listOf(
            GrowthBookTrackingPlugin(
                TrackingPluginConfig(
                    clientKey = "YOUR_CLIENT_KEY",
                    networkDispatcher = networkDispatcher,
                    ingestorHost = "https://us-east-1.gb-ingest.com",
                )
            )
        )
    )
    .initialize()
```

The plugin doesn't inherit the builder's client key or network dispatcher, so pass both to `TrackingPluginConfig`. Without a network dispatcher, no events are sent.

Custom events aren't supported yet. Send them with the [Ingestion API](https://docs.growthbook.io/app/managed-warehouse.md#ingestion-api).

| Option | Description |
| - | - |
| `ingestorHost` | Ingestor for your Data Region. |
| `batchSize` | Events to queue before sending. Default `100`. |
| `batchTimeout` | How long to wait before sending a partial batch. Default `10.seconds`. |

## Experiment Tracking and Feature Usage Callbacks

The SDK provides comprehensive tracking capabilities for experiments and feature usage.

### Experiment Tracking Callback

The tracking callback is called whenever a user is included in an experiment. This is essential for analytics and experiment analysis.

```kotlin theme={null}
val growthBook = GBSDKBuilder(
    apiKey = "sdk_abc123",
    apiHost = "https://cdn.growthbook.io/",
    attributes = mapOf("id" to userId.toGbString()),
    networkDispatcher = GBNetworkDispatcherKtor(),

    // Experiment tracking callback
    trackingCallback = { experiment, result ->
        // Send to your analytics system
        analytics.track("experiment_viewed", mapOf(
            "experiment_id" to experiment.key,
            "variation_id" to result.variationId,
            "variation_value" to result.value,
            "user_id" to userId,
            "in_experiment" to result.inExperiment,
            "hash_used" to result.hashUsed
        ))

        // Optional: Track in your own analytics
        firebaseAnalytics.logEvent("experiment_viewed") {
            param("experiment_id", experiment.key)
            param("variation_id", result.variationId.toString())
        }
    }
).initialize()
```

> [!NOTE]
>
> **Tracking Timing**
>
> The tracking callback is only called when the user is actually included in the experiment. If they're not in the experiment, this callback won't be triggered.

### Feature Usage Callback

The feature usage callback is called every time a feature is evaluated, regardless of whether it's part of an experiment or not.

```kotlin theme={null}
val growthBook = GBSDKBuilder(
    apiKey = "sdk_abc123",
    apiHost = "https://cdn.growthbook.io/",
    attributes = mapOf("id" to userId.toGbString()),
    networkDispatcher = GBNetworkDispatcherKtor(),
    trackingCallback = { _, _ -> }
)
    .setFeatureUsageCallback { featureKey, result ->
        // Track feature usage for monitoring and debugging
        Log.d("GrowthBook", "Feature evaluated: $featureKey = ${result.gbValue}")

        // Send to monitoring systems
        monitoring.trackFeatureUsage(featureKey, result.gbValue)

        // Optional: Track in analytics
        analytics.track("feature_used", mapOf(
            "feature_key" to featureKey,
            "feature_value" to result.gbValue.toString(),
            "source" to result.source.name,
            "user_id" to userId
        ))
    }
    .initialize()
```

### Advanced Tracking Example

Combine both callbacks for comprehensive tracking:

```kotlin theme={null}
class GrowthBookManager {
    private val analytics: AnalyticsService
    private val monitoring: MonitoringService

    fun initializeGrowthBook(userId: String): GrowthBookSDK {
        return GBSDKBuilder(
            apiKey = "sdk_abc123",
            apiHost = "https://cdn.growthbook.io/",
            attributes = mapOf("id" to userId.toGbString()),
            networkDispatcher = GBNetworkDispatcherKtor(),
            trackingCallback = { experiment, result ->
                trackExperiment(experiment, result, userId)
            }
        )
            .setFeatureUsageCallback { featureKey, result ->
                trackFeatureUsage(featureKey, result, userId)
            }
            .initialize()
    }

    private fun trackExperiment(experiment: GBExperiment, result: GBExperimentResult, userId: String) {
        // Send to multiple analytics platforms
        analytics.track("experiment_viewed", mapOf(
            "experiment_id" to experiment.key,
            "variation_id" to result.variationId,
            "user_id" to userId
        ))

        // Track in monitoring for alerting
        monitoring.recordExperimentView(experiment.key, result.variationId)
    }

    private fun trackFeatureUsage(featureKey: String, result: GBFeatureResult, userId: String) {
        // Monitor feature usage patterns
        monitoring.recordFeatureUsage(featureKey, result.gbValue)

        // Track feature performance
        if (result.source == GBFeatureSource.experiment) {
            analytics.track("feature_experiment_used", mapOf(
                "feature_key" to featureKey,
                "experiment_id" to result.experiment?.key,
                "user_id" to userId
            ))
        }
    }
}
```

## Encrypted Features and Secure Attributes

GrowthBook supports encryption for sensitive feature definitions and secure attribute hashing for privacy protection.

### Encrypted Features

Encrypted features ensure that sensitive feature configurations never reach the client in plain text.

#### Setup Encrypted Features

1. **Enable encryption in GrowthBook**: Go to your SDK Connection settings and enable "Encrypt SDK Payload"
2. **Get your encryption key**: Copy the encryption key from the SDK Connection settings
3. **Configure the SDK**:

```kotlin theme={null}
val growthBook = GBSDKBuilder(
    apiKey = "sdk_abc123",
    apiHost = "https://cdn.growthbook.io/",
    attributes = mapOf("id" to userId.toGbString()),
    networkDispatcher = GBNetworkDispatcherKtor(),
    trackingCallback = { _, _ -> },

    // Provide your encryption key
    encryptionKey = "your_encryption_key_here"
).initialize()
```

#### Working with Encrypted Features

```kotlin theme={null}
// The SDK automatically decrypts features when evaluating
val encryptedFeature = growthBook.feature("sensitive-feature")
val value = encryptedFeature.gbValue

// You can check if encryption is working by looking at the source
if (encryptedFeature.source == GBFeatureSource.defaultValue) {
    Log.d("GrowthBook", "Feature decrypted successfully")
}
```

> [!WARNING]
>
> **Security Best Practices**
>
> * Store encryption keys securely (use Android Keystore or environment variables)
> * Never commit encryption keys to version control
> * Rotate encryption keys regularly
> * Use different keys for different environments

### Secure Attributes

Secure attributes allow you to target users based on sensitive information without exposing that information to the client.

#### Setup Secure Attributes

1. **Enable secure attribute hashing** in your SDK Connection settings
2. **Hash sensitive attributes** before passing them to the SDK:

```kotlin theme={null}
import java.security.MessageDigest
import java.nio.charset.StandardCharsets

class SecureAttributeManager {
    private val salt = "your_organization_salt" // From Organization Settings

    fun hashSecureAttribute(value: String): String {
        val input = value + salt
        val digest = MessageDigest.getInstance("SHA-256")
        val hash = digest.digest(input.toByteArray(StandardCharsets.UTF_8))
        return hash.joinToString("") { "%02x".format(it) }
    }

    fun createSecureAttributes(user: User): Map<String, GBValue> {
        return mapOf(
            "id" to user.id.toGbString(),
            "email" to hashSecureAttribute(user.email).toGbString(), // Hash sensitive email
            "phone" to hashSecureAttribute(user.phone).toGbString(), // Hash sensitive phone
            "country" to user.country.toGbString(), // Non-sensitive attributes remain plain
            "plan" to user.plan.toGbString()
        )
    }
}
```

#### Using Secure Attributes

```kotlin theme={null}
val secureManager = SecureAttributeManager()

// Hash sensitive attributes before setting them
val secureAttributes = secureManager.createSecureAttributes(currentUser)

val growthBook = GBSDKBuilder(
    apiKey = "sdk_abc123",
    apiHost = "https://cdn.growthbook.io/",
    attributes = secureAttributes,
    networkDispatcher = GBNetworkDispatcherKtor(),
    trackingCallback = { _, _ -> }
).initialize()

// The SDK will use hashed attributes for targeting
val targetedFeature = growthBook.feature("premium-feature")
```

#### Advanced Secure Attribute Example

```kotlin theme={null}
class UserAttributeManager {
    private val salt = BuildConfig.GROWTHBOOK_SECURE_ATTRIBUTE_SALT

    private fun sha256(input: String): String {
        val digest = MessageDigest.getInstance("SHA-256")
        val hash = digest.digest(input.toByteArray(StandardCharsets.UTF_8))
        return hash.joinToString("") { "%02x".format(it) }
    }

    fun buildUserAttributes(user: User): Map<String, GBValue> {
        val attributes = mutableMapOf<String, GBValue>()

        // Non-sensitive attributes
        attributes["id"] = user.id.toGbString()
        attributes["country"] = user.country.toGbString()
        attributes["plan"] = user.plan.toGbString()
        attributes["signupDate"] = user.signupDate.toGbString()

        // Hash sensitive attributes
        if (user.email.isNotEmpty()) {
            attributes["email"] = sha256(user.email + salt).toGbString()
        }
        if (user.phone.isNotEmpty()) {
            attributes["phone"] = sha256(user.phone + salt).toGbString()
        }

        // Hash PII for targeting
        attributes["userSegment"] = sha256("${user.id}_${user.plan}" + salt).toGbString()

        return attributes
    }
}

// Usage in your app
val attributeManager = UserAttributeManager()
val attributes = attributeManager.buildUserAttributes(currentUser)

growthBook.setAttributes(attributes)
```

### Custom Attributes

You can define custom attributes for advanced targeting scenarios:

```kotlin theme={null}
import com.sdk.growthbook.model.GBArray

val customAttributes = mapOf(
    // Standard attributes
    "id" to userId.toGbString(),
    "country" to "US".toGbString(),

    // Custom business attributes
    "lifetimeValue" to userLifetimeValue.toGbNumber(),
    "lastPurchaseDate" to lastPurchaseTimestamp.toGbString(),
    "preferredCategory" to userPreferences.category.toGbString(),
    "deviceModel" to Build.MODEL.toGbString(),
    "appVersion" to BuildConfig.VERSION_NAME.toGbString(),

    // Computed attributes
    "isHighValueCustomer" to (userLifetimeValue > 1000).toGbBoolean(),
    "daysSinceLastPurchase" to daysSinceLastPurchase.toGbNumber(),

    // Array attributes for complex targeting
    "purchasedCategories" to GBArray(listOf("electronics", "books", "clothing").map { it.toGbString() }),
    "subscriptionFeatures" to GBArray(userSubscriptions.features.map { it.toGbString() })
)

val growthBook = GBSDKBuilder(
    apiKey = "sdk_abc123",
    apiHost = "https://cdn.growthbook.io/",
    attributes = customAttributes,
    networkDispatcher = GBNetworkDispatcherKtor(),
    trackingCallback = { _, _ -> }
).initialize()
```

## Remote Evaluation

> [!TIP]
>
> See the [Remote Evaluation overview](https://docs.growthbook.io/self-host/remote-evaluation.md) for more information about what Remote Evaluation is, how it works, and deployment options.

Remote Evaluation evaluates feature flags on a secure server instead of the client, ensuring sensitive targeting rules and unused variations never reach the client.

### When to Use

Use Remote Evaluation when you need to:

* Keep targeting rules private
* Hide unused feature variations
* Prevent users from seeing all possible values
* Add an extra layer of security

> [!WARNING]
>
> **Backend Required**
>
> Remote Evaluation requires either:
>
> * GrowthBook Cloud with Remote Evaluation enabled
> * A self-hosted GrowthBook Proxy Server
> * A custom remote evaluation backend

### Setup

Enable Remote Evaluation in your SDK Connection settings in GrowthBook, then configure the SDK:

```kotlin theme={null}
val growthBook = GBSDKBuilder(
    apiKey = "sdk_abc123",
    apiHost = "https://cdn.growthbook.io/",
    attributes = mapOf("id" to userId.toGbString()),
    networkDispatcher = GBNetworkDispatcherKtor(),
    trackingCallback = { _, _ -> },
    remoteEval = true // Enable remote evaluation
).initialize()
```

When Remote Evaluation is enabled, the SDK will make an API call to evaluate features whenever user attributes change.

> [!NOTE]
>
> **Sticky Bucketing with Remote Evaluation**
>
> If using Sticky Bucketing with Remote Evaluation, configure sticky bucketing on your remote evaluation backend. You don't need to provide a `StickyBucketService` to the client SDK.

## Serialization Support

The optional `GrowthBookKotlinxSerialization` module provides helpers for working with complex feature values using kotlinx.serialization.

### Installation

```kotlin theme={null}
dependencies {
    implementation("io.growthbook.sdk:GrowthBookKotlinxSerialization:1.0.0")
}
```

### Usage

Define your data classes and use the serialization helpers:

```kotlin theme={null}
import kotlinx.serialization.Serializable
import com.sdk.growthbook.serialization.*

@Serializable
data class AppConfig(
    val apiTimeout: Long,
    val maxRetries: Int,
    val enabledFeatures: List<String>,
    val themeColors: Map<String, String>
)

// Get a complex feature value as a typed object
val configFeature = growthBook.feature("app-config")
val config: AppConfig? = configFeature.gbValue.decodeAs<AppConfig>()

config?.let {
    println("API Timeout: ${it.apiTimeout}")
    println("Max Retries: ${it.maxRetries}")
    println("Enabled Features: ${it.enabledFeatures.joinToString()}")

    // Use the configuration in your app
    setupApiClient(timeout = it.apiTimeout, retries = it.maxRetries)
}
```

> [!NOTE]
>
> **When to Use**
>
> You only need the serialization module if you work with complex JSON feature values and want type-safe deserialization. For simple boolean, string, and number features, you can skip this dependency.

## ProGuard Configuration (Android)

If you use ProGuard or R8 for code shrinking and obfuscation, add these rules to your `proguard-rules.pro`:

```proguard theme={null}
# Core GrowthBook SDK
-keep class com.sdk.growthbook.** { *; }

# Kotlinx Serialization
-keep class kotlinx.serialization.json.** { *; }
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.SerializationKt

# Keep serializers
-keep,includedescriptorclasses class com.sdk.growthbook.**$$serializer { *; }

# Keep companion objects
-keepclassmembers class com.sdk.growthbook.** {
    *** Companion;
}

# Keep classes with KSerializer
-keepclasseswithmembers class com.sdk.growthbook.** {
    kotlinx.serialization.KSerializer serializer(...);
}
```

> [!WARNING]
>
> **Important**
>
> These are baseline rules. Depending on your app's configuration and the features you use, you may need additional rules. Test thoroughly with ProGuard enabled.

## Version History and Breaking Changes

The Kotlin SDK has undergone several major version updates with breaking changes:

### Recent Versions

* **v1.1.63** (2024-11-26) - Changed `value` field type to `kotlinx.serialization.json.JsonElement`
* **v2.0.0** (2025-01-10) - Renamed `value` to `gbValue` with `GBValue` type; added typed `feature<T>()` method
* **v3.0.0** (2025-01-27) - Changed user attributes to use `GBValue` types
* **v4.0.0** (2025-03-03) - Changed `initialize()` to suspend method
* **v5.0.0** (2025-05-22) - Moved GBValue to Core module
* **v6.0.0** (2025-05-22) - Renamed `hostURL` to `apiHost`, added `streamingHost`
* **v6.1.0** (2025-08-15) - Changed `GBStickyBucketService` methods to suspend, added `coroutineScope`
* **v7.1.0** (2026-04-07) - Added `featureValue<T>()` and hid reified typed APIs from Objective-C

### Migration Guidance

When upgrading between major versions, review the changelog on the [GitHub repository](https://github.com/growthbook/growthbook-kotlin) for detailed migration instructions.

## Further Reading

The GitHub repository contains comprehensive documentation including:

* Detailed API reference
* Advanced usage examples
* Integration guides
* Contributing guidelines

**GitHub Repository:** [https://github.com/growthbook/growthbook-kotlin](https://github.com/growthbook/growthbook-kotlin)

## Supported Features
