LogoLogo
My AppsLive demoNewsArticles
  • Introduction
  • 📌Product updates: 2025
    • 2024
    • 2023
  • Getting Started
    • Registration
    • Adding a space
    • Adding an app to the space
  • Basic Events & Custom Events
  • Integration
    • Expert Tips
      • What to track
      • Payments & Anti-cheat
      • Check your integration
    • Integration of SDK 2.0+
      • SDK Integration
        • Android
        • iOS
        • macOS
        • Windows
        • Web
          • Web SDK Integration
          • Web SDK Releases
        • Unity
        • Unreal Engine
        • Godot Engine
      • Automatic payment tracking
        • App Store
        • Google Play
      • Setting up Events
        • Basic methods
        • Secondary methods
        • User profile
        • Anticheat methods
        • Track sessions
      • Push notifications
        • Android
        • iOS
        • Windows (UWP)
        • Unity
          • Android
          • iOS
          • Windows (UWP/WSA)
        • Unreal Engine
      • A/B testing
        • Description of A/B testing on the SDK side
        • Working with A/B tests in the devtodev interface
        • A/B testing examples
    • Integration of SDK 1.0+ (deprecated)
      • SDK Integration
        • iOS
        • Android
        • Windows 8.1 and 10
        • Web
        • Unity
        • Mac OS
        • Adobe Air
        • UE4
      • Setting up Events
        • Basic methods
        • Secondary methods
        • User profile
        • Anti-cheat Methods
      • Push Notifications
        • IOS
        • Android
        • Windows 8.1 and Windows 10
        • Unity
        • Abode Air
        • UE4
    • Test Devices
    • Server API
      • Data API 2.0
      • Subscription API
      • Push API
        • IOS
        • Android
        • Windows UWP
        • Windows
      • Raw Export
      • Labels API
      • Data API
    • Import historical data via API
    • Data Export
      • Data Export to Cloud Storage (BigQuery / Amazon S3)
  • 3rd Party Sources
    • Attribution Trackers
      • AppsFlyer
      • Adjust
      • Branch.io
      • Kochava
      • Tenjin
      • Tune (MAT)
      • Singular
      • Custom postback API
      • Facebook Ads referral decryption
    • App Marketplace Data
      • App Store Connect Stats
      • App Store Subscriptions
      • Google Play Console Stats
      • Google Play Subscriptions
      • AppGallery Connect Stats
    • Ad revenue
      • AdColony
      • AdMob
      • Facebook
      • MoPub
      • Unity Ads
      • Vungle
      • Ad revenue API
    • Cohort export
  • Reports and Functionality
    • Space-related Reports and Functionality
      • Overview
      • Custom dashboards & Reports
      • SQL
        • SQL tips
        • SQL Query examples
      • Audience overlap
    • Project-related Reports and Functionality
      • Overview
        • Real-Time Dashboard
        • Acquisition reports
        • Engagement reports
        • Monetization reports
        • In-game analysis reports
        • Cohort analysis
      • Reports
      • Push Notifications
        • Android Notifications
        • IOS Notifications
        • Windows Notifications
        • Button Templates
      • Predictions
      • Users & Segments
      • Filters
      • A/B Testing
      • Tuning
      • Settings
  • Metrics and Glossary
    • Ad networks metrics
    • Market Metrics
    • Prediction Metrics
    • SDK Metrics
    • Subscription metrics
  • Space Management
  • User Profile Management
  • Limits
  • Scenarios and Best Practices
    • Analytics use cases
    • Match-3
    • MMORPG Games
    • Hyper-Casual games
    • Social Casino
    • RPG games
    • Farming games
    • Non-gaming app
    • Acquisition Example
  • FAQ
    • Identification
    • Raw Data
    • All about data discrepancies
  • Slack
Powered by GitBook
On this page
  • Example №1
  • Hypothesis
  • Test criteria
  • Groups
  • Implementation
  • Example №2
  • Hypothesis
  • Test criteria
  • Groups
  • Implementation
  • Example №3
  • Hypothesis
  • Test criteria
  • Groups
  • Implementation

Was this helpful?

Export as PDF
  1. Integration
  2. Integration of SDK 2.0+
  3. A/B testing

A/B testing examples

PreviousWorking with A/B tests in the devtodev interfaceNextIntegration of SDK 1.0+ (deprecated)

Last updated 1 month ago

Was this helpful?

Example №1

Hypothesis

Our analytics data show that most of the people make a purchase during their first session. However, only 70% of users open our in-app store during the first session. We hypothesize that if we add a purchase screen to the tutorial then 100% of users will face it and the number of purchases during the first session will increase.

Test criteria

The criteria for test inclusion is starting the tutorial. The DTDAnalytics.tutorial(step: Int) event with step = -1 is the trigger.

Groups

Control group: a 10 step tutorial, no purchase screens

Group А: an 11 step tutorial. At step number 5, we will offer a special offer.

Implementation

// Timer constants
struct Constants {
    static let waitGroupConst = 10.0
}

// Value keys
enum ValueKey: String {
  case showStore
}

class AppConfig {
    static func loadDefaults() {
        let appDefaults: [String: Any] = [
            ValueKey.showStore.rawValue: "false"
        ]
        // Set default values
        DTDRemoteConfig.defaults = appDefaults
    }
    
    static func bool(forKey key: ValueKey) -> Bool {
        return DTDRemoteConfig.config[key.rawValue].boolValue
    }
}

class AppLogic {
    var timer: Timer
    // Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
    func startTutorial() {
        // Send a trigger event that indicates tutorial start
        DTDAnalytics.tutorial(step: -1)
    }
    
    func nextTutotrialStep(_ currentStep: Int) {
        DTDAnalytics.tutorial(step: currentStep)
        if AppConfig.bool(forKey: .showStore) && currentStep == 5 {
            // Offer purchasing of a special offer
        }
    }
}

class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication,
        willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Set the maximum time of waiting for an A/B test group
        DTDRemoteConfig.groupDefinitionWaiting = Constants.waitGroupConst
        // Set default values
        AppConfig.loadDefaults()
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.initializeWithAbTest(applicationKey: "appKey",
                                          configuration: config,
                                          abConfigListener: self)
    }
}

extension AppLogic: DTDRemoteConfigListener {
    // Process the result of waiting for A/B test configuration
    func onReceived(result: DTDRemoteConfigReceiveResult) {
        // It is not used in current example
    }

    // Prepare the app UI for changing the remote configuration
    func onPrepareToChange() {
        // Use the main app thread because you are getting ready for working with the interface
        DispatchQueue.main.async { [weak self] in
            // Display the download progress indicator
            self?.showActivityIndicator()
            // Add a timer that will forcibly remove the download progress indicator
            self?.timer = Timer.scheduledTimer(withTimeInterval: Constants.waitGroupConst, repeats: false) { [weak self] _ in
                self?.hideActivityIndicator()
            }
        }
    }
    
    // Apply the values of the assigned group
    func onChanged(result: DTDRemoteConfigChangeResult, error: Error?) {
        defer {
            // Hide the download progress indicator
            DispatchQueue.main.async { [weak self] in
                self?.timer.invalidate();
                self?.hideActivityIndicator()
            }
        }

        switch result {
        case .success:
            // Apply new values
            DTDRemoteConfig.applyConfig()

        case .failure:
            // Error processing
            if let error = error {
                print(error.localizedDescription)
            }

        @unknown default:
            break
        }
    }
}
// Constants .h + .m
@interface Constants : NSObject
// Timer constants
extern double const waitGroupConst;
// Value keys
extern NSString * const showStore;
@end

@implementation Constants
double const waitGroupConst = 10.0;
NSString * const showStore = @"showStore";
@end

// AppConfig .h + .m
@interface AppConfig: NSObject
+(void) loadDefaults;
+(BOOL) getBoolForKey:(NSString *) key;
@end

@implementation AppConfig
+(void)loadDefaults {
    NSDictionary *appDefaults = @{
        showStore: @NO
    };

    // Set default values
    DTDRemoteConfig.defaults = appDefaults;
}

+(BOOL) getBoolForKey:(NSString *) key {
    return DTDRemoteConfig.config[key].boolValue;
}
@end

// AppLogic .h + .m
@interface AppLogic : UIViewController <DTDRemoteConfigListener>
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation AppLogic

- (void)viewDidLoad {
    [super viewDidLoad];
    [self startTutorial];
}

// Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
- (void) startTutorial {
    // Send a trigger event that indicates tutorial start
    [DTDAnalytics tutorialStep: -1];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
        [self nextTutotrialStep:5];
    });
}

- (void) nextTutotrialStep:(NSInteger) currentStep {
    [DTDAnalytics tutorialStep: currentStep];
    if ([AppConfig getBoolForKey:showStore] && currentStep == 5) {
        // Offer purchasing of a special offer
    }
}

// Process the result of waiting for A/B test configuration
- (void)onReceivedResult:(enum DTDRemoteConfigReceiveResult)result {
    // It is not used in current example
}

// Prepare the app UI for changing the remote configuration
- (void)onPrepareToChange {
    // Use the main app thread because you are getting ready for working with the interface
    __weak AppLogic *weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        // Display the progress indicator
        [weakSelf showActivityIndicator];
        // Add a timer that will forcibly remove the progress indicator
        self.timer = [NSTimer scheduledTimerWithTimeInterval:waitGroupConst repeats:false block:^(NSTimer *timer){
            [weakSelf hideActivityIndicator];
        }];
    });
}

// Apply the values of the assigned group
- (void)onChangedResult:(enum DTDRemoteConfigChangeResult)result error:(NSError *)error {
    switch (result) {
        case DTDRemoteConfigChangeResultSuccess:
            // Apply new values
            [DTDRemoteConfig applyConfig];
            break;

        case DTDRemoteConfigChangeResultFailure:
            // Error processing
            if (error) {
                NSLog(@"DTDRemoteConfigError: %@", error.localizedDescription);
            }

        default:
            break;
    }

    // Hide the progress indicator
    __weak AppLogic *weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.timer invalidate];
        [weakSelf hideActivityIndicator];
    });
}

- (void)showActivityIndicator {
    // Display the progress indicator
}

- (void)hideActivityIndicator {
    // Hide the progress indicator
}
@end

// AppDelegate .h + .m
@interface AppDelegate ()
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    AppLogic *appLogicController = [[UIStoryboard storyboardWithName:@"Main" bundle:nil] instantiateViewControllerWithIdentifier:@"AppLogic"];
    UINavigationController *navController = [[UINavigationController alloc]initWithRootViewController:appLogicController];
    self.window.rootViewController = navController;
    [self.window makeKeyAndVisible];

    // Group timeout, optional
    DTDRemoteConfig.groupDefinitionWaiting = waitGroupConst;
    // Implementation defaults params
    [AppConfig loadDefaults];
    [DTDAnalytics applicationKey:appKey abConfigListener:appLogicController];

    return YES;
}
@end
class Constants {
    // Timer constants
    companion object {
        const val waitGroupConst = 10.0
        const val waitGroupConstInMilliseconds = 10000L
    }
}

// Value keys
enum class ValueKey(val value: String) {
    ShowStore("showStore")
}

class AppConfig {
    companion object {
        fun loadDefaults() {
            val appDefaults = mapOf<String, Any>(ValueKey.ShowStore.value to "false")
            DTDRemoteConfig.defaults = appDefaults
        }

        fun bool(key: ValueKey): Boolean {
            return DTDRemoteConfig.config[key.value].booleanValue
        }
    }
}

class AppLogic {
    // Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
    fun startTutorial() {
        // Send a trigger event that indicates tutorial start
        DTDAnalytics.tutorial(step = -1)
    }

    fun nextTutorialStep(currentStep: Int) {
        DTDAnalytics.tutorial(step = currentStep)
        if (AppConfig.bool(ValueKey.ShowStore) && currentStep == 5) {
            // Offer purchasing of a special offer
        }
    }
}

class MainActivity : AppCompatActivity(), DTDRemoteConfigListener {
    //activityIndicator control timer
    var timer: TimerTask? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState, persistentState)
        setContentView(R.layout.activity_main)

        // Set the maximum time of waiting for an A/B test group
        DTDRemoteConfig.groupDefinitionWaiting = Constants.waitGroupConst
        // Set default values
        AppConfig.loadDefaults()
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.initializeWithAbTest(
            appKey = "appKey",
            context = this,
            abConfigListener = this
        )
    }

    // Process the result of waiting for A/B test configuration
    override fun onReceived(result: DTDRemoteConfigReceiveResult) {
        // It is not used in current example
    }

    // Prepare the app UI for changing the remote configuration
    override fun onPrepareToChange() {
        // Use the main app thread because you are getting ready for working with the interface
        runOnUiThread {
            // Display the download progress indicator
            this.showActivityIndicator()
        }
        // Add a timer that will forcibly remove the download progress indicator
        timer = Timer("ActivityIndicator").schedule(Constants.waitGroupConstInMilliseconds) {
            runOnUiThread {
                this.hideActivityIndicator()
            }
        }
    }

    override fun onChanged(result: DTDRemoteConfigChangeResult, ex: Exception?) {
        when (result) {
            DTDRemoteConfigChangeResult.Success -> {
                // Apply new values
                DTDRemoteConfig.applyConfig()
            }
            DTDRemoteConfigChangeResult.Failure -> {
                // Error processing
                ex?.let { Log.e("TAG", ex.toString()) }
            }
        }

        this.timer?.cancel()
        this.timer = null
        
        runOnUiThread {
            this.hideActivityIndicator()
        }
    
class Constants {
    // Timer constants
    final static double waitGroupConst = 10.0;
    final static long waitGroupConstInMilliseconds = 10000L;
}

enum ValueKey {
    ShowStore("showStore");

    private final String stringValue;

    ValueKey(String toString) {
        stringValue = toString;
    }

    @Override
    public String toString() {
        return stringValue;
    }
}

class AppConfig {
    static void loadDefaults() {
        HashMap<String, Object> map = new HashMap<>();
        map.put(ValueKey.ShowStore.toString(), "false");
        DTDRemoteConfig.INSTANCE.setDefaults(map);
    }

    static Boolean bool(ValueKey key) {
        return DTDRemoteConfig.INSTANCE.getConfig().get(key.toString()).getBooleanValue();
    }
}

class AppLogic {
    // Tutorial open event (e.g. by clicking the 'start tutorial' button)
    void startTutorial() {
        // Send a trigger event that indicates tutorial start
        DTDAnalytics.INSTANCE.tutorial( -1);
    }

    void nextTutorialStep(int currentStep) {
        DTDAnalytics.INSTANCE.tutorial(currentStep);
        if (AppConfig.bool(ValueKey.ShowStore) && currentStep == 5) {
            // Offer purchasing of a special offer
        }
    }
}

class MainActivity extends AppCompatActivity implements DTDRemoteConfigListener {
    //activityIndicator control timer
    Timer timer = null;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Set the maximum time of waiting for an A/B test group
        DTDRemoteConfig.INSTANCE.setGroupDefinitionWaiting(Constants.waitGroupConst);
        // Set default values
        AppConfig.loadDefaults();
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.INSTANCE.initializeWithAbTest("appKey", this, this);
    }

    // Process the result of waiting for A/B test configuration
    @Override
    public void onReceived(@NonNull DTDRemoteConfigReceiveResult result) {
        // It is not used in current example
    }

    // Prepare the app UI for changing the remote configuration
    @Override
    public void onPrepareToChange() {
        // Use the main app thread because you are getting ready for working with the interface
        runOnUiThread(() -> {
            // Display the download progress indicator
            showActivityIndicator();
        });

        timer = new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                runOnUiThread(() -> hideActivityIndicator());
            }
        }, Constants.waitGroupConstInMilliseconds);
    }

    @Override
    public void onChanged(@NonNull DTDRemoteConfigChangeResult result, @Nullable Exception ex) {
        if (result == DTDRemoteConfigChangeResult.Success) {
            // Apply new values
            DTDRemoteConfig.INSTANCE.applyConfig();
        }

        if (result == DTDRemoteConfigChangeResult.Failure) {
            // Apply new values
            DTDRemoteConfig.INSTANCE.applyConfig();
        }

        if (timer != null) {
            timer.cancel();
            timer = null;
        }

        runOnUiThread(() -> hideActivityIndicator());
    }
}
// Timer constants
public static class Constants
{
    public const float WAIT_GROUP_TIME = 10.0f;
}

// Value keys
public enum ValueKey
{
    showStore
}

public class AppConfig
{
    public void LoadDefaults()
    {
        var appDefaults = new Dictionary<string, object>
        {
            {ValueKey.showStore.ToString(), false}
        };
        DTDRemoteConfig.Defaults = appDefaults;
    }

    public bool GetBool(ValueKey key) => DTDRemoteConfig.Config[key.ToString()].BoolValue();
}

public class AppLogic : MonoBehaviour
{
    private readonly AppConfig _appConfig = new AppConfig();
    private const string APP_KEY = "appKey";
    private SimpleUI _simpleUI;
    private void Start()
    {
        DontDestroyOnLoad(this);
        _simpleUI = FindObjectOfType<SimpleUI>();
        if (_simpleUI == null) throw new NullReferenceException("UIManager not found.");
        // Set the maximum time of waiting for an A/B test group
        DTDRemoteConfig.GroupDefinitionWaiting = Constants.WAIT_GROUP_TIME;
        // Set default values
        _appConfig.LoadDefaults();
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.InitializeWithAbTests(
            appKey: APP_KEY,
            configListener: this);
    }

    // Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
    // Send a trigger event that indicates tutorial start
    public void StartTutorial() => DTDAnalytics.Tutorial(-1);

    public void NextTutorialStep(int step)
    {
        DTDAnalytics.Tutorial(step);
        if (_appConfig.GetBool(ValueKey.showStore) && step == 5)
        {
            // Offer purchasing of a special offer
            Store.Instance.ShowSpecialOffer();
        }
    }

    // Process the result of waiting for A/B test configuration
    public void OnReceived(DTDRemoteConfigReceiveResult result)
    {
        // It is not used in current example
    }

    // Prepare the app UI for changing the remote configuration
    public void OnPrepareToChange()
    {
        // Display the download progress indicator
        _simpleUI.ShowLoadingIndicator(Constants.WAIT_GROUP_TIME);
    }
    
    // Apply the values of the assigned group
    public void OnChanged(DTDRemoteConfigChangeResult result, string exceptionText = null)
    {
        // Hide the download progress indicator
        _simpleUI.HideLoadingIndicator();
        switch (result)
        {
            case DTDRemoteConfigChangeResult.Failure:
                // Error processing
                Debug.LogError(exceptionText);
                break;
            case DTDRemoteConfigChangeResult.Success:
                // Apply new values
                DTDRemoteConfig.ApplyConfig();
                break;
        }
    }
}
// Timer constants
static class Constants
{
    public const float WaitGroupConst = 10.0f;
}

// Value keys
static class ValueKeys
{
    public const string ShowStore = "showStore";
}

static class AppConfg
{
    public static void LoadDafaults()
    {
        DTDRemoteConfig.Defaults = new Dictionary<string, object>
        {
          [ValueKeys.ShowStore] = false
        };
    }
    
    public static bool GetBoolValue(string key)
    {
        return DTDRemoteConfig.Config[key].BoolValue;
    }
}

class Application : IDTDRemoteConfigListener
{
    public void Run()
    {
       // Set the maximum time of waiting for an A/B test group
        DTDRemoteConfig.GroupDefinitionWaiting = Constants.WaitGroupConst;
        // Set default values
        AppConfg.LoadDafaults();
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.InitializeWithAbTest("appKey", this);
        // Your code to show UI
    }

    // Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
    public void StartTutorial()
    {
        // Send a trigger event that indicates tutorial start
        DTDAnalytics.Tutorial(-1);
    }
    
    public void NextTutotrialStep(int step)
    {
        DTDAnalytics.Tutorial(step);
        if (AppConfg.GetBoolValue(ValueKeys.ShowStore) && step == 5)
        {
            // Offer purchasing of a special offer (method do a job in UI thread)
            UI.ShowSpecialOffer();
        }
    }
    
    // Apply the values of the assigned group
    public void OnChanged(DTDRemoteConfigChangeResult result, string error)
    {
        Debug.WriteLine($"[App-ABTests] OnChanged({result}, {error})");
        switch (result)
        {
            case DTDRemoteConfigChangeResult.Failure:
                // Error processing
                break;
            case DTDRemoteConfigChangeResult.Success:
                // Apply new values
                DTDRemoteConfig.ApplyConfig();
                break;
            default:
                break;
        }
        
        // Hide the download progress indicator(method do a job in UI thread)
        UI.HideLoadingIndicator();
    }

    // Prepare the app UI for changing the remote configuration
    public void OnPrepareToChange()
    {
        Debug.WriteLine($"[App-ABTests] OnPrepareToChange()");
        // Display the download progress indicator (method do a job in UI thread)
        UI.ShowLoadingIndicator();
        // Add a timer that will forcibly remove the download progress indicator
        TimerUtil.CallWithDelay(Constants.WaitGroupConst, () =>
        {
            // Hide the download progress indicator(method do a job in UI thread)
            UI.HideLoadingIndicator();
        });
    }

    // Process the result of waiting for A/B test configuration
    public void OnReceived(DTDRemoteConfigReceiveResult result)
    {
        // It is not used in current example
        Debug.WriteLine($"[App-ABTests] OnReceived({result})");
    }
}
devtodev.remoteConfig.defaults = {
   showStore: false,
}
devtodev.remoteConfig.groupDefinitionWaiting = 10
devtodev.initializeWithAbTest(
    appKey, 
    {
        userId: userId,
        logLevel: logLevel,
        trackingAvailability: trackingAvailability,
    },
    {
        // Process the result of waiting for A/B test configuration
        onReceived: function(result) {
            // It is not used in current example
        },
        // Prepare the app UI for changing the remote configuration
        onPrepareToChange: function() {
            // Display the progress indicator
            ui.showSpinner()
        },
        // Apply the values of the assigned group
        onChanged: function(result, error) {
            ui.hideSpinner()
            switch (result) {
              case DTDRemoteConfigChangeResult.Failure:
                  // Error processing
                  console.error(error);
                  break;
              case DTDRemoteConfigChangeResult.Success:
                  // Apply new values
                  devtodev.remoteConfig.applyConfig()
                  break;
            }
        }
    }
)
function startTutorial() {
    devtodev.tutorial(parseInt(-1))
}
function setTutorialStep(step) {
    devtodev.tutorial(parseInt(step))
    var showStore = window.devtodev.remoteConfig.config['showStore'].boolValue
    if (showStore && step == 5) {
          // Offer purchasing of a special offer
          store.showSpecialOffer();
    }
}
// Timer constant
float WAIT_GROUP_TIME = 15.0f;

void SomeLogicClass::Start() {
    // Set the maximum time of waiting for an A/B test group
    UDTDRemoteConfigBPLibrary::SetGroupDefinitionWaiting(WAIT_GROUP_TIME);

    // Set default values
    FDTDRemoteConfigDefaults Defaults;
    Defaults.BoolDefaults.Add("showStore", false);
    UDTDRemoteConfigBPLibrary::SetDefaults(Defaults);

    const auto onConfigReceive = new FDTDRemoteConfigReceiveResultDelegate();
    onConfigReceive->BindUObject(this, &SomeLogicClass::OnConfigReceive);

    const auto onPrepareToChange = new FDTDRemoteConfigPrepareToChangeDelegate();
    onPrepareToChange->BindUObject(this, &SomeLogicClass::OnPrepareToChange);

    const auto onConfigChange = new FDTDRemoteConfigChangeResultDelegate();
    onConfigChange->BindUObject(this, &SomeLogicClass::OnConfigChange);

    // Initialize the SDK for working with A/B testing
    UDTDAnalyticsBPLibrary::InitializeWithAbTest("AppKey", *onConfigChange, *onPrepareToChange, *onConfigReceive);
}

// Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
void SomeLogicClass::StartTutorial() {
    // Send a trigger event that indicates tutorial start
    UDTDAnalyticsBPLibrary::Tutorial(-1);
}

void SomeLogicClass::NextTutorialStep(int32 step)
{
    UDTDAnalyticsBPLibrary::Tutorial(step);
    bool isNeedShowStore = UDTDRemoteConfigBPLibrary::GetRemoteConfigValue("showStore").BoolValue;
    if (isNeedShowStore && step == 5)
    {
        // Offer purchasing of a special offer
        SomeStoreUI::ShowSpecialOffer();
    }
}

// Process the result of waiting for A/B test configuration
void SomeLogicClass::OnConfigReceive(EDTDRemoteConfigReceiveResult result) {
    // It is not used in current example
}

// Prepare the app UI for changing the remote configuration
void SomeLogicClass::OnPrepareToChange() {
    // Display the download progress indicator
    SomeUI::ShowLoadingIndicator(WAIT_GROUP_TIME);
}

// Apply the values of the assigned group
void SomeLogicClass::OnConfigChange(EDTDRemoteConfigChangeResult result, const FString& error) {
    // Hide the download progress indicator
    SomeUI::HideLoadingIndicator();
    switch (result)
    {
    case EDTDRemoteConfigChangeResult::Success:
    	// Apply new values
	UDTDRemoteConfigBPLibrary::ApplyConfig();
	break;
	
    case EDTDRemoteConfigChangeResult::Failure:
	// Error processing
	UE_LOG(LogTemp, Warning, TEXT("DTDRemoteConfigError: %s"), *error);
	break;

    default:
	break;
    }
}
# Control timer
var timer = Timer.new()
# Timer constant
var waitGroupConst = 10.0
var showStore = "showStore"

func loadDefaults():
	var appDefaults = GDDTDRemoteConfigDefaults.new()
	# Value key
	appDefaults.AddBoolValue(showStore, false)
	DTDRemoteConfig.SetDefaults(appDefaults)
	
func getBool(key: String) -> bool: 
	return DTDRemoteConfig.GetRemoteConfigValue(key).GetBoolValue()

# Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
func startTutorial():
    # Send a trigger event that indicates tutorial start
	DTDAnalytics.Tutorial(-1)

func nextTutorialStep(currentStep: int):
	DTDAnalytics.Tutorial(currentStep)
	if (getBool(showStore) && currentStep == 5):
		#Offer purchasing of a special offer
		pass
	
func _ready():
    # Set the maximum time of waiting for an A/B test group
	DTDRemoteConfig.SetGroupDefinitionWaiting(waitGroupConst)
	# Set default values
	loadDefaults()
	#Initialize the SDK for working with A/B testing
	DTDAnalytics.InitializeWithAbTest("appKey",
	onRemoteConfigChange,
	onRemoteConfigPrepareToChange,
	onRemoteConfigReceive)
	DTDAnalytics.SetLogLevel(GDDTDLogLevel.Debug)
	
func onRemoteConfigChange(result: GDDTDRemoteConfigChangeResult.ChangeResult, error: String):
	match result:
		GDDTDRemoteConfigChangeResult.Success:
			# Apply new values
			DTDRemoteConfig.ApplyConfig()
		GDDTDRemoteConfigChangeResult.Failure:
			# Error processing
			print(error)
			
	timer.stop()
	hideActivityIndicator()
	
# Prepare the app UI for changing the remote configuration
func onRemoteConfigPrepareToChange():
    # Display the download progress indicator
	showActivityIndicator()
	# Add a timer that will forcibly remove the download progress indicator
	timer.connect("timeout", hideActivityIndicator)
	timer.one_shot = true
	timer.wait_time = waitGroupConstInMilliseconds
	add_child(timer)
	timer.start()

# Process the result of waiting for A/B test configuration
func onRemoteConfigReceive(result: GDDTDRemoteConfigReceiveResult.ReceiveResult):
	#It is not used in current example
	pass

Example №2

Hypothesis

Our analytics data show that N users installed the app more than half a year ago but still did not make a purchase. We hypothesize that if we offer a huge discount then we can get additional income.

Test criteria

The criteria for test inclusion is the app install date and the “payer” status.

Groups

Control group: current version with usual prices

Group А: it has a discount badge on the main page. After clicking on the badge, a purchase window pops up

Implementation

 // Timer constants
struct Constants {
    static let waitConfigConst = 10.0
    static let waitGroupConst = 15.0
}

// Value keys
enum ValueKey: String {
  case maximumDiscount
}

class AppConfig {
    static func loadDefaults() {
        let appDefaults: [String: Any] = [
            ValueKey.maximumDiscount.rawValue: "false"
        ]
        // Set default values
        DTDRemoteConfig.defaults = appDefaults
    }
    
    static func bool(forKey key: ValueKey) -> Bool {
        return DTDRemoteConfig.config[key.rawValue].boolValue
    }
}

class AppLogic {
    override func viewDidLoad() {
        super.viewDidLoad()
        // Display the launch screen
        showLaunchScreen()
    }
    
    // Launching the main logic of the application
    func startAppForReal() {
        // Update app UI
        updateAppUI()
        // Hide launch screen
        hideLaunchScreen()
    }
    
    func updateAppUI() {
        if bool(forKey: .maximumDiscount) {
            // Display a discount badge clicking on which invokes a purchase window popup
        }
    }
}

class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication,
        willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Set the maximum time of waiting for the A/B test configuration
        DTDRemoteConfig.remoteConfigWaiting = Constants.waitConfigConst
        // Set the maximum time of waiting for an A/B test group
        DTDRemoteConfig.groupDefinitionWaiting = Constants.waitGroupConst
        // Set default values
        AppConfig.loadDefaults()
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.initializeWithAbTest(applicationKey: "appKey",
                                          configuration: config,
                                          abConfigListener: self)
    }
}

extension AppLogic: DTDRemoteConfigListener {
    // Process the result of waiting for A/B test configuration
    func onReceived(result: DTDRemoteConfigReceiveResult) {
        // If the attempt fails, launch the main logic of the application
        if result == .failure {
            DispatchQueue.main.async { [weak self] in
                self?.startAppForReal()
            }
        }
    }

    // Prepare the app UI for changing the remote configuration
    func onPrepareToChange() {
        // It is not used in current example
    }
    
    // Apply the values of the assigned group
    func onChanged(result: DTDRemoteConfigChangeResult, error: Error?) {
        defer {
            // Launch the main logic of the application
            DispatchQueue.main.async { [weak self] in
              self?.startAppForReal()
            }
        }

        switch result {
        case .success:
            // Apply new values
            DTDRemoteConfig.applyConfig()

        case .failure:
            // Error processing
            if let error = error {
                print(error.localizedDescription)
            }

        @unknown default:
            break
        }
    }
}

Note

When receiving the configuration, the SDK always calls the onReceived(result: DTDRemoteConfigReceiveResult) method, and when enrolling in a test - the onPrepareToChange() and onChanged(result: DTDRemoteConfigChangeResult, error: Error?) method. However, you can take some additional precocious measures. You can add a timer as in the following example:

func showLaunchScreen() {
    // Add a timer that will forcibly launch the main logic of the application
    timer = Timer.scheduledTimer(withTimeInterval: waitConfigConst + waitGroupConst, 
                                          repeats: false) { [weak self] _ in
        self?.startAppForReal()
    }
    showActivityIndicator()
}
// Constants .h + .m
@interface Constants : NSObject
// Timer constants
extern double const waitConfigConst;
extern double const waitGroupConst;
// Value keys
extern NSString * const maximumDiscount;
@end

@implementation Constants
double const waitConfigConst = 10.0;
double const waitGroupConst = 15.0;
NSString * const maximumDiscount = @"maximumDiscount";
@end

// AppConfig .h + .m
@interface AppConfig: NSObject
+(void) loadDefaults;
+(BOOL) getBoolForKey:(NSString *) key;
@end

@implementation AppConfig
+(void)loadDefaults {
    NSDictionary *appDefaults = @{
        maximumDiscount: @NO
    };

    // Set default values
    DTDRemoteConfig.defaults = appDefaults;
}

+(BOOL) getBoolForKey:(NSString *) key {
    return DTDRemoteConfig.config[key].boolValue;
}
@end

// AppLogic .h + .m
@interface AppLogic : UIViewController <DTDRemoteConfigListener>
@end

@implementation AppLogic
- (void)viewDidLoad {
    [super viewDidLoad];
    // Display the launch screen
    [self showLaunchScreen];
}

// Launching the main logic of the application
- (void) startAppForReal {
    // Update app UI
    [self updateAppUI];
    // Hide launch screen
    [self hideLaunchScreen];
}

- (void) updateAppUI {
    if ([AppConfig getBoolForKey:maximumDiscount]) {
        // Display a discount badge clicking on which invokes a purchase window popup
    }
}

// Process the result of waiting for A/B test configuration
- (void)onReceivedResult:(enum DTDRemoteConfigReceiveResult)result {
    // If the attempt fails, launch the main logic of the application
    if (result == DTDRemoteConfigReceiveResultFailure) {
        __weak AppLogic *weakSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [weakSelf startAppForReal];
        });
    }
}

// Prepare the app UI for changing the remote configuration
- (void)onPrepareToChange {
    // It is not used in current example
}

// Apply the values of the assigned group
- (void)onChangedResult:(enum DTDRemoteConfigChangeResult)result error:(NSError *)error {
    switch (result) {
        case DTDRemoteConfigChangeResultSuccess:
            // Apply new values
            [DTDRemoteConfig applyConfig];
            break;

        case DTDRemoteConfigChangeResultFailure:
            // Error processing
            if (error) {
                NSLog(@"DTDRemoteConfigError: %@", error.localizedDescription);
            }

        default:
            break;
    }

    // Launch the main logic of the application
    __weak AppLogic *weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        [weakSelf startAppForReal];
    });
}

- (void)showLaunchScreen {
    // Display the launch screen
}

- (void)hideLaunchScreen {
    // Hide launch screen
}
@end

// AppDelegate .h + .m
@interface AppDelegate ()
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    AppLogic *appLogicController = [[UIStoryboard storyboardWithName:@"Main" bundle:nil] instantiateViewControllerWithIdentifier:@"AppLogic"];
    UINavigationController *navController = [[UINavigationController alloc]initWithRootViewController:appLogicController];
    self.window.rootViewController = navController;
    [self.window makeKeyAndVisible];

    // Config timeout, optional
    DTDRemoteConfig.remoteConfigWaiting = waitConfigConst;
    // Group timeout, optional
    DTDRemoteConfig.groupDefinitionWaiting = waitGroupConst;
    // Implementation defaults params
    [AppConfig loadDefaults];
    [DTDAnalytics applicationKey:appKey abConfigListener:appLogicController];

    return YES;
}
@end

Note

When receiving the configuration, the SDK always calls the onReceivedResult method, and when enrolling in a test - the onPrepareToChange and onChangedResult method. However, you can take some additional precocious measures. You can add a timer as in the following example:

-(void) showLaunchScreen {
    // Add a timer that will forcibly launch the main logic of the application
    self.timer = [NSTimer scheduledTimerWithTimeInterval:waitConfigConst + waitGroupConst
                                                 repeats:false 
                                                   block:^(NSTimer *timer){
        [weakSelf startAppForReal];
    }];
    showActivityIndicator()
}
class Constants {
    // Timer constants
    companion object {
        const val waitConfigConst = 10.0
        const val waitConfigConstInMilliseconds = 10000L
        const val waitGroupConst = 15.0
        const val waitGroupConstInMilliseconds = 15000L
    }
}

// Value keys
enum class ValueKey(val value: String) {
    MaximumDiscount("maximumDiscount")
}

class AppConfig {
    companion object {
        fun loadDefaults() {
            val appDefaults = mapOf<String, Any>(ValueKey.MaximumDiscount.value to "false")
            // Set default values
            DTDRemoteConfig.defaults = appDefaults
        }

        fun bool(key: ValueKey): Boolean {
            return DTDRemoteConfig.config[key.value].booleanValue
        }
    }
}

class AppLogic {
    fun viewDidLoad() {
        // Display the launch screen
        showLaunchScreen()
    }

    // Launching the main logic of the application
    fun startAppForReal() {
        // Update app UI
        updateAppUI()
        // Hide launch screen
        hideLaunchScreen()
    }

    fun updateAppUI() {
        if (AppConfig.bool(ValueKey.MaximumDiscount)) {
            // Display a discount badge clicking on which invokes a purchase window popup
        }
    }
}

class MainActivity : AppCompatActivity(), DTDRemoteConfigListener {
    //activityIndicator control timer
    var timer: Timer? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState, persistentState)
        setContentView(R.layout.activity_main)

        // Set the maximum time of waiting for the A/B test configuration
        DTDRemoteConfig.remoteConfigWaiting = Constants.waitConfigConst
        // Set the maximum time of waiting for an A/B test group
        DTDRemoteConfig.groupDefinitionWaiting = Constants.waitGroupConst
        // Set default values
        AppConfig.loadDefaults()
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.initializeWithAbTest(
            appKey = "appKey",
            context = this,
            abConfigListener = this
        )
    }

    // Process the result of waiting for A/B test configuration
    override fun onReceived(result: DTDRemoteConfigReceiveResult) {
        // If the attempt fails, launch the main logic of the application
        if (result == DTDRemoteConfigReceiveResult.Failure) {
            runOnUiThread {
                startAppForReal()
            }
        }
    }


    // Prepare the app UI for changing the remote configuration
    override fun onPrepareToChange() {
        // It is not used in current example
    }

    override fun onChanged(result: DTDRemoteConfigChangeResult, ex: Exception?) {
        when (result) {
            DTDRemoteConfigChangeResult.Success -> {
                // Apply new values
                DTDRemoteConfig.applyConfig()
            }
            DTDRemoteConfigChangeResult.Failure -> {
                // Error processing
                ex?.let { Log.e("TAG", ex.toString()) }
            }
        }

        runOnUiThread {
            startAppForReal()
        }
    }
}

Note

When receiving the configuration, the SDK always calls the onReceivedResult method, and when enrolling in a test - the onPrepareToChange and onChangedResult method. However, you can take some additional precocious measures. You can add a timer as in the following example:

fun showLaunchScreen() {
    // Add a timer that will forcibly launch the main logic of the application
    tiemr = Timer("ActivityIndicator").schedule(
        Constants.waitConfigConstInMilliseconds +
                Constants.waitGroupConstInMilliseconds
    ) {
        runOnUiThread {
            startAppForReal()
        }
    }
    showActivityIndicator()
}
class Constants {
    // Timer constants
    final static double waitGroupConst = 10.0;
    final static long waitGroupConstInMilliseconds = 10000L;

    final static double waitConfigConst = 15.0;
    final static long waitConfigConstInMilliseconds = 15000L;
}

enum ValueKey {
    MaximumDiscount("maximumDiscount");

    private final String stringValue;

    ValueKey(String toString) {
        stringValue = toString;
    }

    @Override
    public String toString() {
        return stringValue;
    }
}

class AppConfig {
    static void loadDefaults() {
        HashMap<String, Object> map = new HashMap<>();
        map.put(ValueKey.MaximumDiscount.toString(), "false");
        DTDRemoteConfig.INSTANCE.setDefaults(map);
    }

    static Boolean bool(ValueKey key) {
        return DTDRemoteConfig.INSTANCE.getConfig().get(key.toString()).getBooleanValue();
    }
}

class AppLogic {
    static void viewDidLoad() {
        // Display the launch screen
        showLaunchScreen();
    }

    // Launching the main logic of the application
    static void startAppForReal() {
        // Update app UI
        updateAppUI();
        // Hide launch screen
        hideLaunchScreen();
    }

    static void updateAppUI() {
        if (AppConfig.bool(ValueKey.MaximumDiscount)) {
            // Display a discount badge clicking on which invokes a purchase window popup
        }
    }
}

class MainActivity extends AppCompatActivity implements DTDRemoteConfigListener {
    //activityIndicator control timer
    Timer timer = null;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Set the maximum time of waiting for the A/B test configuration
        DTDRemoteConfig.INSTANCE.setRemoteConfigWaiting(Constants.waitConfigConst);
        // Set the maximum time of waiting for an A/B test group
        DTDRemoteConfig.INSTANCE.setGroupDefinitionWaiting(Constants.waitGroupConst);
        // Set default values
        AppConfig.loadDefaults();
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.INSTANCE.initializeWithAbTest("appKey", this, this);
    }

    // Process the result of waiting for A/B test configuration
    @Override
    public void onReceived(@NonNull DTDRemoteConfigReceiveResult result) {
        // If the attempt fails, launch the main logic of the application
        if (result == DTDRemoteConfigReceiveResult.Failure) {
            runOnUiThread(AppLogic::startAppForReal);
        }
    }

    // Prepare the app UI for changing the remote configuration
    @Override
    public void onPrepareToChange() {
        // It is not used in current example
    }

    @Override
    public void onChanged(@NonNull DTDRemoteConfigChangeResult result, @Nullable Exception ex) {
        if (result == DTDRemoteConfigChangeResult.Success) {
            // Apply new values
            DTDRemoteConfig.INSTANCE.applyConfig();
        }

        if (result == DTDRemoteConfigChangeResult.Failure) {
            // Error processing
            if (ex != null) {
                Log.e("TAG", ex.toString());
            }
        }

        if (timer != null) {
            timer.cancel();
            timer = null;
        }

        runOnUiThread(AppLogic::startAppForReal);
    }
}

Note

When receiving the configuration, the SDK always calls the onReceivedResult method, and when enrolling in a test - the onPrepareToChange and onChangedResult method. However, you can take some additional precocious measures. You can add a timer as in the following example:

    void showLaunchScreen() {
        // Add a timer that will forcibly launch the main logic of the application
        timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                runOnUiThread(AppLogic::startAppForReal);
            }
        }, Constants.waitConfigConstInMilliseconds +
                Constants.waitGroupConstInMilliseconds);

        showActivityIndicator();
    }
 // Timer constants
public static class Constants
{
    public static float WAIT_GROUP_TIME = 10.0f;
    public static float WAIT_CONFIG_TIME = 15.0f;
}

// Value keys
public enum ValueKey
{
    maximumDiscount
}

public class AppConfig
{
    public void LoadDefaults()
    {
        var appDefaults = new Dictionary<string, object>
        {
            {ValueKey.maximumDiscount.ToString(), false}
        };
        // Set default values
        DTDRemoteConfig.Defaults = appDefaults;
    }
    
    public bool GetBool(ValueKey key) => DTDRemoteConfig.Config[key.ToString()].BoolValue();
}

public class AppLogic : MonoBehaviour, IDTDRemoteConfigListener
{
    private readonly AppConfig _appConfig = new AppConfig();
    private const string APP_KEY = "appKey";
    private SimpleUI _simpleUI;

    private void Start()
    {
        DontDestroyOnLoad(this);
        _simpleUI = FindObjectOfType<SimpleUI>();
        if (_simpleUI == null) throw new NullReferenceException("UIManager not found.");
        // Set the maximum time of waiting for an A/B test group
        DTDRemoteConfig.GroupDefinitionWaiting = Constants.WAIT_GROUP_TIME;
        // Set the maximum time of waiting for the A/B test configuration
        DTDRemoteConfig.RemoteConfigWaiting = Constants.WAIT_CONFIG_TIME;
        // Set default values
        _appConfig.LoadDefaults();
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.InitializeWithAbTests(
            appKey: APP_KEY,
            configListener: this);

        // Show the loading indicator with timer.
        _simpleUI.ShowLoadingIndicator(Constants.WAIT_GROUP_TIME + Constants.WAIT_CONFIG_TIME);
    }

    public void UpdateAppUI()
    {
        if (_appConfig.GetBool(ValueKey.maximumDiscount))
        {
            // Display a discount badge clicking on which invokes a purchase window popup
        }
    }

    // Process the result of waiting for A/B test configuration.    
    public void OnReceived(DTDRemoteConfigReceiveResult result)
    {
        // If the attempt fails, hide the loading indicator and stop timer.
        if (result == DTDRemoteConfigReceiveResult.Failure)
        {
            _simpleUI.HideLoadingIndicator();
        }
    }

    // Prepare the app UI for changing the remote configuration    
    public void OnPrepareToChange()
    {
        // It is not used in current example
    }
    
    // Apply the values of the assigned group
    public void OnChanged(DTDRemoteConfigChangeResult result, string exceptionText = null)
    {
        // Hide the loading indicator and stop timer.
        _simpleUI.HideLoadingIndicator();
        switch (result)
        {
            case DTDRemoteConfigChangeResult.Failure:
                // Error processing
                if (exceptionText != null) Debug.LogError(exceptionText);
                break;
            case DTDRemoteConfigChangeResult.Success:
                // Apply new values
                DTDRemoteConfig.ApplyConfig();
                break;
        }
        
        UpdateAppUI();
    }
}

Note

When receiving the configuration, the SDK always calls the OnReceived(DTDRemoteConfigReceiveResult result) method, and when enrolling in a test - the OnPrepareToChange() and OnChanged(DTDRemoteConfigChangeResult result, string exceptionText = null) method. However, you can take some additional precocious measures. You can add a timer as in the following example:

public class SimpleUI : MonoBehaviour
{
    [SerializeField] private Canvas loadingIndicator;

    private Coroutine _timer;

    private IEnumerator HideAfterTimeout(float timeout, Canvas target)
    {
        yield return new WaitForSeconds(timeout);
        target.gameObject.SetActive(false);
    }

    public void ShowLoadingIndicator(float? timeout = null)
    {
        loadingIndicator.gameObject.SetActive(true);
        if (timeout != null)
            _timer = StartCoroutine(HideAfterTimeout(timeout.Value, loadingIndicator));
    }
    
    public void HideLoadingIndicator()
    {
        loadingIndicator.gameObject.SetActive(false);
        if (_timer != null) StopCoroutine(_timer);
    }
}
using DevToDev.Analytics;
using System.Collections.Generic;
using System.Diagnostics;

// Timer constants
static class Constants
{
    public const float WaitConfigConst = 10.0f;
    public const float WaitGroupConst = 15.0f;
}

// Value keys
static class ValueKeys
{
    public const string MaximumDiscount = "maximumDiscount";
}

static class AppConfg
{
    public static void LoadDafaults()
    {
        // Set default values
        DTDRemoteConfig.Defaults = new Dictionary<string, object>
        {
            [ValueKeys.MaximumDiscount] = false
        };
    }

    public static bool GetBoolValue(string key)
    {
        return DTDRemoteConfig.Config[key].BoolValue;
    }
}

class Application : IDTDRemoteConfigListener
{
    public void Run()
    {
        // Set the maximum time of waiting for an A/B test group
        DTDRemoteConfig.RemoteConfigWaiting = Constants.WaitConfigConst;
        // Set the maximum time of waiting for the A/B test configuration
        DTDRemoteConfig.GroupDefinitionWaiting = Constants.WaitGroupConst;
        // Set default values
        AppConfg.LoadDafaults();
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.InitializeWithAbTest("appKey", this);
        // Show launch screen (method do a job in UI thread)
        UI.ShowLaunchScreen();
    }

    public void ShowMainScene()
    {
        // Show main screen (also hide launch screen) (method do a job in UI thread)
        UI.ShowMainScreen();
        if (AppConfg.GetBoolValue(ValueKeys.MaximumDiscount))
        {
            // Display a discount badge clicking on which invokes a purchase window popup
            // (method do a job in UI thread)
            UI.ShowMaximumDiscount();
        }
    }

    // Apply the values of the assigned group
    public void OnChanged(DTDRemoteConfigChangeResult result, string error)
    {
        Debug.WriteLine($"[App-ABTests] OnChanged({result}, {error})");
        switch (result)
        {
            case DTDRemoteConfigChangeResult.Failure:
                // Error processing
                break;
            case DTDRemoteConfigChangeResult.Success:
                // Apply new values
                DTDRemoteConfig.ApplyConfig();
                break;
            default:
                break;
        }

        // Show main screen (also hide launch screen)
        ShowMainScene();
    }

    // Prepare the app UI for changing the remote configuration   
    public void OnPrepareToChange()
    {
        // It is not used in current example
        Debug.WriteLine($"[App-ABTests] OnPrepareToChange()");
    }

    // Process the result of waiting for A/B test configuration.   
    public void OnReceived(DTDRemoteConfigReceiveResult result)
    {
        Debug.WriteLine($"[App-ABTests] OnReceived({result})");
        // If the attempt fails, show main screen (also hide launch screen)
        if (result == DTDRemoteConfigReceiveResult.Failure)
        {
            ShowMainScene();
        }
    }
}

Note

When receiving the configuration, the SDK always calls the OnReceived(DTDRemoteConfigReceiveResult result) method, and when enrolling in a test - the OnPrepareToChange() and OnChanged(DTDRemoteConfigChangeResult result, string error). However, you can take some additional precocious measures. You can add a timer as in the following example:

public void showLaunchScreen() {
    // Add a timer that will forcibly launch the main logic of the application
    TimerUtil.CallWithDelay(Constants.WaitConfigConst + Constants.WaitGroupConst, () =>
    {
        UI.StartAppForReal();
    });
    UI.ShowActivityIndicator();
}
devtodev.remoteConfig.defaults = {
   maximumDiscount: false,
}
devtodev.remoteConfig.groupDefinitionWaiting = 10
devtodev.remoteConfig.remoteConfigWaiting = 15
devtodev.initializeWithAbTest(
    appKey, 
    {
        userId: userId,
        logLevel: logLevel,
        trackingAvailability: trackingAvailability,
    },
    {
        // Process the result of waiting for A/B test configuration
        onReceived: function(result) {
            // It is not used in current example
        },
        // Prepare the app UI for changing the remote configuration
        onPrepareToChange: function() {
            // Display the progress indicator
            ui.showSpinner()
        },
        // Apply the values of the assigned group
        onChanged: function(result, error) {
            ui.hideSpinner()
            switch (result) {
              case DTDRemoteConfigChangeResult.Failure:
                  // Error processing
                  console.error(error);
                  break;
              case DTDRemoteConfigChangeResult.Success:
                  // Apply new values
                  devtodev.remoteConfig.applyConfig()
                  break;
            }
            updateAppUI();
        }
    }
)
function updateAppUI() {
    var maximumDiscount = window.devtodev.remoteConfig.config['maximumDiscount'].boolValue
    if (maximumDiscount) {
        // Display a discount badge clicking on which invokes a purchase window popup
    }
}
// Timer constants
static float WAIT_CONFIG_TIME = 15.0f;
static float WAIT_GROUP_TIME = 10.0f;

void SomeLogicClass::Start() {
    // Set the maximum time of waiting for an A/B test group
    UDTDRemoteConfigBPLibrary::SetRemoteConfigWaiting(WAIT_CONFIG_TIME);
    // Set the maximum time of waiting for the A/B test configuration
    DTDRemoteConfigBPLibrary::SetGroupDefinitionWaiting(WAIT_GROUP_TIME);

    // Set default values
    FDTDRemoteConfigDefaults Defaults;
    Defaults.BoolDefaults.Add("maximumDiscount", false);
    UDTDRemoteConfigBPLibrary::SetDefaults(Defaults);

    const auto onConfigReceive = new FDTDRemoteConfigReceiveResultDelegate();
    onConfigReceive->BindUObject(this, &SomeLogicClass::OnConfigReceive);

    const auto onPrepareToChange = new FDTDRemoteConfigPrepareToChangeDelegate();
    onPrepareToChange->BindUObject(this, &SomeLogicClass::OnPrepareToChange);

    const auto onConfigChange = new FDTDRemoteConfigChangeResultDelegate();
    onConfigChange->BindUObject(this, &SomeLogicClass::OnConfigChange);

    // Initialize the SDK for working with A/B testing
    UDTDAnalyticsBPLibrary::InitializeWithAbTest("AppKey", *onConfigChange, *onPrepareToChange, *onConfigReceive);

    // Show the loading indicator with timer.
    SomeUI::showLaunchScreen(WAIT_CONFIG_TIME + WAIT_GROUP_TIME);
}

void SomeLogicClass::UpdateAppUI()
{
    if (UDTDRemoteConfigBPLibrary::GetRemoteConfigValue("maximumDiscount").BoolValue)
    {
        // Display a discount badge clicking on which invokes a purchase window popup
    }
}

// Process the result of waiting for A/B test configuration
void SomeLogicClass::OnConfigReceive(EDTDRemoteConfigReceiveResult result) {
    // If the attempt fails, hide the loading indicator and stop timer.
    if (result == EDTDRemoteConfigReceiveResult::Failure)
    {
        SomeUI::HideLoadingIndicator();
    }
}

// Prepare the app UI for changing the remote configuration
void SomeLogicClass::OnPrepareToChange() {
    // It is not used in current example
}

// Apply the values of the assigned group
void SomeLogicClass::OnConfigChange(EDTDRemoteConfigChangeResult result, const FString& error) {
    // Hide the loading indicator and stop timer.
    SomeUI::HideLoadingIndicator();
    switch (result)
    {
    case EDTDRemoteConfigChangeResult::Success:
        // Apply new values
	UDTDRemoteConfigBPLibrary::ApplyConfig();
	break;
	
    case EDTDRemoteConfigChangeResult::Failure:
	// Error processing
	UE_LOG(LogTemp, Warning, TEXT("DTDRemoteConfigError: %s"), *error);
	break;

    default:
    	break;
    }

    UpdateAppUI();
}
# Control timer
var timer = Timer.new()
# Timer constants
var waitConfigConst = 10.0
var waitGroupConst = 15.0

var maximumDiscount = "maximumDiscount"

func loadDefaults():
	var appDefaults = GDDTDRemoteConfigDefaults.new()
	# Set default values
	appDefaults.AddBoolValue(maximumDiscount, false)
	DTDRemoteConfig.SetDefaults(appDefaults)
	
func getBool(key: String) -> bool: 
	return DTDRemoteConfig.GetRemoteConfigValue(key).GetBoolValue()
	
func viewDidLoad():
	#Display the launch screen
	showLaunchScreen()
	
#Launching the main logic of the application
func startAppForReal():
# Update app UI
	updateAppUI()
# Hide launch screen
	hideLaunchScreen()
	
func updateAppUI() :
	if getBool(maximumDiscount):
	# Display a discount badge clicking on which invokes a purchase window popup
		pass

func _ready():
		# Set the maximum time of waiting for the A/B test configuration
	DTDRemoteConfig.SetRemoteConfigWaiting(waitConfigConst)
		# Set the maximum time of waiting for an A/B test group
	DTDRemoteConfig.SetGroupDefinitionWaiting(waitGroupConst)
		# Set default values
	loadDefaults()
		# Initialize the SDK for working with A/B testing
	var config = GDDTDAnalyticsConfiguration.new()
	DTDAnalytics.InitializeWithConfigWithAbTest("appKey",
	config,
	onRemoteConfigChange,
	onRemoteConfigPrepareToChange,
	onRemoteConfigReceive)

func onRemoteConfigChange(result: GDDTDRemoteConfigChangeResult.ChangeResult, error: String):
	match result:
		GDDTDRemoteConfigChangeResult.Success:
			# Apply new values
			DTDRemoteConfig.ApplyConfig()
		GDDTDRemoteConfigChangeResult.Failure:
			# Error processing
			print(error)
			
	startAppForReal()

# Prepare the app UI for changing the remote configuration
func onRemoteConfigPrepareToChange():
	# It is not used in current example
	pass

# Process the result of waiting for A/B test configuration
func onRemoteConfigReceive(result: GDDTDRemoteConfigReceiveResult.ReceiveResult):
	if (result == GDDTDRemoteConfigReceiveResult.Failure):
		# If the attempt fails, launch the main logic of the application
		startAppForReal()
		
func showLaunchScreen():
	# Add a timer that will forcibly launch the main logic of the application
	timer.connect("ActivityIndicator", startAppForReal)
	timer.one_shot = true
	timer.wait_time = waitConfigConst + waitGroupConst
	add_child(timer)
	timer.start()
	showActivityIndicator()

Example №3

Hypothesis

Our analytics data show that only 60% of our users complete the tutorial. We hypothesize that if we make it easier (group A) or reduce the number of seps (group B), we will increase the percentage of tutorial completion.

Test criteria

New users who have not started the tutorial yet.

Groups

Control group: current version with usual difficulty and usual number of steps

Group А: low difficulty, usual number of steps

Group B: usual difficulty, few steps

Implementation

// Timer constants
struct Constants {
    static let waitConfigConst = 10.0
}

// Value keys
enum ValueKey: String {
    case difficulty
    case stepCount
}

class AppConfig {
    static func loadDefaults() {
        func loadDefaultValues() {
        let appDefaults: [String: Any] = [
            ValueKey.difficulty.rawValue: 3,
            ValueKey.stepCount.rawValue: 10
        ]
        // Set default values
        DTDRemoteConfig.defaults = appDefaults
    }
    
    func integer(forKey key: ValueKey) -> Int {
        DTDRemoteConfig.config[key.rawValue].integerValue
    }
}

class AppLogic {
    // Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
    func startTutorial() {
        // Display the download progress indicator
        showActivityIndicator()
        // Send a trigger event that indicates tutorial start
        DTDAnalytics.tutorial(step: -1)
    }

    // Initiate tutorial completion
    func realStartTutorial() {
        // Hide the download progress indicator
        hideActivityIndicator()
        // Initiate the tutorial by using the remote values (difficulty and stepCount)
        runTutorial(stepCount: integer(forKey: .stepCount), 
                    difficulty: integer(forKey: .difficulty))
    }
}

class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication,
        willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Set the maximum time of waiting for the A/B test configuration
        DTDRemoteConfig.remoteConfigWaiting = Constants.waitConfigConst
        // Set default values
        AppConfig.loadDefaults()
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.initializeWithAbTest(applicationKey: "appKey",
                                          configuration: config,
                                          abConfigListener: self)
    }
}

extension AppLogic: DTDRemoteConfigListener {
    // Process the result of waiting for A/B test configuration
    func onReceived(result: DTDRemoteConfigReceiveResult) {
        // It is not used in current example
    }

    // Prepare the app UI for changing the remote configuration
    func onPrepareToChange() {
        // It is not used in current example
    }
    
    // Apply the values of the assigned group
    func onChanged(result: DTDRemoteConfigChangeResult, error: Error?) {
        defer {
            // Initiate tutorial completion
            DispatchQueue.main.async { [weak self] in
                self?.realStartTutorial()
            }
        }

        switch result {
        case .success:
            // Apply new values
            DTDRemoteConfig.applyConfig()

        case .failure:
            // Error processing
            if let error = error {
                print(error.localizedDescription)
            }

        @unknown default:
            break
        }
    }
}
// Constants .h + .m
@interface Constants : NSObject
// Timer constants
extern double const waitConfigConst;
// Value keys
extern NSString * const difficulty;
extern NSString * const stepCount;
@end

@implementation Constants
double const waitConfigConst = 10.0;
NSString * const difficulty = @"difficulty";
NSString * const stepCount = @"stepCount";
@end

// AppConfig .h + .m
@interface AppConfig: NSObject
+(void) loadDefaults;
+(NSInteger) getIntegerForKey:(NSString *) key;
@end

@implementation AppConfig
+(void)loadDefaults {
    NSDictionary *appDefaults = @{
        difficulty: @3,
        stepCount: @10
    };

    // Set default values
    DTDRemoteConfig.defaults = appDefaults;
}

+(NSInteger) getIntegerForKey:(NSString *) key {
    return DTDRemoteConfig.config[key].integerValue;
}
@end

// AppLogic .h + .m
@interface AppLogic : UIViewController <DTDRemoteConfigListener>
@end

@implementation AppLogic

- (void)viewDidLoad {
    [super viewDidLoad];
    // Display the launch screen
    [self startTutorial];
}

// Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
- (void) startTutorial {
    // Display the download progress indicator
    [self showActivityIndicator];
    // Send a trigger event that indicates tutorial start
    [DTDAnalytics tutorialStep:-1];
}

// Initiate tutorial completion
- (void) realStartTutorial {
    // Hide the download progress indicator
    [self hideActivityIndicator];
    // Initiate the tutorial by using the remote values (difficulty and stepCount)
    [self runTutorial: [AppConfig getIntegerForKey:stepCount]
       withDifficulty: [AppConfig getIntegerForKey:difficulty]];
}

- (void) runTutorial:(NSInteger) stepCount withDifficulty:(NSInteger) difficulty {
   // Start tutorial
}

// Process the result of waiting for A/B test configuration
- (void)onReceivedResult:(enum DTDRemoteConfigReceiveResult)result {
    // It is not used in current example
}

// Prepare the app UI for changing the remote configuration
- (void)onPrepareToChange {
    // It is not used in current example
}

// Apply the values of the assigned group
- (void)onChangedResult:(enum DTDRemoteConfigChangeResult)result error:(NSError *)error {
    switch (result) {
        case DTDRemoteConfigChangeResultSuccess:
            // Apply new values
            [DTDRemoteConfig applyConfig];
            break;

        case DTDRemoteConfigChangeResultFailure:
            // Error processing
            if (error) {
                NSLog(@"DTDRemoteConfigError: %@", error.localizedDescription);
            }

        default:
            break;
    }

    // Initiate tutorial completion
    __weak AppLogic *weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        [weakSelf realStartTutorial];
    });
}

- (void)showActivityIndicator {
    // Display the launch screen
}

- (void)hideActivityIndicator {
    // Hide launch screen
}
@end

// AppDelegate .h + .m
@interface AppDelegate ()
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    AppLogic *appLogicController = [[UIStoryboard storyboardWithName:@"Main" bundle:nil] instantiateViewControllerWithIdentifier:@"AppLogic"];
    UINavigationController *navController = [[UINavigationController alloc]initWithRootViewController:appLogicController];
    self.window.rootViewController = navController;
    [self.window makeKeyAndVisible];

    // Set the maximum time of waiting for the A/B test configuration
    DTDRemoteConfig.remoteConfigWaiting = waitConfigConst;
    // Implementation defaults params
    [AppConfig loadDefaults];
    [DTDAnalytics applicationKey:appKey abConfigListener:appLogicController];

    return YES;
}
@end
class Constants {
    // Timer constants
    companion object {
        const val waitConfigConst = 10.0
        const val waitConfigConstInMilliseconds = 10000L
    }
}

enum class ValueKey(val value: String) {
    Difficulty("difficulty"),
    StepCount("stepCount")
}

class AppConfig {
    companion object {
        fun loadDefaults() {
            val appDefaults = mapOf<String, Any>(
                ValueKey.Difficulty.value to 3,
                ValueKey.StepCount.value to 10
            )
            // Set default values
            DTDRemoteConfig.defaults = appDefaults
        }

        fun integer(key: ValueKey): Int {
            return DTDRemoteConfig.config[key.value].intValue
        }
    }
}

class AppLogic {
    // Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
    fun startTutorial() {
        // Display the download progress indicator
        showActivityIndicator()
        // Send a trigger event that indicates tutorial start
        DTDAnalytics.tutorial(step = -1)
    }


    // Initiate tutorial completion
    fun realStartTutorial() {
        // Hide the download progress indicator
        hideActivityIndicator()
        // Initiate the tutorial by using the remote values (difficulty and stepCount)
        runTutorial(
            stepCount = AppConfig.integer(ValueKey.StepCount),
            difficulty = AppConfig.integer(ValueKey.Difficulty)
        )
    }
}

class MainActivity : AppCompatActivity(), DTDRemoteConfigListener {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState, persistentState)
        setContentView(R.layout.activity_main)

        // Set the maximum time of waiting for the A/B test configuration
        DTDRemoteConfig.remoteConfigWaiting = Constants.waitConfigConst
        // Set default values
        AppConfig.loadDefaults()
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.initializeWithAbTest(
            appKey = "appKey",
            context = this,
            abConfigListener = this
        )
    }

    // Process the result of waiting for A/B test configuration
    override fun onReceived(result: DTDRemoteConfigReceiveResult) {
        // It is not used in current example
    }


    // Prepare the app UI for changing the remote configuration
    override fun onPrepareToChange() {
        // It is not used in current example
    }

    override fun onChanged(result: DTDRemoteConfigChangeResult, ex: Exception?) {
        when (result) {
            DTDRemoteConfigChangeResult.Success -> {
                // Apply new values
                DTDRemoteConfig.applyConfig()
            }
            DTDRemoteConfigChangeResult.Failure -> {
                // Error processing
                ex?.let { Log.e("TAG", ex.toString()) }
            }
        }

        runOnUiThread {
            realStartTutorial()
        }
    }
}
class Constants {
    // Timer constants
    final static double waitGroupConst = 10.0;
    final static long waitGroupConstInMilliseconds = 10000L;
}

enum ValueKey {
    Difficulty("difficulty"),
    StepCount("stepCount");

    private final String stringValue;

    ValueKey(String toString) {
        stringValue = toString;
    }

    @Override
    public String toString() {
        return stringValue;
    }
}

class AppConfig {
    static void loadDefaults() {
        HashMap<String, Object> map = new HashMap<>();
        map.put(ValueKey.Difficulty.toString(), 3);
        map.put(ValueKey.StepCount.toString(), 10);
        DTDRemoteConfig.INSTANCE.setDefaults(map);
    }

    static Integer integer(ValueKey key) {
        return DTDRemoteConfig.INSTANCE.getConfig().get(key.toString()).getIntValue();
    }
}

class AppLogic {
    // Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
    static void startTutorial() {
        // Display the download progress indicator
        showActivityIndicator();
        // Send a trigger event that indicates tutorial start
        DTDAnalytics.INSTANCE.tutorial(-1);
    }

    // Initiate tutorial completion
    static void realStartTutorial() {
        // Hide the download progress indicator
        hideActivityIndicator();
        // Initiate the tutorial by using the remote values (difficulty and stepCount)
        runTutorial(
                AppConfig.integer(ValueKey.StepCount),
                AppConfig.integer(ValueKey.Difficulty)
        );
    }
}

class MainActivity extends AppCompatActivity implements DTDRemoteConfigListener {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Set the maximum time of waiting for the A/B test configuration
        DTDRemoteConfig.INSTANCE.setRemoteConfigWaiting(Constants.waitGroupConst);
        // Set default values
        AppConfig.loadDefaults();
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.INSTANCE.initializeWithAbTest("appKey", this, this);
    }

    // Process the result of waiting for A/B test configuration
    @Override
    public void onReceived(@NonNull DTDRemoteConfigReceiveResult result) {
        // It is not used in current example
    }

    // Prepare the app UI for changing the remote configuration
    @Override
    public void onPrepareToChange() {
        // It is not used in current example
    }

    @Override
    public void onChanged(@NonNull DTDRemoteConfigChangeResult result, @Nullable Exception ex) {
        if (result == DTDRemoteConfigChangeResult.Success) {
            // Apply new values
            DTDRemoteConfig.INSTANCE.applyConfig();
        }

        if (result == DTDRemoteConfigChangeResult.Failure) {
            // Error processing
            if (ex != null) {
                Log.e("TAG", ex.toString());
            }
        }

        runOnUiThread(AppLogic::realStartTutorial);
    }
}
// Timer constants
public static class Constants
{
    public const float WAIT_CONFIG_TIME = 10.0f;
}
    
// Value keys
public enum ValueKey
{
    difficulty,
    stepCount
}

public class AppConfig
{
    public void LoadDefaults()
    {
        var appDefaults = new Dictionary<string, object>
        {
            {ValueKey.difficulty.ToString(), 3},
            {ValueKey.stepCount.ToString(), 10}
        };
        // Set default values
        DTDRemoteConfig.Defaults = appDefaults;
    }
    
    public int GetInt(ValueKey key) => DTDRemoteConfig.Config[key.ToString()].IntValue();
}

public class AppLogic : MonoBehaviour, IDTDRemoteConfigListener
{
    private readonly AppConfig _appConfig = new AppConfig();
    private const string APP_KEY = "appKey";
    private SimpleUI _simpleUI;
    
    private void Start()
    {
        DontDestroyOnLoad(this);
        _simpleUI = FindObjectOfType<SimpleUI>();
        if (_simpleUI == null) throw new NullReferenceException("UIManager not found.");
        // Set the maximum time of waiting for the A/B test configuration
        DTDRemoteConfig.RemoteConfigWaiting = Constants.WAIT_CONFIG_TIME;
        // Set default values
        _appConfig.LoadDefaults();
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.InitializeWithAbTests(
            appKey: APP_KEY,
            analyticsConfiguration: _analyticsConfiguration,
            configListener: this);
    }

    // Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
    public void StartTutorial()
    {
        // Send a trigger event that indicates tutorial start
        DTDAnalytics.Tutorial(-1);
        // Display the download progress indicator
        _simpleUI.ShowLoadingIndicator(Constants.WAIT_CONFIG_TIME);
    }
    
    // Initiate tutorial completion
    private void RealStartTutorial()
    {
        // Hide the download progress indicator
        _simpleUI.HideLoadingIndicator();
        // Initiate the tutorial by using the remote values (difficulty and stepCount)
        RunTutorial(
            stepCount: DTDRemoteConfig.Config[ValueKey.stepCount].IntValue(),
            difficulty: DTDRemoteConfig.Config[ValueKey.difficulty].IntValue()
        );
    }

    // Process the result of waiting for A/B test configuration
    public void OnReceived(DTDRemoteConfigReceiveResult result)
    {
        // It is not used in current example.
    }

    // Prepare the app UI for changing the remote configuration
    public void OnPrepareToChange()
    {
        // It is not used in current example
    }

    // Apply the values of the assigned group
    public void OnChanged(DTDRemoteConfigChangeResult result, string exceptionText = null)
    {
        switch (result)
        {
            case DTDRemoteConfigChangeResult.Failure:
                // Error processing
                if (exceptionText != null) Debug.LogError(exceptionText);
                break;
            case DTDRemoteConfigChangeResult.Success:
                // Apply new values
                DTDRemoteConfig.ApplyConfig();
                break;
        }
        
        // Initiate tutorial completion
        RealStartTutorial();
    }
}
using DevToDev.Analytics;
using System.Collections.Generic;
using System.Diagnostics;

// Timer constants
static class Constants
{
    public const float WaitConfigConst = 10.0f;
}

// Value keys
static class ValueKeys
{
    public const string Difficulty = "difficulty";
    public const string StepCount = "stepCount";
}

static class AppConfg
{
    public static void LoadDafaults()
    {
        // Set default values
        DTDRemoteConfig.Defaults = new Dictionary<string, object>
        {
            [ValueKeys.Difficulty] = 3,
            [ValueKeys.StepCount] = 10
        };
    }

    public static int GetIntValue(string key)
    {
        return DTDRemoteConfig.Config[key].Int32Value;
    }
}

class Application : IDTDRemoteConfigListener
{
    public void Run()
    {
        // Set the maximum time of waiting for the A/B test configuration
        DTDRemoteConfig.RemoteConfigWaiting = Constants.WaitConfigConst;
        // Set default values
        AppConfg.LoadDafaults();
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.InitializeWithAbTest("appKey", this);
        // Prepare tutorial scene to show
        PrepareTutorialScene();
    }

    // Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
    public void PrepareTutorialScene()
    {
        // Display the download progress indicator
        UI.ShowLoadingIndicator();
        // Send a trigger event that indicates tutorial start
        DTDAnalytics.Tutorial(-1);
    }

    // Initiate tutorial completion
    public void StartTutorialScene()
    {
        var difficulty = AppConfg.GetIntValue(ValueKeys.Difficulty);
        var stepCount = AppConfg.GetIntValue(ValueKeys.StepCount);
        // Hide the download progress indicator (method do a job in UI thread)
        UI.HideLoadingIndicator();
        // Initiate the tutorial by using the remote values (method do a job in UI thread)
        UI.ShowTutorialScreen(difficulty, stepCount);
    }
    
    // Apply the values of the assigned group
    public void OnChanged(DTDRemoteConfigChangeResult result, string error)
    {
        Debug.WriteLine($"[App-ABTests] OnChanged({result}, {error})");
        switch (result)
        {
            case DTDRemoteConfigChangeResult.Failure:
                // Error processing
                break;
            case DTDRemoteConfigChangeResult.Success:
                // Apply new values
                DTDRemoteConfig.ApplyConfig();
                break;
            default:
                break;
        }

        // Initiate tutorial completion
        StartTutorialScene();
    }

    // Prepare the app UI for changing the remote configuration
    public void OnPrepareToChange()
    {
        Debug.WriteLine($"[App-ABTests] OnPrepareToChange()");
         // It is not used in current example
    }

    
    // Process the result of waiting for A/B test configuration
    public void OnReceived(DTDRemoteConfigReceiveResult result)
    {
        Debug.WriteLine($"[App-ABTests] OnReceived({result})");
         // It is not used in current example
    }
}
devtodev.remoteConfig.defaults = {
   difficulty: 3,
   stepCount: 10
}
devtodev.remoteConfig.remoteConfigWaiting = 10
devtodev.initializeWithAbTest(
    appKey, 
    {
        userId: userId,
        logLevel: logLevel,
        trackingAvailability: trackingAvailability,
    },
    {
        // Process the result of waiting for A/B test configuration
        onReceived: function(result) {
            // It is not used in current example
        },
        // Prepare the app UI for changing the remote configuration
        onPrepareToChange: function() {
        },
        // Apply the values of the assigned group
        onChanged: function(result, error) {
            ui.hideSpinner()
            switch (result) {
              case DTDRemoteConfigChangeResult.Failure:
                  // Error processing
                  console.error(error);
                  break;
              case DTDRemoteConfigChangeResult.Success:
                  // Apply new values
                  devtodev.remoteConfig.applyConfig()
                  var config = window.devtodev.remoteConfig.config // DTDRemoteConfigCollection
                  break;
            }
            realStartTutorial();
        }
    }
)
function startTutorial() {
    devtodev.tutorial(parseInt(-1))
    // Display the progress indicator
    ui.showSpinner()
}
// Initiate tutorial completion
function realStartTutorial() {
    // Hide the download progress indicator
    ui.hideSpinner();
    var stepCount = window.devtodev.remoteConfig.config['stepCount'].intValue
    var difficulty = window.devtodev.remoteConfig.config['difficulty'].intValue
    // Initiate the tutorial by using the remote values (difficulty and stepCount)
    runTutorial(stepCount, difficulty);
}
// Timer constant
const float WAIT_CONFIG_TIME = 10.0f;
    
void SomeLogicClass::Start() {
    // Set the maximum time of waiting for the A/B test configuration
    DTDRemoteConfigBPLibrary::SetRemoteConfigWaiting(WAIT_CONFIG_TIME);

    // Set default values
    FDTDRemoteConfigDefaults Defaults;
    Defaults.IntegerDefaults.Add("difficulty", 3);
    Defaults.IntegerDefaults.Add("stepCount", 10);
    UDTDRemoteConfigBPLibrary::SetDefaults(Defaults);

    const auto onConfigReceive = new FDTDRemoteConfigReceiveResultDelegate();
    onConfigReceive->BindUObject(this, &SomeLogicClass::OnConfigReceive);

    const auto onPrepareToChange = new FDTDRemoteConfigPrepareToChangeDelegate();
    onPrepareToChange->BindUObject(this, &SomeLogicClass::OnPrepareToChange);

    const auto onConfigChange = new FDTDRemoteConfigChangeResultDelegate();
    onConfigChange->BindUObject(this, &SomeLogicClass::OnConfigChange);

    // Initialize the SDK for working with A/B testing
    UDTDAnalyticsBPLibrary::InitializeWithAbTest("AppKey", *onConfigChange, *onPrepareToChange, *onConfigReceive);
}

// Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
void SomeLogicClass::StartTutorial()
{
    // Send a trigger event that indicates tutorial start
    UDTDAnalyticsBPLibrary::Tutorial(-1);
    // Display the download progress indicator
    SomeUI::ShowLoadingIndicator(WAIT_CONFIG_TIME);
}

// Initiate tutorial completion
void SomeLogicClass::RealStartTutorial()
{
    // Hide the download progress indicator
    SomeUI::HideLoadingIndicator();
    // Initiate the tutorial by using the remote values (difficulty and stepCount)
    RunTutorial(UDTDRemoteConfigBPLibrary::GetRemoteConfigValue("difficulty").IntegerValue,
		UDTDRemoteConfigBPLibrary::GetRemoteConfigValue("stepCount").IntegerValue);
}


// Process the result of waiting for A/B test configuration
void SomeLogicClass::OnConfigReceive(EDTDRemoteConfigReceiveResult result) {
    // It is not used in current example.
}

// Prepare the app UI for changing the remote configuration
void SomeLogicClass::OnPrepareToChange() {
    // It is not used in current example
}

// Apply the values of the assigned group
void SomeLogicClass::OnConfigChange(EDTDRemoteConfigChangeResult result, const FString& error) {
    switch (result)
    {
    case EDTDRemoteConfigChangeResult::Success:
    	// Apply new values
	UDTDRemoteConfigBPLibrary::ApplyConfig();
	break;
	
    case EDTDRemoteConfigChangeResult::Failure:
	// Error processing
	UE_LOG(LogTemp, Warning, TEXT("DTDRemoteConfigError: %s"), *error);
	break;

    default:
    	break;
    }

    // Initiate tutorial completion
    RealStartTutorial();
}
# Timer constant
var waitConfigConst = 10.0

var difficulty = "difficulty"
var stepCount = "stepCount"

func loadDefaults():
	var appDefaults = GDDTDRemoteConfigDefaults.new()
	# Set default values
	appDefaults.AddIntegerValue(difficulty, 3)
	appDefaults.AddIntegerValue(stepCount, 10)
	DTDRemoteConfig.SetDefaults(appDefaults)
	
	
func getInteger(key: String) -> int: 
	return DTDRemoteConfig.GetRemoteConfigValue(key).GetIntValue()

# Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
func startTutorial():
	# Display the download progress indicator
	showActivityIndicator()
	#Send a trigger event that indicates tutorial start
	DTDAnalytics.Tutorial(-1)

#  Initiate tutorial completion
func realStartTutorial():
	# Hide the download progress indicator
	hideActivityIndicator()
	#Initiate the tutorial by using the remote values (difficulty and stepCount)
	var stepCount = getInteger(stepCount)
	var difficulty = getInteger(stepCount)

func _ready():
		# Set the maximum time of waiting for an A/B test group
	DTDRemoteConfig.SetRemoteConfigWaiting(waitConfigConst)
		# Set default values
	loadDefaults()
		# Initialize the SDK for working with A/B testing
	DTDAnalytics.InitializeWithAbTest(appKey",
	onRemoteConfigChange,
	onRemoteConfigPrepareToChange,
	onRemoteConfigReceive)

func onRemoteConfigChange(result: GDDTDRemoteConfigChangeResult.ChangeResult, error: String):
	match result:
		GDDTDRemoteConfigChangeResult.Success:
			# Apply new values
			DTDRemoteConfig.ApplyConfig()
		GDDTDRemoteConfigChangeResult.Failure:
			# Error processing
			print(error)
				
	realStartTutorial()

# Prepare the app UI for changing the remote configuration
func onRemoteConfigPrepareToChange():
	# It is not used in current example
	pass

# Process the result of waiting for A/B test configuration
func onRemoteConfigReceive(result: GDDTDRemoteConfigReceiveResult.ReceiveResult):
	# It is not used in current example
	pass