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.
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'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.
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 secondsYou 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 // falseEven 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.
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.errorGracefully 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 automaticallyOr 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():
| Option | Type | Default | Description |
|---|---|---|---|
apiKey | String | null | Your client API key |
baseUrls | List<String> | [] | Base URLs for the API (Tggl API is always added as fallback) |
maxRetries | int | 3 | Max retry count per URL on failure |
timeoutMs | long | 8000 | HTTP request timeout in milliseconds |
pollingIntervalMs | long | 0 | Polling interval in ms (0 to disable) |
storages | List<TgglStorage> | [] | Storage backends for caching flags |
reporting | TgglReporting | null | Custom reporting instance (null uses the built-in one) |
reportingEnabled | boolean | true | Enable or disable the built-in reporting |
appName | String | null | App name sent with reports for identification |
initialContext | Map<String, Object> | {} | Context to send on the first fetch |
initialFetch | boolean | true | Whether to fetch flags on startup |