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.ConfigurationError

2. Configuration

  • Castle.configure(...) throws ConfigurationError when the publishable key is invalid or another setting is malformed. In v3 the builder itself threw a plain RuntimeException for an invalid key; validation is now centralised in configure(...).
    • tconfigure(...) returns quickly; heavy setup runs in the background. It is safe to call from any thread and from Application.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 unconfigured

4. User identification and lifecycle

  • Castle.userJwt(String) was renamed to Castle.setUserJwt(String).
  • Castle.destroy(Application) was renamed to Castle.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(...) and Castle.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 a String name.

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}")
        }
    }
}