androidengineers.Book a session

Scalable Architecture

Multi-tenancy & Theming/Branding

article20 minHard

Some Android apps serve multiple brands or customers from a single codebase — white-label apps, enterprise apps with per-client branding, or multi-org platforms. This is multi-tenancy at the app level.

Flavors: The Simplest Multi-Tenant Approach

For a fixed set of known tenants, Gradle product flavors let you ship separate APKs from one codebase:

// build.gradle.kts
android {
    flavorDimensions += "brand"
    productFlavors {
        create("brandA") {
            dimension = "brand"
            applicationId = "com.brandA.app"
            resValue("string", "app_name", "Brand A")
        }
        create("brandB") {
            dimension = "brand"
            applicationId = "com.brandB.app"
            resValue("string", "app_name", "Brand B")
        }
    }
}

Directory structure:

src/
  main/         ← shared code
  brandA/
    res/
      values/colors.xml    ← Brand A colors
      drawable/logo.xml    ← Brand A logo
  brandB/
    res/
      values/colors.xml    ← Brand B colors
      drawable/logo.xml    ← Brand B logo

Dynamic Theming: Runtime Branding

For apps where the brand is determined at runtime (login-time, configuration download), apply theming programmatically:

data class BrandConfig(
    val primaryColor: Int,
    val accentColor: Int,
    val logoUrl: String,
    val fontFamily: String?,
    val companyName: String
)

class ThemeManager(private val dataStore: DataStore<Preferences>) {
    val brandConfig: Flow<BrandConfig?> = dataStore.data.map { prefs ->
        val json = prefs[BRAND_CONFIG_KEY] ?: return@map null
        Json.decodeFromString<BrandConfig>(json)
    }
}

Apply theme in Activity:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        // Apply brand theme before super.onCreate() / setContentView
        val brandTheme = themeManager.getCurrentTheme()
        setTheme(brandTheme.themeResId)
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
    }
}

Material You: Dynamic Color

Android 12+ supports dynamic color from the user's wallpaper. Opt in to let the system brand your app automatically:

// In Application.onCreate or before setContentView
DynamicColors.applyToActivitiesIfAvailable(this)

For multi-tenant apps, you can override dynamic colors with brand colors:

// Programmatic token override
val brandedTheme = DynamicColorsOptions.Builder()
    .setContentBasedSource(R.color.brand_primary)
    .build()

DynamicColors.applyToActivity(this, brandedTheme)

Resource Overlays via Remote Config

For apps that receive brand configs from a server:

class RemoteBrandingManager(
    private val config: BrandConfig,
    private val context: Context
) {
    fun applyToView(view: View) {
        view.backgroundTintList = ColorStateList.valueOf(config.primaryColor)
    }

    fun applyToButton(button: Button) {
        button.backgroundTintList = ColorStateList.valueOf(config.accentColor)
        button.setTextColor(config.textOnPrimary)
    }

    fun loadLogo(imageView: ImageView) {
        imageView.load(config.logoUrl)
    }
}

Compose Multi-Tenant Theming

In Compose, the entire theme is a composable — replacing it is natural:

@Composable
fun BrandedTheme(
    brandConfig: BrandConfig,
    content: @Composable () -> Unit
) {
    val colorScheme = lightColorScheme(
        primary = Color(brandConfig.primaryColor),
        secondary = Color(brandConfig.accentColor),
        surface = Color(brandConfig.surfaceColor)
    )

    MaterialTheme(
        colorScheme = colorScheme,
        typography = brandConfig.typography ?: MaterialTheme.typography,
        content = content
    )
}

// Usage
@Composable
fun App(brandConfig: BrandConfig) {
    BrandedTheme(brandConfig) {
        MainNavHost()
    }
}

Architecture for Multi-Tenant Data Isolation

// Tenant-scoped repositories
class TenantAwareRepository(
    private val tenantId: String,
    private val api: Api
) {
    suspend fun getItems(): List<Item> = api.getItems(tenantId = tenantId)
}

// Scope tenant context via DI
@Module
@InstallIn(ActivityRetainedComponent::class)
object TenantModule {
    @Provides
    fun provideTenantId(sessionManager: SessionManager): String =
        sessionManager.currentTenantId ?: throw IllegalStateException("No tenant")

    @Provides
    fun provideTenantRepository(tenantId: String, api: Api) =
        TenantAwareRepository(tenantId, api)
}

Key Takeaways

ApproachBest for
Product flavorsFixed set of known brands at build time
Runtime theme switchingDynamic brands loaded from server
Material YouConsumer apps that want system-integrated theming
Compose MaterialThemeClean runtime rebrandable Compose apps
Tenant-scoped DIData isolation between tenants in the same binary

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Multi-tenancy & Theming/Branding | Android System Design | Android Engineers