Android SDK: Migrating from v3 to v4
Version 4.0.0 is a full rewrite of the Castle Android SDK in Kotlin, replacing the previous Java implementation. This guide covers all breaking changes and how to update your integration.
The change to Kotlin and adding of Native Cpp Code allows to integrate more device signals and to apply obfuscation on multiple levels.
Requirements
Android API 26+ (Android 8.0)
Breaking changes
1. Package name
The Java package moved from io.castle.android to io.castle, and CastleConfiguration was renamed to Configuration.
// v3
import io.castle.android.Castle;
import io.castle.android.CastleConfiguration;
// v4
import io.castle.Castle;
import io.castle.Configuration;
import io.castle.ConfigurationError;// v3
import io.castle.android.Castle
import io.castle.android.CastleConfiguration
// v4
import io.castle.Castle
import io.castle.Configuration
import io.castle.ConfigurationError2. Configuration
Castle.configure(...)throwsConfigurationErrorwhen the publishable key is invalid or another setting is malformed. In v3 the builder itself threw a plainRuntimeExceptionfor an invalid key; validation is now centralised inconfigure(...).tconfigure(...)returns quickly; heavy setup runs in the background. It is safe to call from any thread and fromApplication.onCreate().
- The
<meta-data android:name="castle_publishable_key" ... />auto-configure shortcut has been removed. Pass the publishable key explicitly.
// v3
Castle.configure(application, "{Publishable-API-Key}");
// v4
Configuration configuration = new Configuration.Builder()
.publishableKey("{Publishable-API-Key}")
.build();
try {
Castle.configure(application, configuration);
} catch (ConfigurationError e) {
Log.e("Castle", "configure failed: " + e.getMessage());
}// v3
Castle.configure(application, "{Publishable-API-Key}")
// v4
val configuration = Configuration.Builder()
.publishableKey("{Publishable-API-Key}")
.build()
try {
Castle.configure(application, configuration)
} catch (e: ConfigurationError) {
Log.e("Castle", "configure failed: ${e.message}")
}3. Request Token Creation
Castle.createRequestToken() now returns String? - null when configure(...) has not been called (or has been torn down via resetConfiguration()). In v3 it threw a CastleError. Tokens can be created from any thread; the SDK handles the necessary synchronisation internally. It is recommended to avoid generating a token immediately after configure(...) so the SDK can complete its initial signal collection.
// v3
String token = Castle.createRequestToken(); // throws if unconfigured
// v4
@Nullable String token = Castle.createRequestToken(); // null if unconfigured// v3
val token: String = Castle.createRequestToken() // throws if unconfigured
// v4
val token: String? = Castle.createRequestToken() // null if unconfigured4. User identification and lifecycle
Castle.userJwt(String)was renamed toCastle.setUserJwt(String).Castle.destroy(Application)was renamed toCastle.resetConfiguration().Castle.reset()is unchanged - it flushes pending events and clears the persisted user JWT (logout flow).
// v3
Castle.userJwt("user-jwt-token");
Castle.destroy(application);
// v4
Castle.setUserJwt("user-jwt-token");
Castle.resetConfiguration();// v3
Castle.userJwt("user-jwt-token")
Castle.destroy(application)
// v4
Castle.setUserJwt("user-jwt-token")
Castle.resetConfiguration()5. Event tracking
Castle.custom(...)andCastle.screen(...)are now fire-and-forget - they return immediately and enqueue the event in the background.- Custom-event property values support all Kotlin/Java basic Types including
String. - The
Castle.screen(Activity)overload was removed - pass aStringname.
6. OkHttp interceptor
The CastleInterceptor class is no longer exported; obtain the interceptor through the factory.
// v3
new OkHttpClient.Builder()
.addInterceptor(new CastleInterceptor())
.build();
// v4
new OkHttpClient.Builder()
.addInterceptor(Castle.castleInterceptor())
.build();// v3
OkHttpClient.Builder()
.addInterceptor(CastleInterceptor())
.build()
// v4
OkHttpClient.Builder()
.addInterceptor(Castle.castleInterceptor())
.build()Complete Integration Sample
package com.example.app;
import android.app.Application;
import android.util.Log;
import io.castle.Castle;
import io.castle.Configuration;
import io.castle.ConfigurationError;
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
Configuration configuration = new Configuration.Builder()
.publishableKey("{Publishable-API-Key}")
.debugLoggingEnabled(true)
.build();
try {
Castle.configure(this, configuration);
// Castle is ready - set user identity and track events
Castle.setUserJwt("user-jwt-token");
Castle.screen("App Started");
} catch (ConfigurationError e) {
Log.e("Castle", "configure failed: " + e.getMessage());
}
}
}import android.app.Application
import android.util.Log
import io.castle.Castle
import io.castle.Configuration
import io.castle.ConfigurationError
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
val configuration = Configuration.Builder()
.publishableKey("{Publishable-API-Key}")
.debugLoggingEnabled(true)
.build()
try {
Castle.configure(this, configuration)
// Castle is ready — set user identity and track events
Castle.setUserJwt("user-jwt-token")
Castle.screen("App Started")
} catch (e: ConfigurationError) {
Log.e("Castle", "configure failed: ${e.message}")
}
}
}