Skip to main content
Learn how to integrate and use the Stringboot Android SDK in your Android application with practical examples. This is the recommended approach for most use cases. Use android:tag attributes in your XML layouts and call applyStringbootTags() in your Activity. XML Layout (activity_main.xml):
<!-- Greeting with Stringboot tag -->
<TextView
    android:id="@+id/greeting"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/hello_user"
    android:tag="hello_user"
    android:textSize="22sp"
    android:textStyle="bold" />

<!-- Welcome message -->
<TextView
    android:id="@+id/welcome"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/welcome_message"
    android:tag="welcome_message"
    android:textStyle="bold"
    android:textSize="22sp" />

<!-- Offer title in card -->
<TextView
    android:id="@+id/offer_text1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:tag="offer_title"
    android:text="@string/offer_title"
    android:textColor="@android:color/white"
    android:textSize="18sp"
    android:textStyle="bold" />
Activity Code (MainActivity.kt):
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private var currentLanguage = "en"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Load saved language preference
        currentLanguage = prefs.getString("current_language", "en") ?: "en"
        StringProvider.setLocale(currentLanguage)

        enableEdgeToEdge()
        window.statusBarColor = Color.TRANSPARENT
        window.navigationBarColor = Color.TRANSPARENT

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Auto-apply Stringboot to all TextViews with android:tag
        binding.root.applyStringbootTags()

        // Set up UI
        setupLanguageButton()
        setupLanguageDisplay()
        setupFAQButton()

        // Load initial strings
        loadStrings()
    }
}
How it works:
  1. Set android:tag="string_key" on any TextView in XML
  2. Call binding.root.applyStringbootTags() in your Activity
  3. All tagged TextViews automatically load strings from StringProvider
  4. UI updates automatically when language changes

Get String (Synchronous)

Use StringProvider.get() for synchronous string access. This method is NOT a suspend function and returns immediately from cache.
// Direct synchronous string access
dialogBinding.dialogTitle.text = StringProvider.get("language_dialog_title")
dialogBinding.dialogSubtitle.text = StringProvider.get("language_dialog_subtitle")
Key Points:
  • get() is synchronous (not suspend)
  • Returns immediately from cache
  • Falls back to database if not in memory cache
  • Returns "??key??" if string not found
  • Can optionally fetch from network with allowNetworkFetch = true
Full Signature:
val text = StringProvider.get(
    key = "welcome_message",
    lang = "en",  // Optional, defaults to device locale
    allowNetworkFetch = false  // Optional, defaults to false
)

Reactive Flow for Auto-Updating UI

Use Kotlin Flow to reactively update UI when language changes or network sync completes.
private fun setupLanguageDisplay() {
    // Cancel previous observation if exists
    languageDisplayJob?.cancel()

    languageDisplayJob = lifecycleScope.launch {
        // Use Flow to reactively update the language display
        StringProvider.getFlow("status_current_language", currentLanguage)
            .collect { template ->
                val displayText = if (template.contains("%s")) {
                    template.format(getLanguageDisplayName(currentLanguage))
                } else {
                    template
                }
                binding.tvCurrentLanguage.text = displayText
            }
    }
}
Benefits:
  • Automatically updates when language changes
  • Updates when network sync completes
  • Lifecycle-aware (cancels when activity destroyed)
  • Perfect for dynamic content

Complete Language Switching Pattern

Switch languages while maintaining smooth UI transitions and preventing content flashing.
private fun switchLanguage(newLanguage: ActiveLanguage) {
    lifecycleScope.launch {
        try {
            Toast.makeText(
                this@MainActivity,
                "Switching to ${newLanguage.name}...",
                Toast.LENGTH_SHORT
            ).show()

            // Update current language
            currentLanguage = newLanguage.code
            StringProvider.setLocale(currentLanguage)

            // Preload cache with existing strings to avoid flash
            StringProvider.preloadLanguage(currentLanguage, maxStrings = 500)

            // Refresh from network in background
            val refreshSuccess = StringProvider.refreshFromNetwork(currentLanguage)
            if (!refreshSuccess) {
                StringbootLogger.w("Network refresh failed, using cached/local strings")
            }

            // Restart language display observation for new language
            setupLanguageDisplay()

            // Re-apply tags to refresh UI with new language
            withContext(Dispatchers.Main) {
                binding.root.applyStringbootTags()
            }

            // Save language preference
            prefs.edit { putString("current_language", currentLanguage) }

        } catch (e: Exception) {
            StringbootLogger.e("Error switching language", e)
            Toast.makeText(
                this@MainActivity,
                "Failed to switch language",
                Toast.LENGTH_SHORT
            ).show()
        }
    }
}
Key Steps:
  1. Set locale with setLocale()
  2. Preload language to avoid UI flash
  3. Refresh from network (non-blocking)
  4. Restart Flow observations
  5. Re-apply tags to update all TextViews
  6. Save preference for next app launch

Get Available Languages

Retrieve available languages with automatic fallback to cached languages.
private suspend fun getAvailableLanguages(): List<ActiveLanguage> {
    return withContext(Dispatchers.IO) {
        try {
            // Try to get languages from server first
            val serverLanguages = StringProvider.getAvailableLanguagesFromServer()

            if (serverLanguages.isNotEmpty()) {
                StringbootLogger.i("Retrieved ${serverLanguages.size} languages from server")
                return@withContext serverLanguages
            }

            // Fallback to locally cached languages
            val cachedCodes = StringProvider.getAvailableLanguages()
            if (cachedCodes.isNotEmpty()) {
                StringbootLogger.i("Using ${cachedCodes.size} cached languages")
                return@withContext cachedCodes.map { code ->
                    ActiveLanguage(
                        code = code,
                        name = getLanguageDisplayName(code),
                        isActive = true
                    )
                }
            }

            // Ultimate fallback: English only
            StringbootLogger.w("No languages available, defaulting to English")
            listOf(ActiveLanguage(code = "en", name = "English", isActive = true))

        } catch (e: Exception) {
            StringbootLogger.e("Error getting available languages", e)
            listOf(ActiveLanguage(code = "en", name = "English", isActive = true))
        }
    }
}
Three-tier fallback:
  1. Server languages (fresh data)
  2. Cached language codes
  3. English-only fallback

FAQ Management

Load and display FAQs with tag-based filtering and optional language selection.
private fun loadFAQs() {
    lifecycleScope.launch {
        try {
            // Fetch FAQs using FAQProvider
            val faqs = withContext(Dispatchers.IO) {
                FAQProvider.getFAQs(
                    tag = currentTag,
                    subTags = selectedSubTags,
                    lang = currentLanguage,
                    allowNetworkFetch = true
                )
            }

            StringbootLogger.d("Loaded ${faqs.size} FAQs for tag: $currentTag, subTags: $selectedSubTags")

            // Update UI with FAQs
            faqAdapter.updateFAQs(faqs)

            if (faqs.isEmpty()) {
                Toast.makeText(
                    this@FAQDemoActivity,
                    "No FAQs found for tag: $currentTag",
                    Toast.LENGTH_SHORT
                ).show()
            }
        } catch (e: Exception) {
            StringbootLogger.e("Error loading FAQs: ${e.message}", e)
            Toast.makeText(
                this@FAQDemoActivity,
                "Error loading FAQs: ${e.message}",
                Toast.LENGTH_LONG
            ).show()
        }
    }
}
FAQ Filtering Examples:
// Filter by tag only
FAQProvider.getFAQs(tag = "Identity Verification", lang = "en")

// Filter by tag and subTags
FAQProvider.getFAQs(
    tag = "Identity Verification",
    subTags = listOf("AE", "refunds", "disputes"),
    lang = "en",
    allowNetworkFetch = true
)

Dynamic UI with sbTextView

Create dynamic TextViews that automatically load and update strings programmatically.
// Create dynamic sbTextView
val stringView = sbTextView(context).apply {
    setKey("app_name", "Stringboot")
    setPadding(16)
    setBackgroundColor(0xFFEEEEEE.toInt())
}
cardContent.addView(stringView)
sbTextView Features:
  • Loads strings from StringProvider automatically
  • Updates when language changes
  • Handles network fetch and caching
  • Provides fallback text

Language Switching Extension

Simple activity-level extension for quick language switching.
fun switchLanguageExample(activity: AppCompatActivity) {
    activity.changeLanguage("fr") { success ->
        if (success) {
            // All sbTextViews are already updated automatically!
        }
    }
}
One-line language switching:
// Switch to French
changeLanguage("fr") { success ->
    if (success) {
        Log.i("App", "Language switched successfully")
    }
}

Preload Language for Fast Access

Warm up the cache synchronously to ensure instant string access.
// Synchronously warm cache before UI loads
runBlocking {
    val locale = StringProvider.deviceLocale()
    StringProvider.preloadLanguage(locale, maxStrings = 500)
    Log.i("Stringboot", "📦 Preloaded ${StringProvider.getStringCount(locale)} cached strings for $locale into memory")
}
Usage in Coroutines:
lifecycleScope.launch {
    StringProvider.preloadLanguage("en", maxStrings = 500)
    // Subsequent string access will be instant (<1ms)
}

Get String Count and Cache Stats

Monitor cache performance and debug string availability.
// Get string count for locale
val count = StringProvider.getStringCount(locale)
Log.i("Stringboot", "Loaded $count strings for $locale")

// Get cache statistics
val stats = StringProvider.getCacheStats()
println("Memory: ${stats.memorySize} / ${stats.memoryMaxSize}")
println("Hit rate: ${stats.hitRate * 100}%")
println("DB entries: ${stats.dbSize}")

A/B Testing Integration

SDK v1.2.0+ includes built-in A/B testing support. The SDK automatically:
  • Generates or uses a persistent device ID for consistent experiment bucketing
  • Receives experiment assignments from the backend (V2 API)
  • Delivers the correct variant strings to users
  • Integrates with your analytics platform (Firebase, Mixpanel, Amplitude, etc.)

How It Works

  1. Device ID: SDK generates a UUID per installation (or uses your app’s device ID)
  2. X-Device-ID Header: Sent with every API request
  3. Backend Assignment: Server assigns device to experiment variants based on device ID
  4. String Resolution: SDK receives pre-resolved strings for assigned variants
  5. Analytics Tracking: SDK notifies your analytics handler of experiment assignments

Basic A/B Testing Setup

import com.stringboot.sdk.analytics.StringbootAnalyticsHandler
import com.stringboot.sdk.models.ExperimentAssignment

class StringbootApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        // SDK will generate and persist a UUID automatically
        StringbootExtensions.autoInitialize(this)
    }
}

With Custom Device ID

Use your app’s existing device ID for consistency across SDKs:
class StringbootApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        // Use your app's existing device ID for consistency
        StringbootExtensions.autoInitialize(
            context = this,
            providedDeviceId = "your-app-device-id-12345"
        )
    }
}

With Analytics Integration

Track experiment assignments in your analytics platform:
class StringbootApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        val analyticsHandler = object : StringbootAnalyticsHandler {
            override fun onExperimentsAssigned(experiments: Map<String, ExperimentAssignment>) {
                experiments.forEach { (key, assignment) ->
                    // Firebase Analytics
                    firebaseAnalytics.setUserProperty(
                        "stringboot_exp_$key",
                        assignment.variantName
                    )

                    // Mixpanel
                    mixpanel.people.set(
                        "stringboot_exp_$key",
                        assignment.variantName
                    )

                    // Amplitude
                    val identify = Identify()
                        .set("stringboot_exp_$key", assignment.variantName)
                    amplitude.identify(identify)
                }
            }
        }

        StringbootExtensions.autoInitialize(
            context = this,
            providedDeviceId = getYourAppDeviceId(),
            analyticsHandler = analyticsHandler
        )
    }

    /**
     * Optional: Provide your app's existing device ID for consistent A/B testing
     */
    private fun getYourAppDeviceId(): String? {
        // Example: Use Firebase Installation ID
        // return FirebaseInstallations.getInstance().id.await()

        // Or let SDK generate its own UUID
        return null
    }
}

Using Firebase Installation ID

import com.google.firebase.installations.FirebaseInstallations
import kotlinx.coroutines.tasks.await

private suspend fun getFirebaseDeviceId(): String? {
    return try {
        FirebaseInstallations.getInstance().id.await()
    } catch (e: Exception) {
        null
    }
}

// In Application.onCreate()
lifecycleScope.launch {
    val deviceId = getFirebaseDeviceId()
    StringbootExtensions.autoInitialize(
        context = this@StringbootApplication,
        providedDeviceId = deviceId
    )
}

Get Current Device ID

Retrieve the device ID used for A/B testing:
val deviceId = StringProvider.getDeviceId()
Log.i("Stringboot", "Device ID: $deviceId")

Debugging Experiments

The SDK logs experiment assignments in debug builds:
📦 Catalog response: 22 strings, experiments: 2
📊 Received 2 experiment(s) from server:
   • welcome-message-test → variant-a (experiment: 22ee1616-36c4-457b-8e3e-f5be39823176)
   • pricing-copy-test → variant-b (experiment: 33ff2727-47d5-568e-9a11-8ccf00678287)
   ✓ Saved experiments to persistent storage

A/B Testing Best Practices

// ✅ Use consistent device ID across all SDKs in your app
StringbootExtensions.autoInitialize(
    context = this,
    providedDeviceId = yourAppDeviceId  // Same ID for Stringboot, analytics, etc.
)

// ✅ Set analytics properties as user properties (not events)
firebaseAnalytics.setUserProperty("stringboot_exp_key", variantName)

// ✅ Let SDK handle experiment assignment
// Don't try to manually assign users to variants

// ❌ Don't generate new device IDs on each session
// Device ID must persist across app launches for consistent bucketing

// ❌ Don't use session IDs or temporary identifiers
// Use installation-level persistent IDs