Kotlin

This SDK is designed for Kotlin applications, including Android and Kotlin Multiplatform. It uses the remote evaluation paradigm, which means that the context is sent to the Tggl API and flag values are returned pre-evaluated by the server. This way you don't expose your flag evaluation logic to the client.

Info

Looking for Java? It is the same SDK, but we have a dedicated documentation for it!

Installation

Add the dependency to your project:

Gradle (Kotlin DSL)

implementation("io.tggl:tggl-client:1.0.0")

Gradle (Groovy)

implementation 'io.tggl:tggl-client:1.0.0'
Info

The SDK requires Java 8 or later.

Quick start

Create a TgglRemoteClient with your client API key and an initial context:

import io.tggl.TgglRemoteClient
import io.tggl.TgglRemoteClientOptions
 
val client = TgglRemoteClient(TgglRemoteClientOptions.builder()
    .apiKey("YOUR_CLIENT_API_KEY")
    .initialContext(mapOf("userId" to "abc123"))
    .build())
 
// Wait for the initial flags to be fetched
client.waitReady().join()
 
// Read a flag value
val value = client.get("my-feature", false)
 
if (value) {
    // ...
}

The TgglRemoteClient sends the context to the Tggl API and receives all flag values in a single request. After the initial fetch, reading flag values with get is instantaneous since they are stored in memory.

Setting the context

The context represents the current user or environment. When you call setContext, the client sends the context to the Tggl API and receives the corresponding flag values:

client.setContext(mapOf(
    "userId" to "abc123",
    "plan" to "premium",
    "country" to "US"
)).join()

setContext returns a CompletableFuture that completes when the response has been received and flags have been updated. You can also use it in a coroutine-friendly way:

import kotlinx.coroutines.future.await
 
suspend fun updateContext() {
    client.setContext(mapOf("userId" to "abc123")).await()
    val value = client.get("my-feature", false)
}

If setContext is called multiple times in quick succession, only the response for the most recent context is applied. Stale responses from earlier calls are automatically discarded.

Evaluating flags

The get method reads a flag value from the locally cached flags. It takes the flag slug and a default value, which is returned if the flag is not found or if the client has not fetched flags yet:

val enabled = client.get("my-feature", false)
val variant = client.get("color-button", "#000000")
val count = client.get("max-items", 10)

Choose the default value carefully to ensure your application behaves correctly even if the Tggl service is unreachable for any reason or if someone from your team inadvertently removes the flag entirely.

Info

The SDK performs a type check on the returned value. If the flag value does not match the type of the default value you provided, the default value is returned instead. This prevents ClassCastException at call sites.

Getting all flags

You can retrieve all flag values at once:

val allFlags: Map<String, Any> = client.getAll()

This returns a map containing all flags and their values as returned by the server.

Setting flags directly

You can set flags directly without making a network request, which is useful for testing or offline scenarios:

client.setFlags(mapOf(
    "my-feature" to true,
    "color-button" to "#FF0000"
))

Waiting for the client to be ready

The client needs to fetch flag values from the server before it can return accurate results. If you call get before the client is ready, it will return the default value for all flags.

There are multiple ways to wait for the client to be ready:

// 1. Block the current thread (using CompletableFuture)
client.waitReady().join()
 
// 2. Suspend in a coroutine
client.waitReady().await()
 
// 3. Async callback
client.onReady {
    // Client is ready
}
 
// 4. Check synchronously
if (client.isReady) {
    // ...
}

The client is ready as soon as the first set of flags has been received, either from the API or from a storage.

Polling for live updates

You can update your flags at any time from the Tggl dashboard. To get these updates in your application, the client can poll the Tggl API periodically by re-sending the current context.

Polling is disabled by default (set to 0), since frontend applications typically call setContext explicitly when the user or state changes:

val client = TgglRemoteClient(TgglRemoteClientOptions.builder()
    .apiKey("YOUR_CLIENT_API_KEY")
    // Enable polling every 30 seconds
    .pollingIntervalMs(30_000)
    .build())

Polling can be started or changed at any time after the client has been created:

client.startPolling(20_000) // Every 20 seconds

You can disable polling at any time:

client.stopPolling()
// Or equivalently
client.startPolling(0)

You can manually trigger a refresh at any time, which re-sends the current context:

client.refetch().join()

Storages

Storages are used to cache flag values between application restarts to improve startup time and provide a fallback mechanism in case the Tggl API is unreachable. They are completely optional, but highly recommended for mobile applications.

A storage must implement the TgglStorage interface:

import io.tggl.storage.TgglStorage
import java.util.concurrent.CompletableFuture
 
class MyStorage : TgglStorage {
 
    override fun get(): CompletableFuture<String?> {
        // Read from your persistence layer
        return CompletableFuture.completedFuture(
            readFromPreferences()
        )
    }
 
    override fun set(value: String): CompletableFuture<Void> {
        // Write to your persistence layer
        writeToPreferences(value)
        return CompletableFuture.completedFuture(null)
    }
 
    override fun close(): CompletableFuture<Void> {
        // Optional: clean up resources
        return CompletableFuture.completedFuture(null)
    }
}

The get and set methods are used to retrieve and store the serialized flag state respectively. The serialization is handled by the client internally. The close method is optional and is called when the client is shut down.

Then pass your storage when creating the client:

val client = TgglRemoteClient(TgglRemoteClientOptions.builder()
    .apiKey("YOUR_CLIENT_API_KEY")
    .addStorage(MyStorage())
    .build())

For redundancy, you can provide multiple storages. The client will try to read from each storage in order when starting, and will write to all storages when flags are updated. If one storage fails, the client can still retrieve the state from another.

Storage-only mode (no network)

Even if the client will prime its state from storages at startup, it will still try to fetch the latest flags from the Tggl API unless you disable the initial fetch:

val client = TgglRemoteClient(TgglRemoteClientOptions.builder()
    .pollingIntervalMs(0)
    .initialFetch(false)
    .addStorage(myStorage)
    .build())

This can be useful for unit tests or specific scenarios where you want to use a fixed set of flags without making any network request.

Reporting

By default, the SDK sends flag evaluation data to Tggl to give you insights on how your flags are actually used in production and to help you manage flag lifecycles.

Reporting is batched and sent every 10 seconds by default to minimize network usage. You can change the reporting interval when creating the client by providing a custom TgglReporting instance:

val client = TgglRemoteClient(TgglRemoteClientOptions.builder()
    .apiKey("YOUR_CLIENT_API_KEY")
    .reporting(TgglReporting(TgglReportingOptions.builder()
        .apiKey("YOUR_CLIENT_API_KEY")
        .flushIntervalMs(15_000) // Every 15 seconds
        .build()))
    .build())

You can also change the reporting interval at any time after the client has been created:

val reporting = client.reporting
 
// Start (or change interval) to flush every 20 seconds
reporting.start(20_000)

You can stop reporting at any time:

reporting.stop()
// Or equivalently
reporting.start(0)

You can disable reporting entirely from the beginning:

val client = TgglRemoteClient(TgglRemoteClientOptions.builder()
    .apiKey("YOUR_CLIENT_API_KEY")
    .reportingEnabled(false)
    .build())
 
val reporting = client.reporting
reporting.isActive // false

Even when the scheduler is disabled, you can still manually flush pending reports:

reporting.flush().join()

Using the proxy

Having a proxy between your application and the Tggl API can help reduce latency, improve reliability, and reduce costs. You can configure the client to use a proxy by setting the baseUrls option:

val client = TgglRemoteClient(TgglRemoteClientOptions.builder()
    .apiKey("YOUR_CLIENT_API_KEY")
    .baseUrls(listOf("https://my-tggl-proxy.example.com"))
    .build())

Note that baseUrls is a list, so you can provide multiple URLs for redundancy. The client will try each URL in order in case of failures. The Tggl API (https://api.tggl.io) is always added as a final fallback automatically, even if you don't include it in the list.

val client = TgglRemoteClient(TgglRemoteClientOptions.builder()
    .apiKey("YOUR_CLIENT_API_KEY")
    .baseUrls(listOf(
        "https://my-tggl-proxy1.example.com",
        "https://api.tggl.io",
        "https://my-tggl-proxy2.example.com"
    ))
    .build())

The baseUrls you provide are automatically passed down to the TgglReporting instance used by the client, so reporting will also go through the proxy unless you explicitly override it with a custom TgglReporting.

Retry mechanism

Because network requests can fail for various reasons, the client has a built-in retry mechanism with exponential backoff (100ms, 200ms, 400ms, capped at 500ms):

val client = TgglRemoteClient(TgglRemoteClientOptions.builder()
    .apiKey("YOUR_CLIENT_API_KEY")
    // Retry at most 2 times after first failure (default is 3)
    .maxRetries(2)
    // Timeout requests after 5 seconds (default is 8 seconds)
    .timeoutMs(5_000)
    .build())

You can disable the retry mechanism by setting maxRetries to 0, but this is not recommended in production environments. Timeouts are applied to each individual request, not to the whole retry sequence.

Info

Not every request is retried. For example, if the server returns a 401 error code, the request is considered failed and will not be retried.

The retry mechanism is applied to each URL in baseUrls individually. For example, if you have 3 URLs and maxRetries is set to 2, the client will try each URL up to 3 times (1 initial try + 2 retries) before moving to the next URL.

Tracking

To integrate with your existing tracking or analytics system, you can register a callback that will be called every time a flag is evaluated:

client.onFlagEval { event ->
    println("Flag evaluated: ${event.slug()} = ${event.value()} (default: ${event.defaultValue()})")
}

onFlagEval returns a Runnable that can be called to unregister the listener:

val unsubscribe = client.onFlagEval { event -> /* ... */ }
 
// Later, stop listening
unsubscribe.run()

Listening for flag changes

You can be notified when flag values change after a fetch:

client.onFlagsChange { changedFlags ->
    println("Changed flags: $changedFlags")
}

The callback receives a list of flag slugs that have changed (added, removed, or modified). Similarly, you can listen for successful fetches:

client.onFetchSuccessful {
    println("Flags fetched successfully")
}

Error handling

For resiliency, the client swallows all errors and returns the default value when a fetch fails. However, you can listen to error events to be notified when something goes wrong:

client.onError { error ->
    System.err.println("Tggl client error: ${error.message}")
}

Additionally, you can get the error from the last fetch operation:

val error = client.error

Gracefully shutting down

The TgglRemoteClient implements AutoCloseable, so it can be used with Kotlin's use block. When closed, it stops polling, flushes any pending reporting data, closes all storages, and releases all resources:

TgglRemoteClient(options).use { client ->
    client.waitReady().join()
    // Use the client...
}
// All resources cleaned up automatically

Or close it manually:

client.close()

You should make sure to close the client before your application exits to ensure that the last batch of reporting data is sent.

Configuration reference

The full list of options available on TgglRemoteClientOptions.builder():

OptionTypeDefaultDescription
apiKeyStringnullYour client API key
baseUrlsList<String>[]Base URLs for the API (Tggl API is always added as fallback)
maxRetriesint3Max retry count per URL on failure
timeoutMslong8000HTTP request timeout in milliseconds
pollingIntervalMslong0Polling interval in ms (0 to disable)
storagesList<TgglStorage>[]Storage backends for caching flags
reportingTgglReportingnullCustom reporting instance (null uses the built-in one)
reportingEnabledbooleantrueEnable or disable the built-in reporting
appNameStringnullApp name sent with reports for identification
initialContextMap<String, Object>{}Context to send on the first fetch
initialFetchbooleantrueWhether to fetch flags on startup